Adaptive Responsive Layout in Jetpack Compose

Jolly Raiyani
5 min readJan 24, 2024

--

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 Modifiers for the right job. Here is a list of the most common ones:

  • Use Row/Column and provide their child composables with Modifier.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 with SpaceBetween,SpaceEvenly or SpaceAround to distribute the space between their children.
  • Let Images take as much space as possible. Consider giving them a Modifier.aspectRatio() and ContentScale.Crop to make them look good on any screen size.
  • Swap your LazyColumn/LazyRow with LazyGrids 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 passing GridCells.Fixed() to the cell parameter of your LazyGrid.

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 ❤

--

--

Jolly Raiyani

I am a mobile app developer. I have worked with many different clients, and have always been able to deliver high-quality results.