Build adaptive apps with Jetpack Compose

1. Introduction

In this codelab you learn how to build adaptive apps for phones, tablets, and foldables, and how they enhance reachability with Jetpack Compose. You also learn best practices for using Material 3 components and theming.

Before we dive in, it's important to understand what we mean by adaptability.

Adaptability

The UI for your app should be responsive to account for different screen sizes, orientations and form factors. An adaptive layout changes based on the screen space available to it. These changes range from simple layout adjustments to fill up space, choosing respective navigation styles, to changing layouts completely to make use of additional room.

To learn more, check out Adaptive design.

In this codelab you explore how to use and think about adaptability when using Jetpack Compose. You build an application, called Reply, that shows you how to implement adaptability for all kinds of screens, and how adaptability and reachability work together to give users an optimal experience.

What you'll learn

  • How to design your app to target all screen sizes with Jetpack Compose.
  • How to target your app for different foldables.
  • How to use different types of navigation for better reachability and accessibility.
  • How to design Material 3 color-schemes and dynamic theming to provide an optimal accessibility experience.
  • How to use Material 3 components to provide the best experience for every screen size.

What you'll need

We will use the Resizable emulator for this codelab, as it lets us switch between different types of devices and screen sizes.

Resizable emulator with options of phone, unfolded, tablet and desktop.

If you're unfamiliar with Compose, consider taking the Jetpack Compose basics codelab before completing this codelab.

What you'll build

  • An interactive Reply Email client app using best practices for adaptable designs, different material navigations and optimal screen space usage.

Multiple device support showcase that you will achieve in this codelab

2. Get set up

To download the sample app, you can either:

or clone the GitHub repository from the command line:

git clone https://github.com/googlecodelabs/android-compose-codelabs.git
cd android-compose-codelabs/AdaptiveUICodelab

Alternatively, you can download two zip files:

We recommend that you start with the code in the main branch and follow the codelab step-by-step at your own pace.

Open Project into Android Studio

  1. On the Welcome to Android Studio window, select c01826594f360d94.png Open an Existing Project.
  2. Select the folder <Download Location>/AdaptiveUICodelab (make sure you select the AdaptiveUICodelab directory containing build.gradle).
  3. When Android Studio has imported the project, test that you can run the main branch.

Explore the start code

The main branch code contains the ui package. You will work with the following files in that package.

  • MainActivity - Entry point activity where you start your app. You make changes in this file.
  • ReplyApp.kt - Contains main screen UI composables.
  • ReplyListOnlyContent.kt - Contains composables for providing lists and detail screens. You make changes in this package.
  • utils/WindowStateUtils.kt - Contains utility functions for device postures and window state. You don't make changes in this file.

Let's first focus on MainActivity.kt.

MainActivity.kt

override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)

   setContent {
       ReplyTheme {
           val uiState = viewModel.uiState.collectAsState().value
           ReplyApp(uiState)
       }
   }
}

If you run this app on a resizable emulator and try different device types, like a phone or tablet, the UI just expands to the given space instead of taking advantage of screen space or providing reachability ergonomics.

Initial screen on phone

Initial stretched view on tablet

Let's improve it to take advantage of the screen space and improve the user experience while still keeping accessibility at its core.

3. Make apps adaptable

This section introduces what making apps adaptable means, and what components Material 3 provides to make that easier for us. We'll also cover the types of screens and states you'll target, including phones, tablets, large tablets, and foldables.

Let's start by going through the fundamentals of window sizes, fold postures and different types of navigation options. Then we can use these APIs in our app to make it more adaptive.

Handling window sizes

Android devices come in all shapes and sizes, from phones to foldables to tablets and ChromeOS devices. To support as many screen sizes as possible, your UI needs to be responsive and adaptive. To help you find the right threshold at which to change the UI of your app, we've defined new breakpoint values that help classify devices into predefined size classes (compact, medium and expanded), called window size classes. These are a set of opinionated viewport breakpoints that help you design, develop, and test responsive and adaptive application layouts.

The categories were chosen specifically to balance layout simplicity, with the flexibility to optimize your app for unique cases. Window size class is always determined by the screen space available to the app, which may not be the entire physical screen for multitasking or other segmentations.

WindowWidthSizeClass for compact, medium and expanded distribution.

WindowHeightSizeClass for compact, medium and expanded distribution.

Both width and height are classified separately, so at any point in time, your app has two window size classes—one for width and one for height. Available width is usually more important than available height due to the ubiquity of vertical scrolling, so for this case you'll also use width size classes.

Compose-based apps should use the material3-window-size-class library, which calculates a WindowSizeClass based on the current window metrics with calculateWindowSizeClass(). Start by adding this dependency to your app level build.gradle file.

Build.gradle

dependencies {

   implementation "androidx.compose.material3:material3-window-size-class:1.1.0"
}

The calculateWindowSizeClass() method helps you get a Compose remembered state so that whenever there are configuration changes in size, your UI tree renders again based on the new size.

To start supporting adaptable sizes, add calculateWindowSizeClass() to the root of your Compose UI and pass it to ReplyApp composable. You can now make changes to MainActivity.kt so that it looks like this.

Because this API is still experimental, you need to add @OptIn(ExperimentalMaterial3WindowSizeClassApi::class) to the onCreate() function.

MainActivity.kt

@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)

   setContent {
       ReplyTheme {
           val windowSize = calculateWindowSizeClass(this)
           val uiState = viewModel.uiState.collectAsState().value
           ReplyApp(uiState, windowSize.widthSizeClass)
       }
   }
}

Now let's accommodate window size width class params in ReplyApp() composable in ReplyApp.kt.

ReplyApp.kt

@Composable
fun ReplyApp(
   replyHomeUIState: ReplyHomeUIState,
   windowSize: WindowWidthSizeClass,
 )

With these changes you can see that the ReplyApp has information about the latest window size to correctly use the space. If you run your app now it compiles, but without any visible changes.

You should also update your ReplyAppPreview() composables, which will help preview all types of screen sizes in one panel.

MainActivity.kt

@Preview(showBackground = true)
@Composable
fun ReplyAppPreview() {
   ReplyTheme {
       ReplyApp(
           replyHomeUIState = ReplyHomeUIState(emails = LocalEmailsDataProvider.allEmails),
           windowSize = WindowWidthSizeClass.Compact
       )
   }
}

@Preview(showBackground = true, widthDp = 700)
@Composable
fun ReplyAppPreviewTablet() {
   ReplyTheme {
       ReplyApp(
           replyHomeUIState = ReplyHomeUIState(emails = LocalEmailsDataProvider.allEmails),
           windowSize = WindowWidthSizeClass.Medium
       )
   }
}

@Preview(showBackground = true, widthDp = 1000)
@Composable
fun ReplyAppPreviewDesktop() {
   ReplyTheme {
       ReplyApp(
           replyHomeUIState = ReplyHomeUIState(emails = LocalEmailsDataProvider.allEmails),
           windowSize = WindowWidthSizeClass.Expanded
       )
   }
}

Before you adjust the UI based on window size information, you should also take the device's fold posture into consideration and make a decision on layout placements.

4. Handle fold states

Make sure that your app responds to fold state changes, fold with hinges, and different fold postures so it looks good no matter what device it's running on. There can be many fold states and postures with different types of foldable available, but let's start with the following three postures to target your app UI.

Normal Posture

This is the basic foldable state where a device is either fully opened or fully folded. You then let windowSizeClass take care of the UI.

Book Posture

This is where the fold state of the device is HALF_OPENED and in portrait mode with the hinge crease dividing the two parts of the screen. You want to avoid placing readable content in the crease area.

Separating

Foldable devices have a fold in the display that separates two portions (typically halves) of the display. The fold has a dimension and can separate the two portions with an occlusionType, which defines whether the fold occludes part of the display (a full occlusion is reported for dual screen devices). Device states where occlusion is full because of the physical hinge, avoid placing touchable or visible parts under the hinge.

bfd485c789473de4.png

You've already set up the DevicePosture interface in utils/WindowStateUtils.kt:

WindowStateUtils.kt

/**
* Information about the posture of the device
*/
sealed interface DevicePosture {
   object NormalPosture : DevicePosture

   data class BookPosture(
       val hingePosition: Rect
   ) : DevicePosture

   data class Separating(
       val hingePosition: Rect,
       var orientation: FoldingFeature.Orientation
   ) : DevicePosture
}

To get information about folding posture, check WindowLayoutInfo, which gives you the foldingFeature of any device. Add this in the MainActivity's onCreate() method before calling setContent().

MainActivity.kt

override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   /**
    * Flow of [DevicePosture] that emits every time there's a change in the windowLayoutInfo
    */
   val devicePostureFlow =  WindowInfoTracker.getOrCreate(this).windowLayoutInfo(this)
       .flowWithLifecycle(this.lifecycle)
       .map { layoutInfo ->
           val foldingFeature =
               layoutInfo.displayFeatures
                   .filterIsInstance<FoldingFeature>()
                   .firstOrNull()
           when {
               isBookPosture(foldingFeature) ->
                   DevicePosture.BookPosture(foldingFeature.bounds)

               isSeparating(foldingFeature) ->
                   DevicePosture.Separating(foldingFeature.bounds, foldingFeature.orientation)

               else -> DevicePosture.NormalPosture
           }
       }
       .stateIn(
           scope = lifecycleScope,
           started = SharingStarted.Eagerly,
           initialValue = DevicePosture.NormalPosture
       )
   
   
    setContent {
       // App compose content
   }
}

Observe the device posture flow as a Compose state reacts to fold state changes. Add these changes to setContent().

MainActivity.kt

setContent {
   ReplyTheme {
       val uiState = viewModel.uiState.collectAsState().value
       val windowSize = calculateWindowSizeClass(this)
       val devicePosture = devicePostureFlow.collectAsState().value
       ReplyApp(uiState, windowSize.widthSizeClass, devicePosture)
   }
}

Now accommodate devicePosture params in the **ReplyApp()**composable in ReplyApp.kt.

ReplyApp.kt

@Composable
fun ReplyApp(
   replyHomeUIState: ReplyHomeUIState,
   windowSize: WindowWidthSizeClass,
   foldingDevicePosture: DevicePosture,
 )

Update your ReplyAppPreview() composable to have device postures.

MainActivity.kt

@Preview(showBackground = true)
@Composable
fun ReplyAppPreview() {
   ReplyTheme {
       ReplyApp(
           replyHomeUIState = ReplyHomeUIState(emails = LocalEmailsDataProvider.allEmails),
           windowSize = WindowWidthSizeClass.Compact,
           foldingDevicePosture = DevicePosture.NormalPosture,
       )
   }
}

@Preview(showBackground = true, widthDp = 700)
@Composable
fun ReplyAppPreviewTablet() {
   ReplyTheme {
       ReplyApp(
           replyHomeUIState = ReplyHomeUIState(emails = LocalEmailsDataProvider.allEmails),
           windowSize = WindowWidthSizeClass.Medium,
           foldingDevicePosture = DevicePosture.NormalPosture,
       )
   }
}

@Preview(showBackground = true, widthDp = 1000)
@Composable
fun ReplyAppPreviewDesktop() {
   ReplyTheme {
       ReplyApp(
           replyHomeUIState = ReplyHomeUIState(emails = LocalEmailsDataProvider.allEmails),
           windowSize = WindowWidthSizeClass.Expanded,
           foldingDevicePosture = DevicePosture.NormalPosture,
       )
   }
}

The Compose UI is now ready to react to both device size and fold state changes. You can continue from here to design your UI for different states. Whenever there are fold state changes you want your UI to react like this:

Foldable UI adaptation

Check out more about fold postures and hinges.

So far we have just made ReplyApp() composable to observe size, configuration, and fold posture changes, but no UI elements are actually using this information. In the next steps, you'll use these parameters to determine your user interface.

5. Dynamic navigation

Let's start adapting the UI with navigation, as it's the first thing users interact with. Note that users hold different types of devices differently so different types of navigation may provide better reachability and ergonomics. Material provides several Material navigation components that help you implement navigation, depending on the window size class of the device.

Bottom navigation

Bottom navigation is perfect for compact sizes, as we naturally hold the device where our thumb can easily reach all the bottom navigation touch points. Use it whenever you have a compact device size or a foldable in a compact folded state.

Bottom navigation bar with items

For a medium size device, or most mobile phones in landscape orientation, the navigation rail is great for easy navigation and reachability as our thumb naturally falls on the top left of the device. You can also use the navigation drawer, along with the navigation rail, to show more information.

Navigation rail with items

The navigation drawer provides an easy way to see detailed information for navigation tabs, and is easily accessible when you're using tablets or larger devices. You can use the navigation drawer along with the navigation rail or Bottom navigation. There are two kinds of navigation drawers available: a modal navigation drawer and a permanent navigation drawer.

Modal Navigation drawer

You can also use a modal navigation drawer for compact to medium size phones and tablets as it can be expanded or hidden as an overlay on the content.

Modal navigation bar with items

Permanent navigation drawer

You can also use a permanent navigation drawer for fixed navigation for large tablets, Chromebooks, and desktops.

Permanent navigation bar with items

Implementing dynamic navigation

Now let's switch between different types of navigation as the device state and size changes, while keeping the user interaction and reachability at its core.

Depending on the window size class, set the different navigation type as suitable. For compact categories, add bottom navigation, and for medium size windows use the navigation rail. For large tablets, desktop use the permanent navigation drawer as you have enough space and a touch target.

Let's add dynamic navigation to the app. Open ReplyApp.kt and add this at the start of the ReplyApp() composable.

ReplyApp.kt

/**
* This will help us select type of navigation and content type depending on window size and
* fold state of the device.
*/
val navigationType: ReplyNavigationType

when (windowSize) {
   WindowWidthSizeClass.Compact -> {
       navigationType = ReplyNavigationType.BOTTOM_NAVIGATION
   }
   WindowWidthSizeClass.Medium -> {
       navigationType = ReplyNavigationType.NAVIGATION_RAIL
   }
   WindowWidthSizeClass.Expanded -> {
       navigationType = ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER
   }
   else -> {
       navigationType = ReplyNavigationType.BOTTOM_NAVIGATION
   }
}

ReplyNavigationWrapperUI(navigationType, replyHomeUIState)

In this code we use windowSizeClass width info to decide the type of navigation needed and pass navigationType to the ReplyNavigationWrapperUI() composable.

As for handling fold states, you also want to take the hinge position into consideration and avoid placing touch action or readable content at the hinge or crease area. For large desktops or tablets where you would use a permanent navigation drawer, switch to the navigation rail and divide content around the crease or hinge.

Replace the above code inside the expanded type of window width size class.

ReplyApp.kt

WindowWidthSizeClass.Expanded -> {
   navigationType = if (foldingDevicePosture is DevicePosture.BookPosture) {
       ReplyNavigationType.NAVIGATION_RAIL
   } else {
       ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER
   }
}

You have your navigation type all set now, so add it to ReplyNavigationWrapperUI() and ReplyListOnlyContent() to pass down the composable tree.

ReplyApp.kt

@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ReplyNavigationWrapperUI(
   navigationType: ReplyNavigationType,
   replyHomeUIState: ReplyHomeUIState
) {
   val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
   val scope = rememberCoroutineScope()
   val selectedDestination = ReplyDestinations.INBOX

   ReplyAppContent(navigationType, replyHomeUIState)
}

@Composable
fun ReplyAppContent(
   navigationType: ReplyNavigationType,
   replyHomeUIState: ReplyHomeUIState,
   onDrawerClicked: () -> Unit = {}
)

Since the navigation drawer acts as the container for your UI ReplyAppContent, wrap it with a permanent or modal navigation drawer depending on your navigationType. Replace the code inside ReplyNavigationWrapperUI.

ReplyApp.kt

@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ReplyNavigationWrapperUI(
   navigationType: ReplyNavigationType,
   replyHomeUIState: ReplyHomeUIState
) {
    val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
    val scope = rememberCoroutineScope()
    val selectedDestination = ReplyDestinations.INBOX


   if (navigationType == ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER) {
       PermanentNavigationDrawer(
           drawerContent = { 
               PermanentDrawerSheet {
                   NavigationDrawerContent(selectedDestination) 
               }
           }
       ) {
           ReplyAppContent(navigationType, replyHomeUIState)
       }
   } else {
       ModalNavigationDrawer(
           drawerContent = {
               ModalDrawerSheet {
                   NavigationDrawerContent(
                       selectedDestination,
                       onDrawerClicked = {
                           scope.launch {
                               drawerState.close()
                           }
                       }
                   )
               }
           },
           drawerState = drawerState
       ) {
           ReplyAppContent(
               navigationType, replyHomeUIState,
               onDrawerClicked = {
                   scope.launch {
                       drawerState.open()
                   }
               }
           )
       }
   }
}

Now you have a dynamic navigationType that can be used to change the type of navigation drawer whenever there are any configuration changes. In the ReplyAppContent() composable, use navigationType to decide the placement of the navigation rail or bottom navigation. Replace ReplyAppContent() with this code**.**

ReplyApp.kt

@Composable
fun ReplyAppContent(
   navigationType: ReplyNavigationType,
   replyHomeUIState: ReplyHomeUIState,
   onDrawerClicked: () -> Unit = {}
) {
   Row(modifier = Modifier.fillMaxSize()) {
       AnimatedVisibility(visible = navigationType == ReplyNavigationType.NAVIGATION_RAIL) {
           ReplyNavigationRail(
               onDrawerClicked = onDrawerClicked
           )
       }
       Column(modifier = Modifier
           .fillMaxSize()
           .background(MaterialTheme.colorScheme.inverseOnSurface)
       ) {
           ReplyListOnlyContent(replyHomeUIState = replyHomeUIState, modifier = Modifier.weight(1f))

           AnimatedVisibility(visible = navigationType == ReplyNavigationType.BOTTOM_NAVIGATION) {
               ReplyBottomNavigationBar()
           }
       }
   }
}

You wrapped both navigation rail and bottom navigation in the AnimatedVisibility() composable, which helps you automatically animate the entry and exit visibility of each navigation depending on the navigationType value passed to it at visible param.

When you run the app again on your resizable emulator and try out different types, notice that whenever the screen configuration changes or you unfold a folding device, the navigation changes to the appropriate type for that size.

Showing adaptability changes for different size of devices.

Congratulations, you've learned about different types of navigation to support different types of screen sizes and states!

In the next section you explore how to take advantage of any remaining screen area instead of stretching the same list item edge to edge.

6. Screen space use

No matter if you're running the app on a small tablet, unfolded device, or large tablet, the screen is stretched to fill the remaining space. You want to make sure you can take advantage of that screen space to show more info, like for this app, showing email and threads to users on the same page.

As with navigationType, you create a contentType that helps you decide whether to show just a list content, or show both a list and detail content dynamically on screen state changes.

Now add contentType to the app. Open ReplyApp.kt and update the start of the ReplyApp() composable where you added the navigation type.

ReplyApp.kt

val navigationType: ReplyNavigationType
val contentType: ReplyContentType

when (windowSize) {
   WindowWidthSizeClass.Compact -> {
       navigationType = ReplyNavigationType.BOTTOM_NAVIGATION
       contentType = ReplyContentType.LIST_ONLY
   }
   WindowWidthSizeClass.Medium -> {
       navigationType = ReplyNavigationType.NAVIGATION_RAIL
       contentType = ReplyContentType.LIST_ONLY
   }
   WindowWidthSizeClass.Expanded -> {
       navigationType = if (foldingDevicePosture is DevicePosture.BookPosture) {
           ReplyNavigationType.NAVIGATION_RAIL
       } else {
           ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER
       }
       contentType = ReplyContentType.LIST_AND_DETAIL
   }
   else -> {
       navigationType = ReplyNavigationType.BOTTOM_NAVIGATION
       contentType = ReplyContentType.LIST_ONLY
   }
}

ReplyNavigationWrapperUI(navigationType, contentType, replyHomeUIState)

We can also take into consideration the fold posture and hinge while showing only the list or list and detail content. Whenever we detect that there is a crease or hinge available dividing a medium size foldable, choose the LIST_AND_DETAIL content type to avoid content or touch targets at the hinge. Let's add support for that for the Medium windowSizeClass value.

ReplyApp.kt

WindowWidthSizeClass.Medium -> {
   navigationType = ReplyNavigationType.NAVIGATION_RAIL
   contentType = if (foldingDevicePosture is DevicePosture.BookPosture
       || foldingDevicePosture is DevicePosture.Separating) {
       ReplyContentType.LIST_AND_DETAIL
   } else {
       ReplyContentType.LIST_ONLY
   }
}

Now that you have your content type all set, add it to ReplyNavigationWrapperUI() and ReplyListOnlyContent() to pass down the composable tree as you did previously for the navigation type.

ReplyApp.kt

@Composable
private fun ReplyNavigationWrapperUI(
   navigationType: ReplyNavigationType,
   contentType: ReplyContentType,
   replyHomeUIState: ReplyHomeUIState
) {
   // App drawer state

   if (navigationType == ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER) {
       PermanentNavigationDrawer(drawerContent = {...}) {
           ReplyAppContent(navigationType, contentType, replyHomeUIState)
       }
   } else {
       ModalNavigationDrawer(
           drawerContent = {...},
           drawerState = drawerState
       ) {
           ReplyAppContent(
               navigationType, contentType, replyHomeUIState,
               onDrawerClicked = {...}
           )
       }
   }

}



@Composable
fun ReplyAppContent(
   navigationType: ReplyNavigationType,
   contentType: ReplyContentType,
   replyHomeUIState: ReplyHomeUIState,
   onDrawerClicked: () -> Unit = {}
)

You can now use this contentType in the ReplyAppContent() composable and determine the layout type.

ReplyApp.kt

@Composable
fun ReplyAppContent(
   navigationType: ReplyNavigationType,
   contentType: ReplyContentType,
   replyHomeUIState: ReplyHomeUIState,
   onDrawerClicked: () -> Unit = {}
) {
   Row(modifier = Modifier.fillMaxSize()) {
       AnimatedVisibility(visible = navigationType == ReplyNavigationType.NAVIGATION_RAIL) {
           ReplyNavigationRail(
               onDrawerClicked = onDrawerClicked
           )
       }
       Column(modifier = Modifier
           .fillMaxSize()
           .background(MaterialTheme.colorScheme.inverseOnSurface)
       ) {
           if (contentType == ReplyContentType.LIST_AND_DETAIL) {
               ReplyListAndDetailContent(
                   replyHomeUIState = replyHomeUIState,
                   modifier = Modifier.weight(1f),
               )
           } else {
               ReplyListOnlyContent(replyHomeUIState = replyHomeUIState, modifier = Modifier.weight(1f))
           }

           AnimatedVisibility(visible = navigationType == ReplyNavigationType.BOTTOM_NAVIGATION) {
               ReplyBottomNavigationBar()
           }
       }
   }
}

37194a7fbdb35544.png

Run the app again on a resizable emulator for all the different types of devices and notice that whenever the screen configuration changes, or we unfold a folding device, the navigation and screen content dynamically changes in response to the device state changes. Jetpack Compose makes these kinds of changes very easy to write in a declarative pattern.

Showing adaptability changes for different size of devices.

Congratulations, you've successfully made your app adaptable for all kinds of device states and sizes. Go ahead and play around with running the app in foldables, tablets, or other mobile devices.

In the next parts, you explore how these changes for adaptability help to lay down the structure for reachability as well.

7. Enhance reachability

Reachability is the ability to navigate or use a device without requiring extreme hand positions or changing hand placements to initiate any interaction with an app.

When users hold an opened device, their reach is likely limited. Specify interactions in a layout with these ergonomic regions in mind.

  • Users can reach this area by extending their fingers, which makes it slightly inconvenient for many to reach.
  • Users can reach this area comfortably
  • Reaching this area is challenging when holding the device

In the Reply app, in the Dynamic navigation section, you added multiple modes of navigation to be used depending on the screen state. Material components, like the bottom navigation bar, navigation rail, and navigation drawer, make navigation easily reachable based on how we hold devices of different form factors.

Screen with navigation rail and permanent navigation drawer

We also added a List and detail form factor that lets users easily switch between threads, and to scroll through them on large devices using both left and right hands without changing the placements.

8. Congratulations

Congratulations, you've successfully completed this codelab and learned how to make apps adaptive with Jetpack Compose.

You learned how to check a device's size and fold state, and update your app's UI, navigation, and other functions accordingly. You also learned how adding adaptability also provides enhancements in reachability and the user experience.

What's next?

Check out the other codelabs on the Compose pathway.

Sample apps

  • The sample apps are a collection of many apps that incorporate the best practices explained in codelabs.

Reference docs