# Scale your app for every screen with Compose
- Speakers: Francesco Romano (Senior Developer Relations Engineer)
- Track: Apps foundation
- Video: https://www.youtube.com/watch?v=SGTkTKX7qq4
- Page: https://developer.android.com/events/dev-summit/talks/scale-your-app-for-every-screen-with-compose
## About the talk
The Android ecosystem is vast, with users shifting seamlessly across phones, tablets, foldables, and desktop environments. Now that we are Compose First, this session explores how to architect your app for resizable windows and diverse aspect ratios—where traditional layout assumptions break down, leading to stretched UIs and lost state.
Explore how the latest capabilities in Jetpack Compose, Navigation 3, and WindowManager can help you build dynamic layouts that naturally scale from compact screens to dual-pane displays. Walk away with strategies to accelerate your development workflow using Gemini and our skills, like the adaptive skill, to automatically refactor legacy codebases for larger screens.
## Summary
- Android platforms require adaptive apps because the system ignores orientation and aspect ratio locks on larger screens starting in Android 16, with manifest opt-outs removed in Android 17.
- The experimental `MediaQuery` API declaratively tracks active UI capabilities such as window size and folding postures. It replaces stitching together multiple legacy platform managers into Compose state.
- Stable `Grid` and `FlexBox` primitives provide adaptive layout building blocks. `Grid` enables parameterized multi-dimensional arrangements within a single composable, while `FlexBox` provides one-dimensional space distribution and wrapping.
- Jetpack Navigation 3 structures app-level multi-pane flows around state and scenes. Custom scene strategies evaluate UI capabilities and window size classes to dynamically switch between single-pane and multi-pane presentations.
- Compose Adaptive skills for Gemini automate app migrations to modern responsive components. The updated skill refactors layouts, resolves state loss bugs, and configures desktop inputs such as keyboard shortcuts and cursors.
## Resources
- [Install Android CLI](https://goo.gle/android-cli)
## Topics
jetpack-compose, adaptive-layouts, mediaquery, grid-layout, flexbox, navigation-3, android-skills
## Transcript
### Introduction to scaling apps across screens
**Francesco Romano**
[00:06](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=6s) Hi everyone.
[00:08](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=8s) Today, we're going to have a deep technical dive on how to scale your app to every screen with Jetpack Compose.
[00:16](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=16s) Why scaling to every screen?
[00:17](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=17s) Because the Android ecosystem has expanded across so many different form factors.
[00:22](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=22s) Your APK can run on phones, foldables, tablets, Chromebooks and Googlebooks, TVs, automotive infotainment systems, XR devices, and so on.
[00:33](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=33s) So first thing, it's meet your users where they are.
[00:37](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=37s) And second, the shift of the Android platform towards adaptive apps, it's not transitory.
[00:46](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=46s) It's not like we're going back to a phone-only world, it's here to stay.
[00:50](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=50s) And I have a quick timeline here.
[00:53](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=53s) In 2022, we started laying the foundation.
[00:57](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=57s) We had Android 12L, the first release of Jetpack WindowManager, we gave you APIs like window size classes, and we published the first version of the adaptive app quality guidelines.
[01:10](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=70s) Then, we also updated Google Play so that ratings are split by device type, and we started to include the adaptive app quality guidelines into ranking decisions.
[01:23](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=83s) In Android 15, we also had desktop windowing landed in AOSP, and Compose Material 3 Adaptive that hit stable.
[01:34](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=94s) And then starting from Android 16, SDK 36, the system simply ignores orientation locks, aspect ratio locks, resizability flags on bigger screens.
[01:46](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=106s) Together with the release of connected displays, now your APK can run in virtually any window size.
[01:54](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=114s) And SDK 36 gave you one temporary manifest flag to opt out from orientation and aspect ratio override.
[02:03](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=123s) In Android 17, that opt-out is gone.
[02:06](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=126s) So building adaptive, it's a baseline platform requirement.
[02:13](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=133s) Now, over the last decade, we worked closely with teams across the ecosystem, and we learned first-hand why building adaptive can feel hard.
[02:23](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=143s) You have some state loss, fragmented window APIs, unclear signals on when to switch layout, and just some old assumptions baked deep down into the codebase.
[02:36](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=156s) So we took everything that we learned from this decade of developer challenges, and we built it directly into two places: first, into the UI toolkit itself.
[02:47](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=167s) Today, we will explore some major new capabilities in Jetpack Compose: media query, grid, `FlexBox`, and Navigation 3.
[02:56](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=176s) But also in agentic tools that write the code with you through the official Android skills for Gemini in Android Studio and the Android CLI.
[03:07](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=187s) For example, we will see the Compose adaptive skills that was released at the Google I/O earlier this year.
[03:13](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=193s) It was already capable of migrating phone layouts to bigger window sizes.
[03:19](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=199s) Now, it can also fix your state loss, it can implement foldable-unique experiences like tabletop, and improve input support by discovering and implementing keyboard shortcuts, custom cursors, tooltips, and so on.
[03:39](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=219s) Okay, let's start with Jetpack Compose, which is the foundation for all of this.
[03:47](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=227s) Jetpack Compose went stable in 2021, so that marks its fifth birthday.
[03:53](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=233s) And over those five years, we've seen across thousands of production apps how much easier and more reliable it is to build adaptive with Compose.
[04:05](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=245s) Android development is now officially Compose-first.
[04:08](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=248s) That means all the new modern adaptive layouts, styling, navigation components are built for Compose.
[04:17](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=257s) And from a system architecture perspective, the biggest advantage of Compose for adaptive apps over the legacy view system, it's how state and UI interact.
[04:30](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=270s) And to explain this, I want to do a quick comparison with the view system.
[04:35](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=275s) So, think about how the view system worked.
[04:39](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=279s) Your UI was a tree of mutable objects that you inflated once from static XML files.
[04:49](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=289s) You had different resource qualifiers, and let's start with the major issue here: duplication across different resource qualifiers.
[04:58](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=298s) When you build an adaptive UI in XML, you don't have any conditional logic inside the layout file.
[05:04](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=304s) Instead, you just duplicate the entire hierarchy across resource folders, like layout, layout smallest window 600 dp, layout land, and so on.
[05:17](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=317s) Okay, look at the `EditText` example on the right.
```xml
```
```xml
```
[05:21](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=321s) That's the exact same logical component that handles text input, and it's scattered across different XML files.
[05:32](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=332s) If you update an attribute like `inputType` or `maxLength` in the phone layout, nothing in the compiler forces you to update it also in the tablet layout.
[05:42](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=342s) And because the components are decoupled across different files, structural drift is almost guaranteed over time.
[05:53](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=353s) And that duplication is also linked to another issue, which is state loss during configuration changes.
[06:00](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=360s) Remember that the in the View system, UI components are stateful.
[06:05](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=365s) An `EditText` owns its internal Editable buffer, custom cursor position, focus state, some state.
[06:15](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=375s) So, when a user, let's say, unfolds the device or resizes the window across a certain threshold, Android destroys the entire view hierarchy and inflates the new XML files from scratch.
[06:29](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=389s) To save the user input, the framework relies on onSaveInstanceState, which will pack the state linked to the ID of that view.
[06:39](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=399s) But that creates three possible failure modes.
[06:43](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=403s) First, if someone accidentally named the ID `text_input` instead of `text_msg` in the other XML, the state restoration silently drops the user's input text with zero warnings.
[06:59](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=419s) Second, onRestoreInstanceState runs after onViewCreated, which can frequently clashes with the ViewModel observers, can cause callback loops.
[07:11](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=431s) And third, if you swap the widget type across qualifiers, let's say on a compact screen maybe you want a spinner, and on a wider screen you want a RadioGroup, if they share the same ID, that crashes at runtime with a ClassCastException.
[07:29](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=449s) In Jetpack Compose, instead, your UI is not an object that you construct once.
[07:35](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=455s) Your UI is a pure function of state.
[07:38](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=458s) And that change completely what a configuration change even is.
[07:45](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=465s) What is a configuration change in Compose?
[07:48](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=468s) I have a hint.
### Handling configuration changes and UI capabilities
**Francesco Romano**
[07:52](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=472s) Yeah.
[07:52](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=472s) In Compose, a configuration change is just another recomposition, and recompositions happen all the time.
[07:59](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=479s) When the window gets wider, maybe because the user unfolded the devices, your composables read the new size, and the ones whose output depends on it just rerun.
[08:11](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=491s) That's That's the whole event.
[08:13](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=493s) But now, let's see what it means to write modern adaptive apps using the latest Compose tools and, components.
[08:23](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=503s) So, the XML world made you ask questions like, "Am I running on a phone, a tablet, or a desktop?", and then branching the UI based on the device category.
[08:35](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=515s) But there's a fundamental reason to avoid doing that.
[08:38](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=518s) Even when running on the exact same physical device, the application context and the UI capabilities can change dynamically at runtime.
[08:49](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=529s) So, The message that I want to leave is that UI decisions should be driven by the current active capabilities: current window size, posture, some hardware characteristics.
[09:03](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=543s) So, let's see a classic example, which is desktop windowing on an external monitor.
[09:09](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=549s) If you take your Pixel phone and plug it on an external monitor on your desk, the exact same app process that was just running on a 6-inch portrait screen is now running on a 32-inch wide monitor with physical keyboard and mouse attached.
[09:27](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=567s) So, checking what the device type the app is running on guarantees nothing about the screen it is actually being displayed on, or the input modalities.
[09:40](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=580s) And, on top of that, Android supports multi-windows, so windows can be resized.
[09:46](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=586s) Even if the screen of a Googlebook is bigger than the phone, your app may be running on a 400 dp wide window, together with other apps.
[09:59](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=599s) So, querying the static physical display metrics can also be meaningless.
[10:06](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=606s) So, to architect apps for Compose first, we need to understand how to query the environment.
[10:13](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=613s) And I want to start by try to formalize the concept of UI capabilities.
[10:19](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=619s) UI capabilities includes not only the window width and height or the foldable posture, but also input and output modalities that are currently active.
[10:29](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=629s) We have seen in the desktop environment users are not always using touch screen.
[10:36](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=636s) They can use keyboard and mouse.
[10:37](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=637s) In the automotive system, some cars have a rotary knob or different input, different way to input signals.
[10:46](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=646s) And also, beyond the screen size and input, how far the user is from the display, like the viewing distance, or whether a hardware camera or microphone is currently connected.
[11:01](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=661s) Those are all the UI capabilities.
### Detecting postures with the mediaQuery API
**Francesco Romano**
[11:04](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=664s) Now, if you wanted to retrieve all of the signals, you had to stitch together 5, 6 different APIs from different sources.
[11:13](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=673s) You needed the configuration object, the WindowMetricsCalculator, the WindowInfoTracker, the InputManager, the PackageManager, and then, manually, converting each of them in a state, a Compose state.
[11:27](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=687s) Did you like doing that?
[11:29](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=689s) I did not.
[11:31](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=691s) To solve this, we introduced the MediaQuery API in `androidx.compose.ui`.
```kotlin
mediaQuery { ... }
derivedMediaQuery { ... }
```
[11:36](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=696s) It's inspired by the CSS media queries.
[11:39](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=699s) And the word media doesn't mean audio or video playback.
[11:43](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=703s) It refers to the medium, the environment on which your UI is being rendered.
[11:49](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=709s) And MediaQuery gives you a single declarative Compose-native way to query the UI capabilities.
[11:57](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=717s) Let's look at the properties exposed in the `UiMediaScope`.
```kotlin
sealed interface UiMediaScope {
@get:FrequentlyChangingValue val windowWidth: Dp
@get:FrequentlyChangingValue val windowHeight: Dp
val windowPosture: Posture // Flat, Tabletop, Book
val pointerPrecision: PointerPrecision // Fine, Coarse, Blunt, None
val keyboardKind: KeyboardKind // Physical, Virtual, None
val hasCamera: Boolean
val hasMicrophone: Boolean
val viewingDistance: ViewingDistance // Near, Medium, Far
}
```
[12:01](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=721s) `windowWidth` and `windowHeight` in Dp.
[12:03](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=723s) `windowPosture`, which is meant for foldables right now, and can be Flat, Tabletop, or Book.
[12:10](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=730s) `pointerPrecision` tells you the precision of the primary pointing device: Fine for mouse, Stylus, or Trackpad; Coarse for Touch; Blunt for joystick, controllers, or None.
[12:23](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=743s) `keyboardKind` can be physical, virtual, or none.
[12:27](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=747s) `hasCamera` and `hasMicrophone` dynamically track whether this hardware is available.
[12:32](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=752s) Think about external camera that can be plugged in.
[12:35](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=755s) And `viewingDistance` reports Near, Medium, or Far.
[12:41](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=761s) But I think it's easier if we look at a real example, which is detecting if the device is in Tabletop mode.
[12:48](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=768s) Tabletop mode is a posture that lets users half fold the device and usually set it on a surface.
[12:56](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=776s) And it's great for extended video playback, for a video call, time-lapse photography.
[13:04](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=784s) It's also super useful for, you know, following a recipe or watch a slideshow.
[13:11](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=791s) Now, if you wanted to detect Tabletop posture before MediaQuery, you had to write all of this code.
```kotlin
@Composable
fun currentVideoPlayerLayout(): VideoPlayerLayout {
val context = LocalContext.current
val windowLayoutInfo by WindowInfoTracker.getOrCreate(context).windowLayoutInfo(context)
.collectAsStateWithLifecycle(null)
val isTabletopPosture = windowLayoutInfo?.displayFeatures?.any { displayFeature ->
displayFeature is FoldingFeature &&
displayFeature.state == FoldingFeature.State.HALF_OPENED &&
displayFeature.orientation == FoldingFeature.Orientation.HORIZONTAL
} ?: false
return if (isTabletopPosture) {
VideoPlayerLayout.Tabletop
} else {
VideoPlayerLayout.Default
}
}
```
[13:17](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=797s) You had to obtain the context, get an instance of WindowInfoTracker, collect the `windowLayoutInfo`, which is a flow, a lifecycle aware Compose state, and then iterate through the `displayFeatures` to check if there is one that is a `FoldingFeature`, is half opened, and is horizontal.
[13:36](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=816s) And that is just to detect the posture.
[13:39](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=819s) That is a lot of boilerplate.
[13:42](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=822s) With a new MediaQuery API, all of that shrinks down to just this few lines of code.
```kotlin
@Composable
fun currentVideoPlayerLayout(): VideoPlayerLayout {
return mediaQuery {
when(windowPosture) {
UiMediaScope.Posture.Tabletop -> VideoPlayerLayout.Tabletop
else -> VideoPlayerLayout.Default
}
}
}
```
[13:47](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=827s) The `mediaQuery` function takes a single lambda parameter, and inside that lambda's receiver scope, it exposes the set of properties that we just saw.
[13:57](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=837s) In this example, by simply checking `windowPosture` in a when expression, we can immediately return the right composable.
[14:07](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=847s) And here are two other examples of how to use the API.
```kotlin
@Composable
fun AdaptiveChatControls(
onStartVideoCall: () -> Unit,
onSendMessage: () -> Unit,
) {
// 1. Return a Dp value directly based on pointer precision
val chipPadding = mediaQuery {
if (pointerPrecision == PointerPrecision.Fine) 2.dp else 8.dp
}
// 2. Return a Boolean combining hardware capabilities
val canVideoCall = mediaQuery { hasCamera && hasMicrophone }
val hasPhysicalKeyboard = mediaQuery {
keyboardKind == KeyboardKind.Physical
}
Row(modifier = Modifier.padding(chipPadding)) {
if (canVideoCall) {
IconButton(onClick = onStartVideoCall) {
Icon(Icons.Default.Videocam, contentDescription = "Video call")
}
}
Button(onClick = onSendMessage) {
Text(if (hasPhysicalKeyboard) "Send (Ctrl+Enter)" else "Send")
}
}
}
```
[14:11](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=851s) First is information density.
[14:13](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=853s) So when a user is on a laptop, a desktop monitor with a mouse, and pointer precision is Fine, we can tighten the padding on our UI components from, in this example, 8 Dp to 2 Dp to show more content comfortably, since users are not using their fingers, the touch targets can be smaller.
[14:35](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=875s) Also, capability-driven actions.
[14:39](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=879s) Let's say we have a chat input bar, we can check `hasCamera` and `hasMicrophone` to dynamically show or hide the video call action button, or we can combine keyboard kind is physical to show, or not, some keyboard shortcut hints like press Ctrl+Enter to send.
[14:57](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=897s) Those are all retrieved at the run time.
[14:59](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=899s) And MediaQuery is also great for testing and preview generation.
```kotlin
@Preview(name = "Tabletop + Fine Pointer", widthDp = 840, heightDp = 700)
@Composable
fun TabletopPlayerPreview() {
val simulatedMediaScope = object : UiMediaScope {
override val windowWidth = 840.dp
override val windowHeight = 700.dp
override val windowPosture = UiMediaScope.Posture.Tabletop
override val pointerPrecision = UiMediaScope.PointerPrecision.Fine
override val keyboardKind = UiMediaScope.KeyboardKind.Physical
override val hasCamera = true
override val hasMicrophone = true
override val viewingDistance = UiMediaScope.ViewingDistance.Medium
}
CompositionLocalProvider(LocalUiMediaScope provides simulatedMediaScope) {
VideoPlayerScreen()
}
}
```
[15:07](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=907s) So, basically, MediaQuery resolves its receiver through a composition local called `LocalUiMediaScope`.
[15:14](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=914s) So, you can simply provide a fake `UiMediaScope` via `CompositionLocalProvider` in your preview, and you don't need to spin up foldable devices and put 90° or attach a mouse just to run a Compose UI test.
### Responsive screen layouts with Grid and FlexBox
**Francesco Romano**
[15:31](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=931s) Okay, now we know how to query the UI capabilities.
[15:35](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=935s) Now, let's see how to build the actual layout.
[15:40](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=940s) For that, I want to introduce Grid, which is a new layout primitive in `androidx.compose.foundation.layout`, which is now stable.
```kotlin
Grid(
modifier = Modifier.fillMaxSize().padding(16.dp),
config = {
val maxWidthDp = constraints.maxWidth.toDp()
val columnCount = if (maxWidthDp < 600.dp) 2 else 4
repeat(columnCount) {
column(1.fr)
}
row(GridTrackSize.Auto)
row(1.fr)
gap(row = 16.dp, column = 8.dp)
flow = GridFlow.Row
}
) {
// GridScope children...
}
```
[15:48](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=948s) Grid lets you implement a two-dimensional layout by displaying items in columns and rows that adapt to the available container size.
[15:57](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=957s) Let's see some things that you can do in the config block.
[16:00](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=960s) You can append new elements or tracks by calling column or row, and you can use the standard Kotlin loops like repeat.
[16:10](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=970s) You can set gutters between the tracks.
[16:12](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=972s) You can control the auto-placement direction via flow-- `GridFlow.Row` or `GridFlow.Column`.
[16:21](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=981s) And here is a key architecture detail.
[16:25](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=985s) The config block runs during the measure pass, and the grid configuration scope implements density and expose the constraints.
[16:35](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=995s) That means you can inspect `constraints.maxWidth.toDp()` right inside the config to compute the tracks with zero extra recomposition passes.
[16:50](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1010s) In Grid, when you add a children-- when you add a child without modifier, it will automatically flow into the next cell.
```kotlin
Grid(
config = {
repeat(3) { column(1.fr) }
repeat(3) { row(100.dp) }
gap(8.dp)
}
) {
// Spans 2x2 area (rows 1..2, columns 1..2)
HeroCard(modifier = Modifier.gridItem(rows = 1..2, columns = 1..2))
// Overlaps on top of HeroCard in cell (1, 1) aligned to TopStart!
LiveBadge(
modifier = Modifier.gridItem(
row = 1,
column = 1,
alignment = Alignment.TopStart
)
)
// Negative index: placed in last row (-1), spanning all 3 columns
FooterBar(modifier = Modifier.gridItem(row = -1, column = 1, columnSpan = 3))
}
```
[16:58](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1018s) When you want explicit control, you can use `Modifier.gridItem`, and you have one-based coordinates.
[17:05](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1025s) So, row 1, column 1 is the top-left cell.
[17:09](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1029s) It supports a negative indexing, so row -1 is the last row, and so on.
[17:15](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1035s) You can use rowSpan to span an item across a 2D area, and then there is also overlapping.
[17:23](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1043s) So, if two items are placed into overlapping cells, Grid stacks them on top of the other within those bounds, and the Z-ordering is determined by the composition order.
[17:35](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1055s) So, later items will draw on top of the others just like Box.
[17:43](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1063s) Now, we know the basics of Grid.
[17:45](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1065s) Let's look at the video player example.
[17:48](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1068s) Depending on whether the device is flat or half-opened, we need to switch between the default layout or the tabletop layout.
[17:59](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1079s) Before Grid, we had to create two different composables, `DefaultLayout` and `TabletopLayout`, and then branch the code on a when expression.
```kotlin
@Composable
fun VideoPlayerLayout(
player: @Composable () -> Unit,
controller: @Composable () -> Unit,
modifier: Modifier = Modifier
) {
when(currentVideoPlayerLayout()) {
VideoPlayerLayout.Default -> DefaultLayout(player, controller, modifier)
VideoPlayerLayout.Tabletop -> TabletopLayout(player, controller, modifier)
}
}
```
[18:09](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1089s) And you may think, "What's wrong with that?"
[18:13](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1093s) Well, if you look at those layouts, their content is identical.
[18:18](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1098s) Both displays the exact same player and the exact same controller.
[18:23](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1103s) The only difference between them is how these two elements are arranged on screen.
[18:29](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1109s) Until now, because we didn't have a way to pass the layout arrangement itself as a parameter, we were forced to define two separate composables.
[18:42](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1122s) Now, we can parameterize the layout configuration and handle everything within a single composable function.
[18:48](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1128s) And we'll see how.
[18:51](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1131s) So, with the Grid API, we can define named areas as enums.
```kotlin
enum class VideoPlayerArea { Player, Controller }
Grid(
config = { ... },
modifier = modifier
) {
Box(modifier = Modifier.gridItem(areaId = VideoPlayerArea.Player)) {
player()
}
Box(modifier = Modifier.gridItem(areaId = VideoPlayerArea.Controller)) {
controller()
}
}
```
[18:55](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1135s) Then, we can specify child composables, player and controller, to place inside the grid, and we can associate each child with a named area calling `Modifier.gridItem`, and we pass the `areaId`.
[19:09](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1149s) And here we're telling Grid to place the player into the area named `VideoPlayerArea.Player` and the controller into `VideoPlayerArea.Controller`.
[19:20](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1160s) And here we define the two configuration lambdas in a `VideoPlayerGridConfiguration`.
```kotlin
object VideoPlayerGridConfiguration {
val Default: GridConfigurationScope.() -> Unit = {
row(1f)
column(1f)
area(areaId = VideoPlayerArea.Player, row = 1, column = 1)
area(areaId = VideoPlayerArea.Controller, row = 1, column = 1)
}
val Tabletop: GridConfigurationScope.() -> Unit = {
row(0.5f)
row(0.5f)
column(1f)
area(areaId = VideoPlayerArea.Player, row = 1, column = 1)
area(areaId = VideoPlayerArea.Controller, row = 2, column = 1)
}
}
```
[19:26](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1166s) For the default video player layout, we configure a grid of one row and one column, but notice that even if there is just one cell, we can still assign two area names to the exact same cell.
[19:39](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1179s) In this case, as we've seen before, the controller is overlaid on top of the player.
[19:46](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1186s) For tabletop, we just define two rows and one column, placing the player in the top half and the controller in the bottom half, basically, like this.
[20:00](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1200s) Once we have defined those configuration lambdas, we can use MediaQuery directly into the configuration of the grid.
```kotlin
@Composable
fun VideoPlayerLayout(
player: @Composable () -> Unit,
controller: @Composable () -> Unit,
modifier: Modifier = Modifier
) {
Grid(
config = { mediaQuery {
if (isTabletopPosture) { VideoPlayerGridConfiguration.Tabletop }
else { VideoPlayerGridConfiguration.Default }
}
}
modifier = modifier
) {
Box(modifier = Modifier.gridItem(areaId = VideoPlayerArea.Player)) {
player()
}
Box(modifier = Modifier.gridItem(areaId = VideoPlayerArea.Controller)) {
controller()
}
}
}
```
[20:06](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1206s) So, we can now have a single video player layout composable with the UI that dynamically switches between two configurations.
[20:16](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1216s) The next primitive I want to talk about is `FlexBox`.
```kotlin
@Composable
fun AdaptiveActionHeader(tags: List) {
FlexBox(
config = {
direction( if (isCompact)
FlexDirection.Column else FlexDirection.Row )
wrap(FlexWrap.Wrap)
alignItems(FlexAlignItems.Center)
gap(row = 8.dp, column = 12.dp)
},
modifier = Modifier.fillMaxWidth()
) {
Text(
text = "Home",
modifier = Modifier.flex {
basis(FlexBasis.Auto)
grow(1f)
shrink(1f)
}
)
tags.forEach { tag ->
FilterChipItem(
label = tag,
modifier = Modifier.flex { shrink(0f) }
)
}
}
}
```
[20:20](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1220s) So, while Grid handles 2D row and column layouts, `FlexBox`, which is also now stable, handles one-dimensional layouts that still need intelligent space distribution, shrinking, multiline wrapping.
[20:36](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1236s) And it's a convenient, responsive layout that you can use instead of two separate composables, like FlowRow and FlowColumn, because it's a single container whose direction can change dynamically inside its config block without recreating child composables.
[20:53](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1253s) And it implements the full CSS FlexBox algorithm, so you have wrap, justify-content, align-items, gap.
[21:02](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1262s) And on each child, you have `Modifier.flex` that gives you basis, relative grow, shrink factors, alignSelf, and so on.
### App-level layout architecture and Navigation 3
**Francesco Romano**
[21:14](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1274s) Okay, now let's zoom out from the screen-level definition to app-level layouts, how we structure our top-level navigation and multi-pane screen arrangements across the entire application, not just the single screen.
[21:32](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1292s) While MediaQuery gives you the exact window width and height in Dp, for app-level architecture, you want standardized, opinionated viewport breakpoints, which are the window size classes.
[21:46](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1306s) Basically, you want every screen to be responsive within each of these buckets, while you can have more structural changes and updates and differences between different buckets.
[21:59](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1319s) For example, by showing just a list on a compact screen or both list and detail in an expanded window.
[22:09](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1329s) You can build window size classes directly on top of `UiMediaScope` in a very clean way, which is defining an extension property on `UiMediaScope`.
```kotlin
val UiMediaScope.windowSizeClass: WindowSizeClass
@Composable get() = derivedMediaQuery {
WindowSizeClass.BREAKPOINTS_V2.computeWindowSizeClass(
widthDp = windowWidth.value,
heightDp = windowHeight.value
)
}
@Composable
fun AppRoot() {
val sizeClass = mediaQuery { windowSizeClass }
val isExpandedWidth = sizeClass.isWidthAtLeastBreakpoint(
WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND // 600dp
)
// Drive app-level navigation and scene strategies...
}
```
[22:20](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1340s) One thing to notice, it's using `derivedMediaQuery`, not the standard MediaQuery.
[22:25](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1345s) Why?
[22:26](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1346s) Because window width and height are frequently changing values.
[22:30](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1350s) When users are resizing your app, like on a desktop, these values can change every frame.
[22:38](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1358s) And you probably don't want to recompose your app-level scaffold on every frame, but only when the window actually crosses a breakpoint.
[22:49](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1369s) And once we have the window size classes, how do we transition our app between a single-pane flow to multi-pane layouts when we have more space?
[22:58](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1378s) This is where Jetpack Navigation 3 shines, because Nav 3 was architected from ground up around Compose state and scenes.
[23:07](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1387s) And first, you own the back stack, which is simply a list of NavKeys, so it's very flexible, and it's easy to save as a state.
[23:16](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1396s) Each key on the stack resolves to a NavEntry containing its composable and metadata.
[23:23](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1403s) And NavDisplay doesn't hard-code how entries are drawn; instead, it evaluates a list of scene strategies.
[23:33](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1413s) Within the strategy, you can inspect the UI capabilities, for example, the window size classes.
```kotlin
if (isExpandedWidth) {
/* return a multi-pane scene */
}
else {
/* return a single pane scene */
}
```
[23:39](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1419s) You can inspect the back stack, and then decide whether to render one NavEntry in a single pane, or multiple items simultaneously, or one on top of the other.
[23:51](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1431s) It's very flexible.
[23:54](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1434s) And beside the multi-pane strategy, window size classes also drive what navigation components you want to use to maximize ergonomics and the use of screen space.
[24:05](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1445s) For example, on compact widths, you want a bottom navigation bar, on medium, a NavRail, on an expanded, a full navigation drawer.
[24:14](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1454s) When you combine Nav3 for app-level layout decisions and responsive components for screen-level definition, you get apps that look like this and shine on virtually every window size and environment.
[24:30](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1470s) For Nav 3 specifically, if you want to find out more about the latest updates, including the support for deep links, check out the talk Building Type-Safe Deep Links with Jetpack Navigation 3.
### Agentic development and automation demo
**Francesco Romano**
[24:42](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1482s) Okay, we've covered a lot of new APIs today: MediaQuery, Grid, `FlexBox`, Nav3.
[24:47](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1487s) And that covers how Jetpack Compose helps you build adaptive apps.
[24:55](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1495s) But in the era of agentic development, there is another extremely important advantage in using Jetpack Compose: it's way more effective.
[25:04](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1504s) You probably heard it during the keynote, we have seen apps using over 30% less tokens after migrating to Compose, and we are working to optimize your agentic workflow for Compose apps even more.
[25:18](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1518s) Today, I want to highlight the major updates to the Compose Adaptive skill.
```shell
android skills list
android skills add adaptive
```
[25:22](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1522s) As I said, in addition to migrating layouts to Grid, Flexbox, and MediaQuery, it now explicitly handle end-to-end state management across the app, and we test the skill against real-world state loss bugs: preserving navigation, scroll position, active media playback, half-typed text inputs, and so on.
[25:46](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1546s) The skill now also includes foldable posture optimization, such as automatically refactoring media and camera screens into Tabletop mode, but also when your app runs on a Googlebook or an external monitor with a mouse and keyboard, layout is only half the story.
[26:06](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1566s) The Adaptive skill now can audit your UI to find and implement keyboard shortcuts, custom pointer cursors, hover states, tooltips, and so on.
[26:20](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1580s) So let's see it in action.
[26:22](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1582s) I recorded a demo of this.
[26:25](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1585s) So we have our app.
[26:27](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1587s) We can look at our pets.
[26:30](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1590s) We can message with them.
[26:32](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1592s) We can look a video of them.
[26:35](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1595s) Yeah, we can see the top pets.
[26:38](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1598s) But you already see there is it's not great.
[26:42](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1602s) Now let's see I just want to look a picture with some drama in it and cats.
[26:47](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1607s) Okay, I want to see them bigger.
[26:49](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1609s) Dah, that's back to home.
[26:54](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1614s) Okay, let me tell my buddy about it.
[26:58](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1618s) Yeah, so, "Buddy", hm.
[27:03](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1623s) The app is not great.
[27:05](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1625s) It's too wide, also.
[27:06](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1626s) You see that it's too wide.
[27:07](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1627s) Let's try to resize it.
[27:11](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1631s) What?
[27:11](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1631s) Eh, yeah, okay.
[27:13](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1633s) And the text that I typed is lost.
[27:17](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1637s) Okay, it's time to ask Gemini to fix this.
[27:21](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1641s) So thanks to the Adaptive skill, the prompt can be as simple as "make the adapt the app adaptive".
[27:29](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1649s) As you can see, I'm using Gemini 3.0 Flash that together with the skill is able to do it.
[27:35](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1655s) It picked up the skill, and now it's starting to run the changes.
[27:41](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1661s) It will ask some questions, like, "Oh, MediaQuery is experimental.
[27:45](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1665s) Do you really want to use it?"
[27:46](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1666s) And keep going.
[27:48](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1668s) I had to speed it up to cut things, but if you come at the demo area, you can try the skills yourself.
[27:56](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1676s) And this is just one of the skills that we released.
[27:59](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1679s) For example, we also have a new skill that will help you build the amazing stylus experiences.
[28:04](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1684s) So, come at the area to try it.
[28:06](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1686s) So, it's done.
[28:10](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1690s) It modified 11 files.
[28:13](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1693s) And let's see it in action.
[28:16](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1696s) Okay.
[28:19](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1699s) Grid is already doing its work.
[28:22](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1702s) The navigation moved to the side.
[28:25](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1705s) Let's try the filters.
[28:29](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1709s) I think they will work now.
[28:30](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1710s) Yeah, they do.
[28:32](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1712s) Okay, let's message with my buddy, and telling that the app is great now.
[28:39](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1719s) Let's see with more space how that looks like.
[28:43](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1723s) Yeah, it migrated to the list-detail layout.
[28:46](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1726s) This run took about 20 minutes and something around 200,000 tokens.
[28:54](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1734s) Okay.
### The ROI of adaptive apps and key takeaways
**Francesco Romano**
[28:55](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1735s) Now, I usually start a talk with business metrics and success cases, but for ADS, I wanted to jump straight into the code.
[29:03](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1743s) However, I want to spend a few minutes to share the engineering return of investment.
[29:08](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1748s) Today, your Android app can potentially run on more than 580 million active large-screen devices, and we also know that the multi-device and foldable users are among the highest-engaged segments on Android.
[29:23](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1763s) Let's look at two concrete case studies that prove what happens when you ship an adaptive build.
[29:31](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1771s) First is Gratitude, which is a popular self-care and daily journaling app.
[29:36](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1776s) By adopting adaptive adaptive screen designs, Gratitude established tablets and foldables as a high-growth acquisition channel.
[29:46](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1786s) First, they doubled the acquisition velocity on large screens.
[29:50](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1790s) Daily installs specifically originating from tablets and foldables grew by 100%.
[29:56](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1796s) They expanded the large-screen share of their overall user bases from 7% to 10% of total installs.
[30:07](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1807s) The second is BoxBoxClub, which is a Formula 1 and motorsport companion app, and after updating their build for foldables and larger screens, their telemetry showed massive gains: 7.6% increase in overall active users and a 54% increase in engagement time.
[30:31](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1831s) Okay.
[30:31](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1831s) We are at the end of the talk, and I want to give you three important takeaways.
[30:37](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1837s) Building Adaptive allows your app to serve more devices and more users, while future-proofing your app for this expanding ecosystem.
[30:48](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1848s) Android development is Compose First.
[30:50](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1850s) All the new tools, libraries, and skills are built for Compose.
[30:54](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1854s) Compose is great for adaptive layouts and for agentic development.
[30:58](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1858s) Codebases are smaller and easier to understand, agents use less tokens when dealing with Compose code.
[31:06](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1866s) So, go ahead, try all the new tools, the skills, the APIs, and have fun.
[31:14](https://www.youtube.com/watch?v=SGTkTKX7qq4&t=1874s) Thank you.