12.6 C
New York
Wednesday, October 16, 2024

Making a responsive dashboard format for JetLagged with Jetpack Compose



Making a responsive dashboard format for JetLagged with Jetpack Compose

Posted by Rebecca Franks – Developer Relations Engineer

This weblog publish is a part of our sequence: Adaptive Highlight Week the place we offer assets—weblog posts, movies, pattern code, and extra—all designed that will help you adapt your apps to telephones, foldables, tablets, ChromeOS and even automobiles. You’ll be able to learn extra within the overview of the Adaptive Highlight Week, which might 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 properly fitted to designing and implementing layouts that modify themselves to render content material in a different way throughout a wide range of sizes. Through the use of logic coupled with Window Dimension Lessons, Circulate layouts, movableContentOf and LookaheadScope, we will 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 an attractive dashboard-like format. This text exhibits how we’ve achieved this.

Moving image demonstrating responsive design in Jetlagged where items animate positions automatically

Responsive design in Jetlagged the place gadgets animate positions routinely

Use FlowRow and FlowColumn to construct layouts that reply to totally different display sizes

Utilizing Circulate layouts ( FlowRow and FlowColumn ) make it a lot simpler to implement responsive, reflowing layouts that reply to display sizes and routinely movement content material to a brand new line when the out there area 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 area out there for the dashboard, and place every particular person card in a row or column the place area is used correctly, and on cellular units, we principally have 1 card per row, provided that the gadgets are smaller are there two seen per row.

Some playing cards leverage Modifiers that don’t specify a precise measurement, due to this fact permitting the playing cards to develop to fill the out there width, as an example utilizing Modifier.widthIn(max = 400.dp), or set a sure measurement, 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.measurement(200.dp))
    // and so on 
}

We might additionally leverage the burden modifier to divide up the remaining space of a row or column, take a look at the documentation on merchandise weights for extra info.

Use WindowSizeClasses to distinguish between units

WindowSizeClasses are helpful for build up breakpoints in our UI for when parts ought to show in a different way. In JetLagged, we use the lessons to know whether or not we must always embody playing cards in Columns or hold them flowing one after the opposite.

For instance, if WindowWidthSizeClass.COMPACT, we hold gadgets 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 totally different system sizes:

Side by side comparisons of the differeces in UI on three different sized devices

Totally different UI on totally different sized units

Use movableContentOf to take care of bits of UI state throughout display resizes

Movable content material means that you can save the contents of a Composable to maneuver it round your format hierarchy with out shedding state. It ought to be used for content material that’s perceived to be the identical – simply in a distinct location on display.

Think about this, you’re shifting home to a distinct 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 won’t be the right time of your new timezone, however it would positively have ticked on from the place you left it. The contents contained in the field don’t reset their inner state when the field is moved round.

What should you might use the identical idea in Compose to maneuver gadgets on display with out shedding their inner state?

Take the next situation into consideration: Outline totally 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
        .measurement(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 Structure – displaying the infinite animations as they go:

A purple tile stacked in a column above a pink tile. Both tiles show a counter, counting up from 0 to 100 and back down to 0

However what If we needed to put the tiles in a different way, based mostly on if the telephone is in a distinct orientation (or totally different display measurement), 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 seems to be fairly customary, however operating this on system – we will see that switching between the 2 layouts causes our animations to restart.

A purple tile stacked in a column above a pink tile. Both tiles show a counter, counting upward from 0. The column changes to a row and back to a column, and the counter restarts everytime the layout changes

That is the proper case for movable content material – it’s the identical Composables on display, they’re simply in a distinct location. So how can 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 present have.

A purple tile stacked in a column above a pink tile. Both tiles show a counter, counting upward from 0 to 100. The column changes to a row and back to a column, and the counter continues seamlessly when the layout changes

We are able to now see that our animation state is remembered throughout the totally different compositions. Our clock within the field will now hold state when it is moved around the globe.

Utilizing this idea, we will 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 permits 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.

A purple tile showing Average time in bed stacked in a column above a green tile showing average time sleep. Both tiles show moving bubbles. The column changes to a row and back to a column, and the bubbles continue to move across the tiles as the layout changes

Use Modifier.animateBounds() to have fluid animations between totally different window sizes

From the above instance, we will see that state is maintained between adjustments in format measurement (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 problem.

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 adjustments, notifying composables of the intermediate states between them. LookaheadScope can be used for the brand new shared aspect APIs, that you could 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 will see the transition between the 2 states is extra seamless with out jarring interruptions.

A purple tile showing Average time in bed stacked in a column above a green tile showing average time sleep. Both tiles show moving bubbles. The column changes to a row and back to a column, and the bubbles continue to move across the tiles as the layout changes

Making use of this logic to our entire dashboard, when resizing our format, you will note that we now have a fluid UI interplay all through the entire display.

Moving image demonstrating responsive design in Jetlagged where items animate positions automatically

Abstract

As you’ll be able to see from this text, utilizing Compose has enabled us to construct a responsive dashboard-like format by leveraging movement layouts, WindowSizeClasses, movable content material and LookaheadScope. These ideas may also be used on your personal layouts which will have gadgets shifting round in them too.

For extra info on these totally different matters, you should definitely take a look at the official documentation, for the detailed adjustments to JetLagged, check out this pull request.

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Latest Articles