{"id":262523,"date":"2024-10-16T20:02:57","date_gmt":"2024-10-16T20:02:57","guid":{"rendered":"https:\/\/michigandigitalnews.com\/index.php\/2024\/10\/16\/creating-a-responsive-dashboard-layout-for-jetlagged-with-jetpack-compose\/"},"modified":"2025-06-25T17:10:50","modified_gmt":"2025-06-25T17:10:50","slug":"creating-a-responsive-dashboard-layout-for-jetlagged-with-jetpack-compose","status":"publish","type":"post","link":"https:\/\/michigandigitalnews.com\/index.php\/2024\/10\/16\/creating-a-responsive-dashboard-layout-for-jetlagged-with-jetpack-compose\/","title":{"rendered":"Creating a responsive dashboard layout for JetLagged with Jetpack Compose"},"content":{"rendered":"<p> [ad_1]<br \/>\n<\/p>\n<div>\n<meta content=\"https:\/\/blogger.googleusercontent.com\/img\/b\/R29vZ2xl\/AVvXsEhvmObDTm_UvKKZP8bDM_YVpknfPh2MBo1pZun6Vc_0CTOsoxvFAq0O_tIa33owHNckOXqKEGGf35C7PccmUFU9gA9VX7djih13zWISVNwdqAHY7waJ7tcWoZJUSuCwSnrLCmhQzNCr21e61IJ_8PLUzj6eKApPrxmK-cwMHrYp-iCbQ6XPyiLxIKvRZPU\/s1600\/Jetpack%20Adaptive%20Compose%20Metadata.png\" name=\"twitter:image\"\/><br \/>\n<img decoding=\"async\" src=\"https:\/\/blogger.googleusercontent.com\/img\/b\/R29vZ2xl\/AVvXsEhvmObDTm_UvKKZP8bDM_YVpknfPh2MBo1pZun6Vc_0CTOsoxvFAq0O_tIa33owHNckOXqKEGGf35C7PccmUFU9gA9VX7djih13zWISVNwdqAHY7waJ7tcWoZJUSuCwSnrLCmhQzNCr21e61IJ_8PLUzj6eKApPrxmK-cwMHrYp-iCbQ6XPyiLxIKvRZPU\/s1600\/Jetpack%20Adaptive%20Compose%20Metadata.png\" style=\"display: none;\"\/><\/p>\n<p><em>Posted by  Rebecca Franks &#8211; Developer Relations Engineer<\/em><\/p>\n<p><a href=\"https:\/\/blogger.googleusercontent.com\/img\/b\/R29vZ2xl\/AVvXsEiq-xjyTG_ks5AfowEaHg8NFwpTVt0EqH59N_7RExbYOHp8WESDEmFbRFPB7kc3_tP0B7XGARTbDvX6woNwZgRtwX2xH8xJI0uq4w7T3b2JFOjzB3qfU6EZC-MpYSZ537orOmLoI5VY69Fc-h9XMiJ3MRe7rk66yTMcEdDxSWlrOY71cQ04-kxG-kFalms\/s1600\/Jetpack%20Adaptive%20Compose%20%281%29.png\"><img decoding=\"async\" border=\"0\" data-original-height=\"800\" data-original-width=\"100%\" src=\"https:\/\/blogger.googleusercontent.com\/img\/b\/R29vZ2xl\/AVvXsEiq-xjyTG_ks5AfowEaHg8NFwpTVt0EqH59N_7RExbYOHp8WESDEmFbRFPB7kc3_tP0B7XGARTbDvX6woNwZgRtwX2xH8xJI0uq4w7T3b2JFOjzB3qfU6EZC-MpYSZ537orOmLoI5VY69Fc-h9XMiJ3MRe7rk66yTMcEdDxSWlrOY71cQ04-kxG-kFalms\/s1600\/Jetpack%20Adaptive%20Compose%20%281%29.png\"\/><\/a><\/p>\n<p><i>This blog post is part of our series: Adaptive Spotlight Week where we provide resources\u2014blog posts, videos, sample code, and more\u2014all designed to help you adapt your apps to phones, foldables, tablets, ChromeOS and even cars. You can <a href=\"https:\/\/android-developers.googleblog.com\/2024\/10\/adaptive-spotlight-week.html\" target=\"_blank\" rel=\"noopener\">read more in the overview of the Adaptive Spotlight Week<\/a>, which will be updated throughout the week.<\/i><\/p>\n<hr\/>\n<p>We\u2019ve heard the news, <a href=\"https:\/\/developer.android.com\/jetpack\/compose\/layouts\/adaptive\" target=\"_blank\" rel=\"noopener\">creating adaptive layouts<\/a> in Jetpack Compose is easier than ever. As a declarative UI toolkit, Jetpack Compose is well suited for designing and implementing layouts that adjust themselves to render content differently across a variety of sizes. By using logic coupled with <a href=\"https:\/\/developer.android.com\/guide\/topics\/large-screens\/support-different-screen-sizes#window_size_classes\" target=\"_blank\" rel=\"noopener\">Window Size Classes<\/a>, <a href=\"https:\/\/developer.android.com\/jetpack\/compose\/layouts\/flow\" target=\"_blank\" rel=\"noopener\">Flow layouts<\/a>, <a href=\"https:\/\/developer.android.com\/reference\/kotlin\/androidx\/compose\/runtime\/package-summary#movableContentOf%28kotlin.Function4%29\" target=\"_blank\" rel=\"noopener\">movableContentOf<\/a> and <a href=\"https:\/\/developer.android.com\/reference\/kotlin\/androidx\/compose\/ui\/layout\/LookaheadScope\" target=\"_blank\" rel=\"noopener\">LookaheadScope<\/a>, we can ensure fluid responsive layouts in Jetpack Compose.<\/p>\n<p>Following the release of the <a href=\"https:\/\/github.com\/android\/compose-samples\/tree\/main\/JetLagged\" target=\"_blank\" rel=\"noopener\">JetLagged<\/a> sample at Google I\/O 2023, we decided to add more examples to it. Specifically, we wanted to demonstrate how Compose can be used to create a beautiful dashboard-like layout. This article shows how we\u2019ve achieved this.<\/p>\n<p><image><a href=\"https:\/\/blogger.googleusercontent.com\/img\/b\/R29vZ2xl\/AVvXsEjgeCcBP6eF2jTchTqeULZJXeCaCvA92M-cvj-8FQwEQxKD0DffQSDByWWdZ_ZfrG-2zhTakNjvLfye_DmwmwEEO7qK_-pZ-Wb4IZqRBFQoiU6f92hSUd_kqJHbIwRFhtgB3IsBalN-XN0PH1TQmQhATewShhu-URaUpKHoT5HJwdXVAacRY-KdxhSW5F0\/s1600\/overall_interaction_flow_jetlagged.gif\" target=\"_blank\" rel=\"noopener\"><\/p>\n<div style=\"text-align: center;\"><img decoding=\"async\" alt=\"Moving image demonstrating responsive design in Jetlagged where items animate positions automatically\" border=\"0\" height=\"360\" id=\"imgCaption\" src=\"https:\/\/blogger.googleusercontent.com\/img\/b\/R29vZ2xl\/AVvXsEjgeCcBP6eF2jTchTqeULZJXeCaCvA92M-cvj-8FQwEQxKD0DffQSDByWWdZ_ZfrG-2zhTakNjvLfye_DmwmwEEO7qK_-pZ-Wb4IZqRBFQoiU6f92hSUd_kqJHbIwRFhtgB3IsBalN-XN0PH1TQmQhATewShhu-URaUpKHoT5HJwdXVAacRY-KdxhSW5F0\/s1600\/overall_interaction_flow_jetlagged.gif\" width=\"100%\"\/><\/div>\n<p><\/a><imgcaption><center><em>Responsive design in Jetlagged where items animate positions automatically<\/em><\/center><\/imgcaption><\/image><\/p>\n<h3>Use FlowRow and FlowColumn to build layouts that respond to different screen sizes<\/h3>\n<p>Using <a href=\"https:\/\/developer.android.com\/jetpack\/compose\/layouts\/flow\" target=\"_blank\" rel=\"noopener\">Flow layouts<\/a> ( <span style=\"color: #0d904f; font-family: Courier;\">FlowRow<\/span> and <span style=\"color: #0d904f; font-family: Courier;\">FlowColumn<\/span> ) make it much easier to implement responsive, reflowing layouts that respond to screen sizes and automatically flow content to a new line when the available space in a row or column is full.<\/p>\n<p>In the JetLagged example, we use a <span style=\"color: #0d904f; font-family: Courier;\">FlowRow<\/span>, with a <span style=\"color: #0d904f; font-family: Courier;\">maxItemsInEachRow<\/span> set to 3. This ensures we maximize the space available for the dashboard, and place each individual card in a row or column where space is used wisely, and on mobile devices, we mostly have 1 card per row, only if the items are smaller are there two visible per row.<\/p>\n<p>Some cards leverage Modifiers that don\u2019t specify an exact size, therefore allowing the cards to grow to fill the available width, for instance using <span style=\"color: #0d904f; font-family: Courier;\">Modifier.widthIn(max = 400.dp)<\/span>, or set a certain size, like <span style=\"color: #0d904f; font-family: Courier;\">Modifier.width(200.dp)<\/span>.<\/p>\n<div style=\"background: rgb(248, 248, 248); border: 0px; overflow: auto; width: auto;\">\n<pre style=\"line-height: 125%; margin: 0px;\">FlowRow(\n    modifier = Modifier.fillMaxSize(),\n    horizontalArrangement = Arrangement.Center,\n    verticalArrangement = Arrangement.Center,\n    maxItemsInEachRow = <span style=\"color: #666666;\">3<\/span>\n) {\n    Box(modifier = Modifier.widthIn(max = <span style=\"color: #666666;\">400.d<\/span>p))\n    Box(modifier = Modifier.width(<span style=\"color: #666666;\">200.d<\/span>p))\n    Box(modifier = Modifier.size(<span style=\"color: #666666;\">200.d<\/span>p))\n    <span style=\"color: #408080; font-style: italic;\">\/\/ etc <\/span>\n}\n<\/pre>\n<\/div>\n<p>We could also leverage the weight modifier to divide up the remaining area of a row or column, check out the documentation on <a href=\"https:\/\/developer.android.com\/jetpack\/compose\/layouts\/flow#item-weights\" target=\"_blank\" rel=\"noopener\">item weights<\/a> for more information.<\/p>\n<p><\/p>\n<h3>Use WindowSizeClasses to differentiate between devices<\/h3>\n<p><a href=\"https:\/\/developer.android.com\/guide\/topics\/large-screens\/support-different-screen-sizes#window_size_classes\" target=\"_blank\" rel=\"noopener\">WindowSizeClasses<\/a> are useful for building up breakpoints in our UI for when elements should display differently. In JetLagged, we use the classes to know whether we should include cards in Columns or keep them flowing one after the other.<\/p>\n<p>For example, if <span style=\"color: #0d904f; font-family: Courier;\">WindowWidthSizeClass.COMPACT<\/span>, we keep items in the same <span style=\"color: #0d904f; font-family: Courier;\">FlowRow<\/span>, where as if the layout it larger than compact, they are placed in a <span style=\"color: #0d904f; font-family: Courier;\">FlowColumn<\/span>, nested inside a <span style=\"color: #0d904f; font-family: Courier;\">FlowRow<\/span>:<\/p>\n<div style=\"background: rgb(248, 248, 248); border: 0px; overflow: auto; width: auto;\">\n<pre style=\"line-height: 125%; margin: 0px;\">            FlowRow(\n                modifier = Modifier.fillMaxSize(),\n                horizontalArrangement = Arrangement.Center,\n                verticalArrangement = Arrangement.Center,\n                maxItemsInEachRow = <span style=\"color: #666666;\">3<\/span>\n            ) {\n                JetLaggedSleepGraphCard(uiState.value.sleepGraphData)\n                <span style=\"color: green; font-weight: bold;\">if<\/span> (windowSizeClass == WindowWidthSizeClass.COMPACT) {\n                    AverageTimeInBedCard()\n                    AverageTimeAsleepCard()\n                } <span style=\"color: green; font-weight: bold;\">else<\/span> {\n                    FlowColumn {\n                        AverageTimeInBedCard()\n                        AverageTimeAsleepCard()\n                    }\n                }\n                <span style=\"color: green; font-weight: bold;\">if<\/span> (windowSizeClass == WindowWidthSizeClass.COMPACT) {\n                    WellnessCard(uiState.value.wellnessData)\n                    HeartRateCard(uiState.value.heartRateData)\n                } <span style=\"color: green; font-weight: bold;\">else<\/span> {\n                    FlowColumn {\n                        WellnessCard(uiState.value.wellnessData)\n                        HeartRateCard(uiState.value.heartRateData)\n                    }\n                }\n            }\n<\/pre>\n<\/div>\n<p>From the above logic, the UI will appear in the following ways on different device sizes:<\/p>\n<p><image><a href=\"https:\/\/blogger.googleusercontent.com\/img\/b\/R29vZ2xl\/AVvXsEiJDm1eVXzW0-0LgAnurf0aP2nw0JDs1n-QFQzU-MJVKACvRrRNCAi5WOwtcjk8kyyDH9QE4QmyVVTQxOOQug81Ax8sSdNl0jwqaDcU8h4-Ndvckc63tqj2d0L0O2azk4kTVQutYfkOj6HbTvgclw08XL1X_uvILNjZjjvfwfHpqnePe1sWt59SBLHfY3s\/s1600\/all_screens.png\" target=\"_blank\" rel=\"noopener\"><\/p>\n<div style=\"text-align: center;\"><img decoding=\"async\" alt=\"Side by side comparisons of the differeces in UI on three different sized devices\" border=\"0\" height=\"186\" id=\"imgCaption\" src=\"https:\/\/blogger.googleusercontent.com\/img\/b\/R29vZ2xl\/AVvXsEiJDm1eVXzW0-0LgAnurf0aP2nw0JDs1n-QFQzU-MJVKACvRrRNCAi5WOwtcjk8kyyDH9QE4QmyVVTQxOOQug81Ax8sSdNl0jwqaDcU8h4-Ndvckc63tqj2d0L0O2azk4kTVQutYfkOj6HbTvgclw08XL1X_uvILNjZjjvfwfHpqnePe1sWt59SBLHfY3s\/s1600\/all_screens.png\" width=\"100%\"\/><\/div>\n<p><\/a><imgcaption><center><em>Different UI on different sized devices<\/em><\/center><\/imgcaption><\/image><\/p>\n<h3>Use movableContentOf to maintain bits of UI state across screen resizes<\/h3>\n<p>Movable content allows you to save the contents of a Composable to move it around your layout hierarchy without losing state. It should be used for content that is perceived to be the same &#8211; just in a different location on screen.<\/p>\n<p>Imagine this, you are moving house to a different city, and you pack a box with a clock inside of it. Opening the box in the new home, you\u2019d see that the time would still be ticking from where it left off. It might not be the correct time of your new timezone, but it will definitely have ticked on from where you left it. The contents inside the box don\u2019t reset their internal state when the box is moved around.<\/p>\n<p>What if you could use the same concept in Compose to move items on screen without losing their internal state?<\/p>\n<p>Take the following scenario into account: Define different <span style=\"color: #0d904f; font-family: Courier;\">Tile<\/span> composables that display an infinitely animating value between 0 and 100 over 5000ms.<\/p>\n<p><\/p>\n<div style=\"background: rgb(248, 248, 248); border: 0px; overflow: auto; width: auto;\">\n<pre style=\"line-height: 125%; margin: 0px;\">@Composable\n<span style=\"color: green; font-weight: bold;\">fun<\/span> <span style=\"color: blue;\">Tile1<\/span>() {\n    <span style=\"color: green; font-weight: bold;\">val<\/span> repeatingAnimation = rememberInfiniteTransition()\n\n    <span style=\"color: green; font-weight: bold;\">val<\/span> <span style=\"color: #b00040;\">float<\/span> = repeatingAnimation.animateFloat(\n        initialValue = <span style=\"color: #666666;\">0f<\/span>,\n        targetValue = <span style=\"color: #666666;\">100f<\/span>,\n        animationSpec = infiniteRepeatable(repeatMode = RepeatMode.Reverse,\n            animation = tween(<span style=\"color: #666666;\">5000<\/span>))\n    )\n    Box(modifier = Modifier\n        .size(<span style=\"color: #666666;\">100.d<\/span>p)\n        .background(purple, RoundedCornerShape(<span style=\"color: #666666;\">8.d<\/span>p))){\n        Text(<span style=\"color: #ba2121;\">\"Tile 1 ${float.value.roundToInt()}\"<\/span>,\n            modifier = Modifier.align(Alignment.Center))\n    }\n}\n<\/pre>\n<\/div>\n<p>We then display them on screen using a Column Layout &#8211; showing the infinite animations as they go:<\/p>\n<p><image><\/p>\n<div style=\"text-align: center;\"><img fetchpriority=\"high\" decoding=\"async\" alt=\"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\" border=\"0\" height=\"359\" id=\"imgCaption\" src=\"https:\/\/blogger.googleusercontent.com\/img\/b\/R29vZ2xl\/AVvXsEhuE6wj7uJqjKHASh_Hcqr-GkFYJDNbSgJUaVvTvfJ_9_9eT-wpmhKDSmfyWK4Q57HGNvqBEgBxYiH0LlhCvfn90kcdsjZnp62IzU9U34aPoEK4nZuVb4SqvYgQxgdOTCVI2fZrz_jj_t5Bwc_XhMo7Tn6A89BeG7ho_Zw0D77ofINhoh7eIIlpGt1H9s0\/w640-h359\/image2.gif\" width=\"640\"\/><\/div>\n<p><\/image><\/p>\n<p>But what If we wanted to lay the tiles differently, based on if the phone is in a different orientation (or different screen size), and we don\u2019t want the animation values to stop running? Something like the following:<\/p>\n<div style=\"background: rgb(248, 248, 248); border: 0px; overflow: auto; width: auto;\">\n<pre style=\"line-height: 125%; margin: 0px;\">@Composable\n<span style=\"color: green; font-weight: bold;\">fun<\/span> <span style=\"color: blue;\">WithoutMovableContentDemo<\/span>() {\n    <span style=\"color: green; font-weight: bold;\">val<\/span> mode = remember {\n        mutableStateOf(Mode.Portrait)\n    }\n    <span style=\"color: green; font-weight: bold;\">if<\/span> (mode.value == Mode.Landscape) {\n        Row {\n           Tile1()\n           Tile2()\n        }\n    } <span style=\"color: green; font-weight: bold;\">else<\/span> {\n        Column {\n           Tile1()\n           Tile2()\n        }\n    }\n}\n<\/pre>\n<\/div>\n<p>This looks pretty standard, but running this on device &#8211; we can see that switching between the two layouts causes our animations to restart.<\/p>\n<p><image><a href=\"https:\/\/blogger.googleusercontent.com\/img\/b\/R29vZ2xl\/AVvXsEisOot3wdj0XzrKJUex2AtwlPUKEDPzbnoJoCTX6JH02mIoOFVSsYZ6HpK9M68YDmtEfvwVzp3gdW7pJcNDv2MSyJ9F_F26fMzGV2I17QiTHUcpI4vC7iGwEujnnP4KF5vtZLEHacaxgZkhQxRdMggDk0mktV5t4fr9moyJnfluiQG3bvnaWU6jCtu1kRo\/w640-h359\/image1.gif\" target=\"_blank\" rel=\"noopener\"><\/p>\n<div style=\"text-align: center;\"><img decoding=\"async\" alt=\"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\" border=\"0\" height=\"359\" id=\"imgCaption\" src=\"https:\/\/blogger.googleusercontent.com\/img\/b\/R29vZ2xl\/AVvXsEisOot3wdj0XzrKJUex2AtwlPUKEDPzbnoJoCTX6JH02mIoOFVSsYZ6HpK9M68YDmtEfvwVzp3gdW7pJcNDv2MSyJ9F_F26fMzGV2I17QiTHUcpI4vC7iGwEujnnP4KF5vtZLEHacaxgZkhQxRdMggDk0mktV5t4fr9moyJnfluiQG3bvnaWU6jCtu1kRo\/w640-h359\/image1.gif\" width=\"640\"\/><\/div>\n<p><\/a><\/image><\/p>\n<p>This is the perfect case for movable content &#8211; it is the same Composables on screen, they are just in a different location. So how do we use it? We can just define our tiles in a <span style=\"font-family: Courier;\"><a href=\"https:\/\/developer.android.com\/reference\/kotlin\/androidx\/compose\/runtime\/package-summary#movableContentOf%28kotlin.Function0%29\" target=\"_blank\" rel=\"noopener\">movableContentOf<\/a><\/span> block, using <span style=\"font-family: Courier;\"><a href=\"https:\/\/developer.android.com\/reference\/kotlin\/androidx\/compose\/runtime\/package-summary#remember%28kotlin.Any,kotlin.Any,kotlin.Any,kotlin.Function0%29\" target=\"_blank\" rel=\"noopener\">remember<\/a><\/span> to ensure its saved across compositions:<\/p>\n<div style=\"background: rgb(248, 248, 248); border: 0px; overflow: auto; width: auto;\">\n<pre style=\"line-height: 125%; margin: 0px;\"><span style=\"color: green; font-weight: bold;\">val<\/span> tiles = remember {\n        movableContentOf {\n            Tile1()\n            Tile2()\n        }\n }\n<\/pre>\n<\/div>\n<p>Now instead of calling our composables again inside the <span style=\"color: #0d904f; font-family: Courier;\">Column<\/span> and <span style=\"color: #0d904f; font-family: Courier;\">Row<\/span> respectively, we call <span style=\"color: #0d904f; font-family: Courier;\">tiles()<\/span> instead.<\/p>\n<div style=\"background: rgb(248, 248, 248); border: 0px; overflow: auto; width: auto;\">\n<pre style=\"line-height: 125%; margin: 0px;\">@Composable\n<span style=\"color: green; font-weight: bold;\">fun<\/span> <span style=\"color: blue;\">MovableContentDemo<\/span>() {\n    <span style=\"color: green; font-weight: bold;\">val<\/span> mode = remember {\n        mutableStateOf(Mode.Portrait)\n    }\n    <span style=\"color: green; font-weight: bold;\">val<\/span> tiles = remember {\n        movableContentOf {\n            Tile1()\n            Tile2()\n        }\n    }\n    Box(modifier = Modifier.fillMaxSize()) {\n        <span style=\"color: green; font-weight: bold;\">if<\/span> (mode.value == Mode.Landscape) {\n            Row {\n                tiles()\n            }\n        } <span style=\"color: green; font-weight: bold;\">else<\/span> {\n            Column {\n                tiles()\n            }\n        }\n\n        Button(onClick = {\n            <span style=\"color: green; font-weight: bold;\">if<\/span> (mode.value == Mode.Portrait) {\n                mode.value = Mode.Landscape\n            } <span style=\"color: green; font-weight: bold;\">else<\/span> {\n                mode.value = Mode.Portrait\n            }\n        }, modifier = Modifier.align(Alignment.BottomCenter)) {\n            Text(<span style=\"color: #ba2121;\">\"Change layout\"<\/span>)\n        }\n    }\n}\n<\/pre>\n<\/div>\n<p>This will then remember the nodes generated by those Composables and preserve the internal state that these composables currently have.<\/p>\n<p><image><a href=\"https:\/\/blogger.googleusercontent.com\/img\/b\/R29vZ2xl\/AVvXsEhZpgt3pKBqtSV5_FnN_1gzaUeWg5_gaHooyPkNUnrOZyF07mw50rCufBU2ccXOktYEexBEARo5J6EncTfzL1d3e3O9ABFMBvJ87hDi6Su9lydkAOkjIDndhLJkeYiBkJXwbb7NucDgLktrtM73MalRL9JODkiejjdZLIuUp3qbEUCFmikmEnAptVvbufM\/w640-h359\/image6.gif\" target=\"_blank\" rel=\"noopener\"><\/p>\n<div style=\"text-align: center;\"><img decoding=\"async\" alt=\"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\" border=\"0\" height=\"359\" id=\"imgCaption\" src=\"https:\/\/blogger.googleusercontent.com\/img\/b\/R29vZ2xl\/AVvXsEhZpgt3pKBqtSV5_FnN_1gzaUeWg5_gaHooyPkNUnrOZyF07mw50rCufBU2ccXOktYEexBEARo5J6EncTfzL1d3e3O9ABFMBvJ87hDi6Su9lydkAOkjIDndhLJkeYiBkJXwbb7NucDgLktrtM73MalRL9JODkiejjdZLIuUp3qbEUCFmikmEnAptVvbufM\/w640-h359\/image6.gif\" width=\"640\"\/><\/div>\n<p><\/a><\/image><\/p>\n<p>We can now see that our animation state is remembered across the different compositions. Our clock in the box will now keep state when it&#8217;s moved around the world. <\/p>\n<p>Using this concept, we can keep the animating bubble state of our cards, by placing the cards in <span style=\"color: #0d904f; font-family: Courier;\">movableContentOf<\/span>:<\/p>\n<div style=\"background: rgb(248, 248, 248); border: 0px; overflow: auto; width: auto;\">\n<pre style=\"line-height: 125%; margin: 0px;\"><b>Language<\/b><br\/><span style=\"color: green; font-weight: bold;\">val<\/span> timeSleepSummaryCards = remember {\n            movableContentOf {\n                AverageTimeInBedCard()\n                AverageTimeAsleepCard()\n            }\n        }\n        LookaheadScope {\n            FlowRow(\n                modifier = Modifier.fillMaxSize(),\n                horizontalArrangement = Arrangement.Center,\n                verticalArrangement = Arrangement.Center,\n                maxItemsInEachRow = <span style=\"color: #666666;\">3<\/span>\n            ) {\n                <span style=\"color: #408080; font-style: italic;\">\/\/..<\/span>\n                <span style=\"color: green; font-weight: bold;\">if<\/span> (windowSizeClass == WindowWidthSizeClass.Compact) {\n                    timeSleepSummaryCards()\n                } <span style=\"color: green; font-weight: bold;\">else<\/span> {\n                    FlowColumn {\n                        timeSleepSummaryCards()\n                    }\n                }\n                <span style=\"color: #408080; font-style: italic;\">\/\/<\/span>\n            }\n        }\n<\/pre>\n<\/div>\n<p>This allows the cards state to be remembered and the cards won&#8217;t be recomposed. This is evident when observing the bubbles in the background of the cards, on resizing the screen the bubble animation continues without restarting the animation.<\/p>\n<p><image><a href=\"https:\/\/blogger.googleusercontent.com\/img\/b\/R29vZ2xl\/AVvXsEj21BdF9kLNEN0OssHZNlm-UM41SeyBYXVyU2FZPjx-Pp6wYR9QeEciyg-K1MvST-RxqsUHOy2JI7ZzRCzlI0_C5QDqf6ZFg7gU2GcZ13rXtQbQ9yzk-ZEp23VZ_APonySSz0ggmiZh1SK24vfik3XlQyOxP_swnZZLGvsQmLqSgCe4FPv8o2K49pVGRds\/w400-h400\/image4.gif\" target=\"_blank\" rel=\"noopener\"><\/p>\n<div style=\"text-align: center;\"><img loading=\"lazy\" decoding=\"async\" alt=\"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\" border=\"0\" height=\"400\" id=\"imgCaption\" src=\"https:\/\/blogger.googleusercontent.com\/img\/b\/R29vZ2xl\/AVvXsEj21BdF9kLNEN0OssHZNlm-UM41SeyBYXVyU2FZPjx-Pp6wYR9QeEciyg-K1MvST-RxqsUHOy2JI7ZzRCzlI0_C5QDqf6ZFg7gU2GcZ13rXtQbQ9yzk-ZEp23VZ_APonySSz0ggmiZh1SK24vfik3XlQyOxP_swnZZLGvsQmLqSgCe4FPv8o2K49pVGRds\/w400-h400\/image4.gif\" width=\"400\"\/><\/div>\n<p><\/a><\/image><\/p>\n<h3>Use Modifier.animateBounds() to have fluid animations between different window sizes<\/h3>\n<p>From the above example, we can see that state is maintained between changes in layout size (or layout itself), but the difference between the two layouts is a bit jarring. We\u2019d like this to animate between the two states without issue.<\/p>\n<p>In the latest <a href=\"https:\/\/developer.android.com\/develop\/ui\/compose\/bom#what_if_i_want_to_try_out_alpha_or_beta_releases_of_compose_libraries\" target=\"_blank\" rel=\"noopener\">compose-bom-alpha<\/a> (2024.09.03), there is a new experimental custom Modifier, <span style=\"font-family: Courier;\"><a href=\"https:\/\/developer.android.com\/reference\/kotlin\/androidx\/compose\/animation\/package-summary#%28androidx.compose.ui.Modifier%29.animateBounds%28androidx.compose.ui.layout.LookaheadScope,androidx.compose.ui.Modifier,androidx.compose.animation.BoundsTransform,kotlin.Boolean%29\" target=\"_blank\" rel=\"noopener\">Modifier.animateBounds()<\/a><\/span>.  The <span style=\"color: #0d904f; font-family: Courier;\">animateBounds<\/span> modifier requires a <span style=\"font-family: Courier;\"><a href=\"https:\/\/developer.android.com\/reference\/kotlin\/androidx\/compose\/ui\/layout\/LookaheadScope?hl=en\" target=\"_blank\" rel=\"noopener\">LookaheadScope<\/a><\/span>.<\/p>\n<p><span style=\"color: #0d904f; font-family: Courier;\">LookaheadScope<\/span> enables Compose to perform intermediate measurement passes of layout changes, notifying composables of the intermediate states between them. <span style=\"color: #0d904f; font-family: Courier;\">LookaheadScope<\/span> is also used for the new <a href=\"https:\/\/developer.android.com\/develop\/ui\/compose\/animation\/shared-elements\" target=\"_blank\" rel=\"noopener\">shared element APIs<\/a>, that you may have seen recently.<\/p>\n<p>To use <span style=\"color: #0d904f; font-family: Courier;\">Modifier.animateBounds()<\/span>, we wrap the top-level <span style=\"color: #0d904f; font-family: Courier;\">FlowRow<\/span> in a <span style=\"color: #0d904f; font-family: Courier;\">LookaheadScope<\/span>, and then apply the <span style=\"color: #0d904f; font-family: Courier;\">animateBounds<\/span> modifier to each card. We can also customize how the animation runs, by specifying the <span style=\"color: #0d904f; font-family: Courier;\">boundsTransform<\/span> parameter to a custom spring spec:<\/p>\n<div style=\"background: rgb(248, 248, 248); border: 0px; overflow: auto; width: auto;\">\n<pre style=\"line-height: 125%; margin: 0px;\"><span style=\"color: green; font-weight: bold;\">val<\/span> boundsTransform = { _ : Rect, _: Rect -&gt;\n   spring(\n       dampingRatio = Spring.DampingRatioNoBouncy,\n       stiffness = Spring.StiffnessMedium,\n       visibilityThreshold = Rect.VisibilityThreshold\n   )\n}\n\n\nLookaheadScope {\n   <span style=\"color: green; font-weight: bold;\">val<\/span> animateBoundsModifier = Modifier.animateBounds(\n       lookaheadScope = <span style=\"color: green; font-weight: bold;\">this<\/span>@LookaheadScope,\n       boundsTransform = boundsTransform)\n   <span style=\"color: green; font-weight: bold;\">val<\/span> timeSleepSummaryCards = remember {\n       movableContentOf {\n           AverageTimeInBedCard(animateBoundsModifier)\n           AverageTimeAsleepCard(animateBoundsModifier)\n       }\n   }\n   FlowRow(\n       modifier = Modifier\n           .fillMaxSize()\n           .windowInsetsPadding(insets),\n       horizontalArrangement = Arrangement.Center,\n       verticalArrangement = Arrangement.Center,\n       maxItemsInEachRow = <span style=\"color: #666666;\">3<\/span>\n   ) {\n       JetLaggedSleepGraphCard(uiState.value.sleepGraphData, animateBoundsModifier.widthIn(max = <span style=\"color: #666666;\">600.d<\/span>p))\n       <span style=\"color: green; font-weight: bold;\">if<\/span> (windowSizeClass == WindowWidthSizeClass.Compact) {\n           timeSleepSummaryCards()\n       } <span style=\"color: green; font-weight: bold;\">else<\/span> {\n           FlowColumn {\n               timeSleepSummaryCards()\n           }\n       }\n\n\n       FlowColumn {\n           WellnessCard(\n               wellnessData = uiState.value.wellnessData,\n               modifier = animateBoundsModifier\n                   .widthIn(max = <span style=\"color: #666666;\">400.d<\/span>p)\n                   .heightIn(min = <span style=\"color: #666666;\">200.d<\/span>p)\n           )\n           HeartRateCard(\n               modifier = animateBoundsModifier\n                   .widthIn(max = <span style=\"color: #666666;\">400.d<\/span>p, min = <span style=\"color: #666666;\">200.d<\/span>p),\n               uiState.value.heartRateData\n           )\n       }\n   }\n}\n<\/pre>\n<\/div>\n<p>Applying this to our layout, we can see the transition between the two states is more seamless without jarring interruptions.<\/p>\n<p><image><a href=\"https:\/\/blogger.googleusercontent.com\/img\/b\/R29vZ2xl\/AVvXsEikUkY5b840moPDzQRUQM6aU-kAkJo-oYT3oJFwsz65JteQLblWPHx5uU9HMeZ0NioUpd-BmdKn4eoNVeoJztO7rMW7ZTvC3PmQj2ux68IDdU0KRhbgfAPdy2_yp3R6v9eje8LPhakj1Cz7rrl0LSHTX8pelOFuuv5JaTNgRFuGW5E7nPW2kw8FJBb0_kA\/w400-h400\/image7.gif\" target=\"_blank\" rel=\"noopener\"><\/p>\n<div style=\"text-align: center;\"><img loading=\"lazy\" decoding=\"async\" alt=\"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\" border=\"0\" height=\"400\" id=\"imgCaption\" src=\"https:\/\/blogger.googleusercontent.com\/img\/b\/R29vZ2xl\/AVvXsEikUkY5b840moPDzQRUQM6aU-kAkJo-oYT3oJFwsz65JteQLblWPHx5uU9HMeZ0NioUpd-BmdKn4eoNVeoJztO7rMW7ZTvC3PmQj2ux68IDdU0KRhbgfAPdy2_yp3R6v9eje8LPhakj1Cz7rrl0LSHTX8pelOFuuv5JaTNgRFuGW5E7nPW2kw8FJBb0_kA\/w400-h400\/image7.gif\" width=\"400\"\/><\/div>\n<p><\/a><\/image><\/p>\n<p>Applying this logic to our whole dashboard, when resizing our layout, you will see that we now have a fluid UI interaction throughout the whole screen.<\/p>\n<p><image><a href=\"https:\/\/blogger.googleusercontent.com\/img\/b\/R29vZ2xl\/AVvXsEjgeCcBP6eF2jTchTqeULZJXeCaCvA92M-cvj-8FQwEQxKD0DffQSDByWWdZ_ZfrG-2zhTakNjvLfye_DmwmwEEO7qK_-pZ-Wb4IZqRBFQoiU6f92hSUd_kqJHbIwRFhtgB3IsBalN-XN0PH1TQmQhATewShhu-URaUpKHoT5HJwdXVAacRY-KdxhSW5F0\/s1600\/overall_interaction_flow_jetlagged.gif\" target=\"_blank\" rel=\"noopener\"><\/p>\n<div style=\"text-align: center;\"><img decoding=\"async\" alt=\"Moving image demonstrating responsive design in Jetlagged where items animate positions automatically\" border=\"0\" height=\"360\" id=\"imgCaption\" src=\"https:\/\/blogger.googleusercontent.com\/img\/b\/R29vZ2xl\/AVvXsEjgeCcBP6eF2jTchTqeULZJXeCaCvA92M-cvj-8FQwEQxKD0DffQSDByWWdZ_ZfrG-2zhTakNjvLfye_DmwmwEEO7qK_-pZ-Wb4IZqRBFQoiU6f92hSUd_kqJHbIwRFhtgB3IsBalN-XN0PH1TQmQhATewShhu-URaUpKHoT5HJwdXVAacRY-KdxhSW5F0\/s1600\/overall_interaction_flow_jetlagged.gif\" width=\"100%\"\/><\/div>\n<p><\/a><\/image><\/p>\n<h3>Summary<\/h3>\n<p>As you can see from this article, using Compose has enabled us to build a responsive dashboard-like layout by leveraging flow layouts, <span style=\"color: #0d904f; font-family: Courier;\">WindowSizeClasses<\/span>, movable content and <span style=\"color: #0d904f; font-family: Courier;\">LookaheadScope<\/span>. These concepts can also be used for your own layouts that may have items moving around in them too.<\/p>\n<p>For more information on these different topics, be sure to check out the <a href=\"https:\/\/developer.android.com\/jetpack\/compose\" target=\"_blank\" rel=\"noopener\">official documentation<\/a>, for the detailed changes to JetLagged, take a look at <a href=\"https:\/\/github.com\/android\/compose-samples\/pull\/1473\" target=\"_blank\" rel=\"noopener\">this pull request<\/a>.<\/p>\n<\/div>\n<p>[ad_2]<br \/>\n<br \/><a href=\"http:\/\/android-developers.googleblog.com\/2024\/10\/creating-responsive-dashboard-layout-for-jetlagged-jetpack-compose.html\">Source link <\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>[ad_1] Posted by Rebecca Franks &#8211; Developer Relations Engineer This blog post is part of our series: Adaptive Spotlight Week where we provide resources\u2014blog posts,<\/p>\n","protected":false},"author":1,"featured_media":262524,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_monsterinsights_skip_tracking":false,"_monsterinsights_sitenote_active":false,"_monsterinsights_sitenote_note":"","_monsterinsights_sitenote_category":0,"_uf_show_specific_survey":0,"_uf_disable_surveys":false,"footnotes":""},"categories":[146],"tags":[],"_links":{"self":[{"href":"https:\/\/michigandigitalnews.com\/index.php\/wp-json\/wp\/v2\/posts\/262523"}],"collection":[{"href":"https:\/\/michigandigitalnews.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/michigandigitalnews.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/michigandigitalnews.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/michigandigitalnews.com\/index.php\/wp-json\/wp\/v2\/comments?post=262523"}],"version-history":[{"count":0,"href":"https:\/\/michigandigitalnews.com\/index.php\/wp-json\/wp\/v2\/posts\/262523\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/michigandigitalnews.com\/index.php\/wp-json\/wp\/v2\/media\/262524"}],"wp:attachment":[{"href":"https:\/\/michigandigitalnews.com\/index.php\/wp-json\/wp\/v2\/media?parent=262523"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/michigandigitalnews.com\/index.php\/wp-json\/wp\/v2\/categories?post=262523"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/michigandigitalnews.com\/index.php\/wp-json\/wp\/v2\/tags?post=262523"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}