DEV Community

Cover image for Weather forecast card design using Jetpack Compose
Mahendran
Mahendran

Posted on • Edited on • Originally published at mahendranv.github.io

2 1

Weather forecast card design using Jetpack Compose

Cross posting from my blog

Weather forecast card design using Jetpack Compose

Horizontal weather cards are the second portion in my forecast screen. It contains a message, relative timestamp and an image depicting the weather. Since I don't have images in handy, I picked one and used for all the cards. I got creative with messages though.

img


Why box layout?

We have card and an image stacked in z order and they overlap. A constraint layout can be used to build this card, but I wanted to try box. It's like FrameLayout to me. Also using constraint layout for simple layout is an overkill. Be it android native UI or Compose.


Designing a Weather Card

The overall composable blueprint looks like in below image. To understand better, let's build this layout bottom-up.

image-20210515164325569

...

Card content

// card content
Column(modifier = Modifier.padding(16.dp)) {

    Spacer(modifier = Modifier.height(24.dp))

    // Time
    Text(
        text = time, // "20 minutes ago",
        style = MaterialTheme.typography.caption
    )

    Spacer(modifier = Modifier.height(8.dp))

    // Message
    Text(
        text = message, // "If you don't want to get wet today, don't forget your umbrella.",
        style = MaterialTheme.typography.body1
    )

    Spacer(modifier = Modifier.height(24.dp))
}
Enter fullscreen mode Exit fullscreen mode

Two labels wrapped in a Column with spacer makes our card content. Spacers are added in between the elements to make up required spacing between the labels and the parent (Column).

image-20210515165656824

...

Card design

Now put a card around it and apply few modifiers to it.

  • a shape — RoundedCornerShape
  • top padding — to make space for weather icon on top right
Card(
  shape = RoundedCornerShape(16.dp),
  modifier = Modifier
        .padding(top = 40.dp, start = 16.dp, end = 16.dp, bottom = 8.dp)
) {
  Column(...)
}
Enter fullscreen mode Exit fullscreen mode

image-20210515171455191

...

Putting all in a Box

Box layout stacks all the elements in given z order. Most recent one will be on the top. So, let's put the Card first then the Image.

Box { 
    Card(...)
    Image(
        painter = painterResource(id = R.drawable.cloudy),
        contentDescription = "weather overlap image",
        modifier = Modifier
            .size(100.dp)
    )
}   
Enter fullscreen mode Exit fullscreen mode

image-20210515172249253

Hmm... The rain wasn't supposed to shower there. Let's pull it to the right and shift a bit to the left. Children of Box has access to few properties to help with alignment inside the container.

  • alignment — Alignment.TopEnd
  • offset — x = (-40).dp negative translation in horizontal direction
Image(
    painter = painterResource(id = R.drawable.cloudy),
    contentDescription = "weather overlap image",
    modifier = Modifier
        .size(100.dp)
        .align(alignment = Alignment.TopEnd)
        .offset(x = (-40).dp)
)
Enter fullscreen mode Exit fullscreen mode

image-20210515180739796


Horizontal pager implementation

Google accompanist has an incredibly good set of extension for compose. To implement the pager & indicator, we need corresponding dependencies added to our codebase.

implementation "com.google.accompanist:accompanist-pager:0.9.0"
implementation "com.google.accompanist:accompanist-pager-indicators:0.9.0"
Enter fullscreen mode Exit fullscreen mode

Next step is to wrap the pager and indicator inside a Column and provide a list of mock objects to the render in WeatherCard that we built in above section.

fun WeatherCardCarousal(cards: List<WeatherCard>) {
    val pagerState = rememberPagerState(pageCount = cards.size)
    Column {
        HorizontalPager(
            state = pagerState
        ) { page ->
            WeatherUpdateCard(cards[page])
        }

        HorizontalPagerIndicator(
            pagerState = pagerState,
            modifier = Modifier
                .align(Alignment.CenterHorizontally)
                .padding(16.dp),
        )
    }
}
Enter fullscreen mode Exit fullscreen mode
  • pagerState provides the current index of element for the HorizontalPager

  • WeatherUpdateCard(cards[page]) with the given index, corresponding element picked from mock data and rendered in WeatherCard

  • HorizontalPagerIndicator placed below the pager to react with page swipes in Pager. If it needs to be overlapped on the Pager. We'll wrap them inside a Box and align it.

This is again an example for thinking in compose. Pager updates the pagerState, and PagerIndicator reads the same. Both of 'em doesn't know other component exist. Decoupled and yet communicating.


Here, the complete gist.

//// Pager gradle dependency ////
implementation "com.google.accompanist:accompanist-pager:0.9.0"
implementation "com.google.accompanist:accompanist-pager-indicators:0.9.0"
// Data model
data class WeatherCard(
val time: String,
val message: String,
)
// Single card
@Composable
fun WeatherUpdateCard(weatherCard: WeatherCard) {
Box {
Card(
shape = RoundedCornerShape(16.dp),
modifier = Modifier
.padding(top = 40.dp, start = 16.dp, end = 16.dp, bottom = 8.dp)
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Spacer(modifier = Modifier.height(24.dp))
Text(
text = weatherCard.time, // "20 minutes ago",
style = MaterialTheme.typography.caption
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = weatherCard.message, // "If you don't want to get wet today, don't forget your umbrella.",
style = MaterialTheme.typography.body1
)
Spacer(modifier = Modifier.height(24.dp))
}
}
Image(
painter = painterResource(id = R.drawable.cloudy),
contentDescription = "weather overlap image",
modifier = Modifier
.size(100.dp)
.align(alignment = Alignment.TopEnd)
.offset(x = (-40).dp)
)
}
}
// Carousel
@OptIn(ExperimentalPagerApi::class)
@Composable
fun WeatherCardCarousel(cards: List<WeatherCard>) {
val pagerState = rememberPagerState(pageCount = cards.size)
Column {
HorizontalPager(
state = pagerState
) { page ->
WeatherUpdateCard(cards[page])
}
HorizontalPagerIndicator(
pagerState = pagerState,
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(16.dp),
)
}
}
//// mock data /////
val weatherCards = listOf(
WeatherCard(
time = "2 minutes ago",
message = "The air quality is ideal for most individuals; enjoy your normal outdoor activities and be safe "
),
WeatherCard(
time = "7 minutes ago",
message = "The maximum temperature recorded in the city on Wednesday was 40.5 degrees Celsius and it's climbing"
),
WeatherCard(
time = "15 minutes ago",
message = "The wet weather will also have a cooling effect on the northeastern region for the next five days."
),
WeatherCard(
time = "38 minutes ago",
message = "The IMD has forecast rain in the capital on Thursday evening, and also on Tuesday, Monday next week"
),
)
view raw WeatherCard.kt hosted with ❤ by GitHub

Happy composing!!

Sentry growth stunted Image

If you are wasting time trying to track down the cause of a crash, it’s time for a better solution. Get your crash rates to zero (or close to zero as possible) with less time and effort.

Try Sentry for more visibility into crashes, better workflow tools, and customizable alerts and reporting.

Switch Tools 🔁

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs