Support for different display sizes enables access to your app by the widest variety of devices and greatest number of users.
To support as many display sizes as possible—whether different device screens or different app windows in multi-window mode—design your app layouts to be responsive and adaptive. Responsive/adaptive layouts provide an optimized user experience regardless of display size, enabling your app to accommodate phones, tablets, foldables, ChromeOS devices, portrait and landscape orientations, and resizable display configurations such as split‑screen mode and desktop windowing.
Responsive/adaptive layouts change based on available display space. Changes range from small layout adjustments that fill up space (responsive design) to completely replacing one layout with another so your app can best accommodate different display sizes (adaptive design).
As a declarative UI toolkit, Jetpack Compose is ideal for designing and implementing layouts that dynamically change to render content differently on different display sizes.
Make large layout changes for content-level composables explicit
App-level and content-level composables occupy all of the display space available to your app. For these types of composables, it might make sense to change the overall layout of your app on large displays.
Avoid using physical hardware values for making layout decisions. It might be tempting to make decisions based on a fixed tangible value (Is the device a tablet? Does the physical screen have a certain aspect ratio?), but the answers to these questions may not be useful for determining the space available for your UI.
On tablets, an app might be running in multi-window mode, which means the app may be splitting the screen with another app. In desktop windowing mode or on ChromeOS, an app might be in a resizable window. There might even be more than one physical screen, such as with a foldable device. In all of these cases, the physical screen size isn't relevant for deciding how to display content.
Instead, you should make decisions based on the actual portion of the screen allocated to your app described by the current window metrics provided by the Jetpack WindowManager library. For an example of how to use WindowManager in a Compose app, see the JetNews sample.
Making your layouts adaptive to the available display space also reduces the amount of special handling needed to support platforms like ChromeOS and form factors like tablets and foldables.
When you've determined the metrics of the space available for your app, convert the raw size into a windowsize class as described in Use window size classes. Window size classes are breakpoints designed to balance app logic simplicity with the flexibility to optimize your app for most display sizes. Window size classes refer to the overall window of your app, so use the classes for layout decisions that affect your overall app layout. You can pass window size classes down as state, or you can perform additional logic to create derived state to pass down to nested composables.
@Composable fun MyApp( windowSizeClass: WindowSizeClass = currentWindowAdaptiveInfo().windowSizeClass ) { // Perform logic on the size class to decide whether to show the top app bar. val showTopAppBar = windowSizeClass.windowHeightSizeClass != WindowHeightSizeClass.COMPACT // MyScreen knows nothing about window sizes, and performs logic based on a Boolean flag. MyScreen( showTopAppBar = showTopAppBar, /* ... */ ) }
A layered approach confines display size logic to a single location instead of scattering it across your app in many places that need to be kept in sync. A single location produces state, which can be explicitly passed down to other composables just like any other app state. Explicitly passing state simplifies individual composables because the composables take the window size class or specified configuration along with other data.
Flexible nested composables are reusable
Composables are more reusable when they can be placed in a wide variety of places. If a composable must be placed in a specific location with a specific size, the composable is unlikely to be reusable in other contexts. This also means that individual, reusable composables should avoid implicitly depending on global display size information.
Imagine a nested composable that implements a list-detail layout, which may show either a single pane or two panes side by side:
The list-detail decision should be part of the overall layout for the app, so the decision is passed down from a content-level composable:
@Composable fun AdaptivePane( showOnePane: Boolean, /* ... */ ) { if (showOnePane) { OnePane(/* ... */) } else { TwoPane(/* ... */) } }
What if you instead want a composable to independently change its layout based on the display space available, for example, a card that shows additional details if space allows? You want to perform some logic based on some available display size, but which size specifically?
Avoid trying to use the size of the device's actual screen. This won't be accurate for different types of screens and also won't be accurate if the app isn't fullscreen.
Because the composable is not a content-level composable, don't use the current window metrics directly. If the component is placed with padding (such as with insets), or if the app includes components such as navigation rails or app bars, the amount of display space available to the composable may differ significantly from the overall space available to the app.
Use the width that the composable is actually given to render itself. You have two options to get that width:
If you want to change where or how content is displayed, use a collection of modifiers or a custom layout to make the layout responsive. This could be as straightforward as having a child fill all of the available space, or laying out children with multiple columns if there's enough room.
If you want to change what you show, use
BoxWithConstraints
as a more powerful alternative.BoxWithConstraints
provides measurement constraints that you can use to call different composables based on the available display space. However, this comes at some expense, asBoxWithConstraints
defers composition until the layout phase, when these constraints are known, causing more work to be performed during layout.
@Composable fun Card(/* ... */) { BoxWithConstraints { if (maxWidth < 400.dp) { Column { Image(/* ... */) Title(/* ... */) } } else { Row { Column { Title(/* ... */) Description(/* ... */) } Image(/* ... */) } } } }
Ensure all data is available for different display sizes
When implementing a composable that takes advantage of extra display space, you might be tempted to be efficient and load data as a side effect of the current display size.
However, doing so goes against the principle of unidirectional data flow, where data can be hoisted and provided to composables to render appropriately. Enough data should be provided to the composable so that the composable always has enough content for any display size, even if some portion of the content might not always be used.
@Composable fun Card( imageUrl: String, title: String, description: String ) { BoxWithConstraints { if (maxWidth < 400.dp) { Column { Image(imageUrl) Title(title) } } else { Row { Column { Title(title) Description(description) } Image(imageUrl) } } } }
Building on the Card
example, note that the description
is always passed to
the Card
. Even though the description
is only used when the width permits
displaying it, Card
always requires the description
, regardless of the
available width.
Always passing sufficient content makes adaptive layouts simpler by making them less stateful and avoids triggering side effects when switching between display sizes (which may occur due to a window resize, orientation change, or folding and unfolding a device).
This principle also allows preserving state across layout changes. By hoisting
information that may not be used at all display sizes, you can preserve app
state as the layout size changes. For example, you can hoist a showMore
boolean flag so that the app state is preserved when display resizing causes the
layout to switch between hiding and showing content:
@Composable fun Card( imageUrl: String, title: String, description: String ) { var showMore by remember { mutableStateOf(false) } BoxWithConstraints { if (maxWidth < 400.dp) { Column { Image(imageUrl) Title(title) } } else { Row { Column { Title(title) Description( description = description, showMore = showMore, onShowMoreToggled = { newValue -> showMore = newValue } ) } Image(imageUrl) } } } }
Learn more
To learn more about adaptive layouts in Compose, see the following resources:
Sample apps
- CanonicalLayouts is a repository of proven design patterns that provide an optimal user experience on large displays
- JetNews shows how to design an app that adapts its UI to make use of available display space
- Reply is an adaptive sample for supporting mobile, tablets, and foldables
- Now in Android is an app that uses adaptive layouts to support different display sizes
Videos