Adaptive Responsive Layout in Jetpack Compose
Android has been powering a wide variety of devices with all types of screen sizes. Starting from compact 4 inch displays all the way up to tablets with 12+ inches displays, and now, we have foldable and full blown desktops capable of running android apps.
here is a big task that your app is going to be displayed on various different screen sizes and form factors.
Today we are going through how to adapt your app’s designs to look great on any kind of screen sizes and also how to implement it.
What is responsive design?
Similar to how water adapts to different containers, your UI needs to adapt to the screen they are displayed on.
This approach to make design visible on all different types of screens is called responsive design.
Android introduced Window Size Classes. These are pre-defined breakpoints which take into account the orientation and screen space available to your app based on which you can draw different UI. It categorises the display area into
- Compact
- Medium
- Expanded
A category is assigned to both, height and width of the device which can be used to render the most suitable UI. Coming back to our example, we could decide to render the CompactUI on Compact width devices while ExpandedUI would make more sense for Medium and Expanded
How to build responsive layouts using Material 3 WindowSizeClass
The Material 3 WindowSizeClass
library gives you information about the Window you are currently running in.
This is similar to using BoxWithConstraints
but instead of having the sizing in dp
, the library gives you a token describing the viewport you are have available. This comes in three sizes: Compact, Medium, Expanded.
Add the dependency in your project:
// app/build.gradle
dependencies {
implementation "androidx.compose.material3:material3-window-size-class:1.1.0"
}
and use calculateWindowSizeClass()
in your activity. Whenever the screen size changes (during configuration changes) a new size class will be emitted:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AppTheme {
val sizeClass = calculateWindowSizeClass(activity = this)
val showOnePanel = sizeClass.widthSizeClass == WindowWidthSizeClass.Compact
if (showOnePanel) {
ConversationsList()
} else {
var conversationId by remember {
mutableStateOf(-1)
}
Row(Modifier.fillMaxWidth()) {
ConversationsList(
modifier = Modifier.weight(1f),
onConversationSelected = {
conversationId = it
})
ConversationDetails(
modifier = Modifier.weight(2f),
selectedConversation = conversationId
)
}
}
}
}
}
}
Let’s also check different navigation bars for each device size.
val windowSize = calculateWindowSizeClass(activity = this)
when (windowSize.widthSizeClass) {
WindowWidthSizeClass.Compact -> {
BottomNavigation()
}
WindowWidthSizeClass.Medium -> {
NavigationRail()
}
WindowWidthSizeClass.Expanded -> {
PermanentNavigationDrawer()
}
else -> {
BottomNavigation()
}
}
Component Level Changes
Now that screen level decisions are made, we can drill down into non-root composables and see how to make them adapt to different screen sizes.
For a truly adaptive UI, every composable should be smart enough to render itself properly on any given display area. Based on the available space, it should be able to make a decision on
- What data to show
- How to structure that data
What data to show
Based on the available space, it might make sense to hide or show some fields. For example, showing profile picture only if we have enough space, else just showing the username.
For such cases, we can use BoxWithConstraints composable. As the name suggests, it is similar to a Box composable, but provides the available dimensions in its scope. It will give you the available min and max height and width based on which you can take decisions to show or hide certain components.
fun Profile(user: User) {
BoxWithConstraints(modifier = Modifier.padding(16.dp)) {
when (this.maxWidth) {
in (0.dp..600.dp) -> {
CompactProfile(user)
}
in (601.dp..900.dp) -> {
ExpandedProfile(user)
}
}
}
}
How to structure the data
Now that we have figured out what data to show given an area, the next thing we need to figure out is how to structure or layout the data to make the most of the available space.
For example, based on the available width, we can decide how many children to place in a given row before moving on to the next row.
BoxWithConstraints:
We can use BoxWithConstraints to not only show or hide components but to also decide how to show them given a set of constraints. For example, if the available width is limited, we can render items in a column else we can use a Row to render them side by side.
@Composable
fun Profile(user: User) {
BoxWithConstraints(modifier = Modifier.padding(16.dp)) {
when (this.maxWidth) {
in (0.dp..400.dp) -> {
VerticalProfile(user)
}
in (401.dp..900.dp) -> {
HorizontalProfile(user)
}
}
}
}
Common responsive layout patterns
Being aware of the screen size you are running on is the core of building responsive layouts.
Other than that it is all about using the appropriate components and Modifier
s for the right job. Here is a list of the most common ones:
- Use
Row
/Column
and provide their child composables withModifier.weight()
to make them take up the available space. - Use
FlowRow
/FlowColumn
to make their child composables wrap to the next line if there is not enough space. - Use
Column
/Row
arrangements withSpaceBetween
,SpaceEvenly
orSpaceAround
to distribute the space between their children. - Let
Image
s take as much space as possible. Consider giving them aModifier.aspectRatio()
andContentScale.Crop
to make them look good on any screen size. - Swap your
LazyColumn
/LazyRow
withLazyGrids
and change number of cells depending on the screen size. This way you have a list on mobile and a grid on tablets. You can do this by passingGridCells.Fixed()
to the cell parameter of yourLazyGrid
.
Conclusion
Creating adaptive layouts is crucial for becoming an Android developer. To achieve this, you simply need to incorporate “ifs” and “elses” into your code.
Happy coding ❤