Posted by Rebecca Franks – Developer Relations Engineer
This weblog submit is a part of our collection: Adaptive Highlight Week the place we offer assets—weblog posts, movies, pattern code, and extra—all designed that can assist you adapt your apps to telephones, foldables, tablets, ChromeOS and even vehicles. You may learn extra within the overview of the Adaptive Highlight Week, which shall be up to date all through the week.
We’ve heard the information, creating adaptive layouts in Jetpack Compose is simpler than ever. As a declarative UI toolkit, Jetpack Compose is effectively fitted to designing and implementing layouts that regulate themselves to render content material in another way throughout a wide range of sizes. Through the use of logic coupled with Window Dimension Courses, Move layouts, movableContentOf and LookaheadScope, we are able to guarantee fluid responsive layouts in Jetpack Compose.
Following the discharge of the JetLagged pattern at Google I/O 2023, we determined so as to add extra examples to it. Particularly, we needed to exhibit how Compose can be utilized to create a lovely dashboard-like format. This text exhibits how we’ve achieved this.
Use FlowRow and FlowColumn to construct layouts that reply to completely different display sizes
Utilizing Move layouts ( FlowRow and FlowColumn ) make it a lot simpler to implement responsive, reflowing layouts that reply to display sizes and robotically stream content material to a brand new line when the obtainable house in a row or column is full.
Within the JetLagged instance, we use a FlowRow, with a maxItemsInEachRow set to three. This ensures we maximize the house obtainable for the dashboard, and place every particular person card in a row or column the place house is used correctly, and on cell gadgets, we principally have 1 card per row, provided that the objects are smaller are there two seen per row.
Some playing cards leverage Modifiers that don’t specify a precise dimension, due to this fact permitting the playing cards to develop to fill the obtainable width, as an example utilizing Modifier.widthIn(max = 400.dp), or set a sure dimension, like Modifier.width(200.dp).
FlowRow( modifier = Modifier.fillMaxSize(), horizontalArrangement = Association.Middle, verticalArrangement = Association.Middle, maxItemsInEachRow = 3 ) { Field(modifier = Modifier.widthIn(max = 400.dp)) Field(modifier = Modifier.width(200.dp)) Field(modifier = Modifier.dimension(200.dp)) // and many others }
We may additionally leverage the load modifier to divide up the remaining space of a row or column, take a look at the documentation on merchandise weights for extra data.
Use WindowSizeClasses to distinguish between gadgets
WindowSizeClasses are helpful for build up breakpoints in our UI for when components ought to show in another way. In JetLagged, we use the lessons to know whether or not we should always embody playing cards in Columns or hold them flowing one after the opposite.
For instance, if WindowWidthSizeClass.COMPACT, we hold objects in the identical FlowRow, the place as if the format it bigger than compact, they’re positioned in a FlowColumn, nested inside a FlowRow:
FlowRow( modifier = Modifier.fillMaxSize(), horizontalArrangement = Association.Middle, verticalArrangement = Association.Middle, maxItemsInEachRow = 3 ) { JetLaggedSleepGraphCard(uiState.worth.sleepGraphData) if (windowSizeClass == WindowWidthSizeClass.COMPACT) { AverageTimeInBedCard() AverageTimeAsleepCard() } else { FlowColumn { AverageTimeInBedCard() AverageTimeAsleepCard() } } if (windowSizeClass == WindowWidthSizeClass.COMPACT) { WellnessCard(uiState.worth.wellnessData) HeartRateCard(uiState.worth.heartRateData) } else { FlowColumn { WellnessCard(uiState.worth.wellnessData) HeartRateCard(uiState.worth.heartRateData) } } }
From the above logic, the UI will seem within the following methods on completely different gadget sizes:
Use movableContentOf to keep up bits of UI state throughout display resizes
Movable content material permits you to save the contents of a Composable to maneuver it round your format hierarchy with out dropping state. It ought to be used for content material that’s perceived to be the identical – simply in a special location on display.
Think about this, you’re transferring home to a special metropolis, and also you pack a field with a clock inside it. Opening the field within the new house, you’d see that the time would nonetheless be ticking from the place it left off. It may not be the proper time of your new timezone, however it’ll undoubtedly have ticked on from the place you left it. The contents contained in the field don’t reset their inside state when the field is moved round.
What if you happen to may use the identical idea in Compose to maneuver objects on display with out dropping their inside state?
Take the next state of affairs under consideration: Outline completely different Tile composables that show an infinitely animating worth between 0 and 100 over 5000ms.
@Composable enjoyable Tile1() { val repeatingAnimation = rememberInfiniteTransition() val float = repeatingAnimation.animateFloat( initialValue = 0f, targetValue = 100f, animationSpec = infiniteRepeatable(repeatMode = RepeatMode.Reverse, animation = tween(5000)) ) Field(modifier = Modifier .dimension(100.dp) .background(purple, RoundedCornerShape(8.dp))){ Textual content("Tile 1 ${float.worth.roundToInt()}", modifier = Modifier.align(Alignment.Middle)) } }
We then show them on display utilizing a Column Format – exhibiting the infinite animations as they go:
However what If we needed to put the tiles in another way, based mostly on if the telephone is in a special orientation (or completely different display dimension), and we don’t need the animation values to cease operating? One thing like the next:
@Composable enjoyable WithoutMovableContentDemo() { val mode = keep in mind { mutableStateOf(Mode.Portrait) } if (mode.worth == Mode.Panorama) { Row { Tile1() Tile2() } } else { Column { Tile1() Tile2() } } }
This appears fairly commonplace, however operating this on gadget – we are able to see that switching between the 2 layouts causes our animations to restart.
That is the right case for movable content material – it’s the similar Composables on display, they’re simply in a special location. So how will we use it? We are able to simply outline our tiles in a movableContentOf block, utilizing keep in mind to make sure its saved throughout compositions:
val tiles = keep in mind {
movableContentOf {
Tile1()
Tile2()
}
}
Now as a substitute of calling our composables once more contained in the Column and Row respectively, we name tiles() as a substitute.
@Composable enjoyable MovableContentDemo() { val mode = keep in mind { mutableStateOf(Mode.Portrait) } val tiles = keep in mind { movableContentOf { Tile1() Tile2() } } Field(modifier = Modifier.fillMaxSize()) { if (mode.worth == Mode.Panorama) { Row { tiles() } } else { Column { tiles() } } Button(onClick = { if (mode.worth == Mode.Portrait) { mode.worth = Mode.Panorama } else { mode.worth = Mode.Portrait } }, modifier = Modifier.align(Alignment.BottomCenter)) { Textual content("Change format") } } }
It will then keep in mind the nodes generated by these Composables and protect the inner state that these composables at the moment have.
We are able to now see that our animation state is remembered throughout the completely different compositions. Our clock within the field will now hold state when it is moved all over the world.
Utilizing this idea, we are able to hold the animating bubble state of our playing cards, by putting the playing cards in movableContentOf:
Language
val timeSleepSummaryCards = keep in mind { movableContentOf { AverageTimeInBedCard() AverageTimeAsleepCard() } } LookaheadScope { FlowRow( modifier = Modifier.fillMaxSize(), horizontalArrangement = Association.Middle, verticalArrangement = Association.Middle, maxItemsInEachRow = 3 ) { //.. if (windowSizeClass == WindowWidthSizeClass.Compact) { timeSleepSummaryCards() } else { FlowColumn { timeSleepSummaryCards() } } // } }
This enables the playing cards state to be remembered and the playing cards will not be recomposed. That is evident when observing the bubbles within the background of the playing cards, on resizing the display the bubble animation continues with out restarting the animation.
Use Modifier.animateBounds() to have fluid animations between completely different window sizes
From the above instance, we are able to see that state is maintained between modifications in format dimension (or format itself), however the distinction between the 2 layouts is a bit jarring. We’d like this to animate between the 2 states with out challenge.
Within the newest compose-bom-alpha (2024.09.03), there’s a new experimental customized Modifier, Modifier.animateBounds(). The animateBounds modifier requires a LookaheadScope.
LookaheadScope allows Compose to carry out intermediate measurement passes of format modifications, notifying composables of the intermediate states between them. LookaheadScope can also be used for the brand new shared factor APIs, that you might have seen just lately.
To make use of Modifier.animateBounds(), we wrap the top-level FlowRow in a LookaheadScope, after which apply the animateBounds modifier to every card. We are able to additionally customise how the animation runs, by specifying the boundsTransform parameter to a customized spring spec:
val boundsTransform = { _ : Rect, _: Rect -> spring( dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessMedium, visibilityThreshold = Rect.VisibilityThreshold ) } LookaheadScope { val animateBoundsModifier = Modifier.animateBounds( lookaheadScope = this@LookaheadScope, boundsTransform = boundsTransform) val timeSleepSummaryCards = keep in mind { movableContentOf { AverageTimeInBedCard(animateBoundsModifier) AverageTimeAsleepCard(animateBoundsModifier) } } FlowRow( modifier = Modifier .fillMaxSize() .windowInsetsPadding(insets), horizontalArrangement = Association.Middle, verticalArrangement = Association.Middle, maxItemsInEachRow = 3 ) { JetLaggedSleepGraphCard(uiState.worth.sleepGraphData, animateBoundsModifier.widthIn(max = 600.dp)) if (windowSizeClass == WindowWidthSizeClass.Compact) { timeSleepSummaryCards() } else { FlowColumn { timeSleepSummaryCards() } } FlowColumn { WellnessCard( wellnessData = uiState.worth.wellnessData, modifier = animateBoundsModifier .widthIn(max = 400.dp) .heightIn(min = 200.dp) ) HeartRateCard( modifier = animateBoundsModifier .widthIn(max = 400.dp, min = 200.dp), uiState.worth.heartRateData ) } } }
Making use of this to our format, we are able to see the transition between the 2 states is extra seamless with out jarring interruptions.
Making use of this logic to our complete dashboard, when resizing our format, you will note that we now have a fluid UI interplay all through the entire display.
Abstract
As you possibly can see from this text, utilizing Compose has enabled us to construct a responsive dashboard-like format by leveraging stream layouts, WindowSizeClasses, movable content material and LookaheadScope. These ideas will also be used to your personal layouts which will have objects transferring round in them too.
For extra data on these completely different subjects, you’ll want to take a look at the official documentation, for the detailed modifications to JetLagged, check out this pull request.