Techbase Academy
Android · Lesson 27

Adaptive Layouts and Navigation Drawers

11 phasesMy City: Ibadan GuideAndroid Studio
Phase 1 of 11
Lesson Introduction

Your app will not only run on the phone in your pocket. People open it on a small budget phone, a large phone in landscape, a tablet, a foldable that opens like a book, a Chromebook, or a window that is dragged to half the screen. If your layout is designed for one width, it will look stretched and empty on a tablet and cramped on a small screen.

An adaptive app changes its layout to fit the space it is given. In this lesson you will learn the tools Android gives you: window size classes, adaptive navigation (a bottom bar on phones, a side rail or drawer on large screens) and a list-detail layout that shows two panes side by side when there is room. You will apply them to the official Unit 4 examples: the Reply email app and the Sports practice app, and plan the My City project.

This lesson covers six items of Pathway 3, Unit 4 of Google's Android Basics with Compose course: Adaptive layouts, Intro to Reply app, Build an adaptive app with dynamic navigation, Build an app with an adaptive layout, Practice: Build Sports app and Project: Create a My City app.

By the end you will be able to:

  • Explain why apps must adapt, and what window size classes are.
  • Read the current window size class and branch on it.
  • Choose between bottom navigation bar, navigation rail and navigation drawer.
  • Use NavigationSuiteScaffold to switch the navigation type automatically.
  • Build a list-detail layout that shows one pane on a phone and two panes on a tablet.
  • Test your layout on different emulators and with the resizable emulator.

Big idea: do not ask "is this a tablet?". Ask "how much room does my window have right now?". The window can change at any time.

✏️ Your Task
Practise what you just learned about Lesson Introduction. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 2 of 11
Prerequisite Concepts
  • Navigation (lesson 26). You will move between destinations.
  • ViewModel and UI state (lessons 24 and 25). Adaptive layouts depend on state that survives window changes.
  • Lazy lists (lesson 21). List-detail screens use them.
  • Rotation is a configuration change (lesson 23). Window size changes, such as unfolding a phone or dragging a split-screen divider, are configuration changes too, so your state must survive them.
✏️ Your Task
Practise what you just learned about Prerequisite Concepts. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 3 of 11
Conceptual Understanding: Window Size Classes

A window size class puts the app window into a small set of buckets. You get one bucket for width and one for height.

Class Width Height Typical device
Compact under 600 dp under 480 dp Phone in portrait (width), phone in landscape (height)
Medium 600 to under 840 dp 480 to under 900 dp Small tablet, foldable, large phone in landscape
Expanded 840 dp or more 900 dp or more Tablet, unfolded foldable, desktop window

Newer versions of the library also define large (1200 dp and up) and extra-large (1600 dp and up) width classes for desktops and external displays. They are opt-in; check the current documentation before relying on them.

Two important facts:

  1. A size class describes the window, not the device. A tablet in split-screen can be compact. A phone in landscape can be medium wide.
  2. The class changes at run time. Rotation, folding, resizing, and multi-window all change it, so your UI must react.

What changes at each size?

text
COMPACT width (phone portrait)
+------------------+
|     content      |
|                  |
|                  |
+------------------+
| [Home][Inbox][Me]|   <- bottom navigation bar
+------------------+

MEDIUM width (small tablet, foldable)
+---+--------------+
| R |              |
| A |   content    |    <- navigation rail on the side
| I |              |
| L |              |
+---+--------------+

EXPANDED width (tablet, desktop)
+--------+---------+-----------+
| Drawer |  list   |  detail   |   <- permanent drawer + two panes
|  Home  |  item 1 |  Selected |
|  Inbox |  item 2 |  item     |
|  Sent  |  item 3 |  content  |
+--------+---------+-----------+

The three navigation components

Component Best for Notes
Navigation bar (bottom) Compact width, 3 to 5 destinations Thumb-friendly on phones
Navigation rail (side) Medium width, 3 to 7 destinations Vertical strip of icons beside the content
Navigation drawer Expanded width, or many destinations Permanent on large screens, modal (slide-in) on small ones

Material guidelines say to avoid a bottom bar on wide screens because the buttons end up far from the thumbs and look lost in empty space.

✏️ Your Task
Practise what you just learned about Conceptual Understanding: Window Size Classes. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 4 of 11
Step-by-Step Build 1: Read the Window Size Class

Add the adaptive library. Look up the current stable version on the Compose Material 3 adaptive release page (https://developer.android.com/jetpack/androidx/releases/compose-material3-adaptive) rather than copying one from a tutorial, and add entries to your version catalog following the pattern from lesson 26:

libs.versions.toml
[versions]
adaptive = "<latest stable>"
[libraries]
androidx-adaptive = { group = "androidx.compose.material3.adaptive", name = "adaptive", version.ref = "adaptive" }
androidx-adaptive-layout = { group = "androidx.compose.material3.adaptive", name = "adaptive-layout", version.ref = "adaptive" }
androidx-adaptive-navigation = { group = "androidx.compose.material3.adaptive", name = "adaptive-navigation", version.ref = "adaptive" }
androidx-material3-adaptive-navigation-suite = { group = "androidx.compose.material3", name = "material3-adaptive-navigation-suite" }

The last entry belongs to the Compose Material 3 group, so with the Compose BOM in your project it needs no version. If your project has no BOM, give it a version from the Material 3 release page. Names and grouping of these artifacts have moved between releases, so if Sync fails, check the official pages for the current coordinates.

In app/build.gradle.kts:

build.gradle.kts
dependencies {
 implementation(libs.androidx.adaptive)
 implementation(libs.androidx.adaptive.layout)
 implementation(libs.androidx.adaptive.navigation)
 implementation(libs.androidx.material3.adaptive.navigation.suite)
}

Branch on the window size

kotlin
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable
import androidx.window.core.layout.WindowSizeClass
@Composable
fun AdaptiveGreeting() {
 val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
 val isExpandedWidth = windowSizeClass.isWidthAtLeastBreakpoint(
 WindowSizeClass.WIDTH_DP_EXPANDED_LOWER_BOUND
 )
 val isMediumWidth = windowSizeClass.isWidthAtLeastBreakpoint(
 WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND
 )
 when {
 isExpandedWidth -> ExpandedLayout()
 isMediumWidth -> MediumLayout()
 else -> CompactLayout()
 }
}

isWidthAtLeastBreakpoint is the current way to compare. Older tutorials and the Unit 4 codelabs may show calculateWindowSizeClass(activity) with WindowWidthSizeClass.Compact/Medium/Expanded values from the material3-window-size-class artifact. That still exists in many projects, so you will recognise it, but prefer the adaptive-info API in new code because it works without an Activity reference and plays well with previews.

Pass the result down as a parameter, rather than reading it deep in the tree, to keep composables easy to preview and test:

kotlin
@Composable
fun ReplyApp(
 windowSize: WindowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
) {
 // choose layout from windowSize
}
✏️ Your Task
Practise what you just learned about Step-by-Step Build 1: Read the Window Size Class. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 5 of 11
Step-by-Step Build 2: Adaptive Navigation with NavigationSuiteScaffold

Reply is an email app with four mailboxes: Inbox, Sent, Drafts, Spam. You will build the shell that swaps the navigation component for you.

The data

Mailbox.kt
package com.example.reply
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Drafts
import androidx.compose.material.icons.filled.Inbox
import androidx.compose.material.icons.filled.Report
import androidx.compose.material.icons.filled.Send
import androidx.compose.ui.graphics.vector.ImageVector
enum class Mailbox(val label: String, val icon: ImageVector) {
 INBOX("Inbox", Icons.Filled.Inbox),
 SENT("Sent", Icons.Filled.Send),
 DRAFTS("Drafts", Icons.Filled.Drafts),
 SPAM("Spam", Icons.Filled.Report)
}
data class Email(
 val id: Int,
 val sender: String,
 val subject: String,
 val body: String,
 val mailbox: Mailbox
)
val sampleEmails = listOf(
 Email(1, "Ada", "Meeting on Friday", "Can we move the meeting to 10am?", Mailbox.INBOX),
 Email(2, "Tunde", "Invoice for materials", "Attached is the invoice for the paint.", Mailbox.INBOX),
 Email(3, "Mum", "Sunday lunch", "Jollof and plantain. Do not be late.", Mailbox.INBOX),
 Email(4, "You", "Re: Meeting on Friday", "10am works for me.", Mailbox.SENT),
 Email(5, "You", "Project idea", "A bus tracking app for Ibadan...", Mailbox.DRAFTS),
 Email(6, "Prize Team", "You have WON", "Send your bank details now.", Mailbox.SPAM)
)

(Icons.Filled.Inbox, Drafts, Report and Send are in the extended icon set, material-icons-extended. In many projects it is not included by default, and it is large, so release builds rely on R8 to remove unused icons. If you see unresolved references, add the extended icons dependency or draw your own icons from vector assets, as in lesson 21.)

The scaffold

ReplyApp.kt
package com.example.reply
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffoldDefaults
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteType
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.window.core.layout.WindowSizeClass
@Composable
fun ReplyApp() {
 var currentMailbox by rememberSaveable { mutableStateOf(Mailbox.INBOX) }
 val adaptiveInfo = currentWindowAdaptiveInfo()
 val layoutType = if (
 adaptiveInfo.windowSizeClass.isWidthAtLeastBreakpoint(WindowSizeClass.WIDTH_DP_EXPANDED_LOWER_BOUND)
 ) {
 NavigationSuiteType.NavigationDrawer // permanent drawer on big screens
 } else {
 NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(adaptiveInfo) // bar or rail
 }
 NavigationSuiteScaffold(
 layoutType = layoutType,
 navigationSuiteItems = {
 Mailbox.entries.forEach { mailbox ->
 item(
 icon = { Icon(mailbox.icon, contentDescription = mailbox.label) },
 label = { Text(mailbox.label) },
 selected = mailbox == currentMailbox,
 onClick = { currentMailbox = mailbox }
 )
 }
 }
 ) {
 EmailListScreen(
 emails = sampleEmails.filter { it.mailbox == currentMailbox },
 title = currentMailbox.label
 )
 }
}

How it behaves:

  • On a phone in portrait, calculateFromAdaptiveInfo returns a navigation bar at the bottom.
  • On a medium window it returns a navigation rail on the side.
  • On an expanded window we override with a permanent drawer.
  • currentMailbox is saved with rememberSaveable, so rotation or folding keeps the selected mailbox. (An enum is saved automatically because it is Serializable.)

Without the override, NavigationSuiteScaffold chooses bar or rail only. Dropping the layoutType argument completely gives you the default behaviour with zero extra code, which is often enough.

A simple list screen

kotlin
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun EmailListScreen(emails: List<Email>, title: String, modifier: Modifier = Modifier) {
 Column(modifier = modifier.fillMaxSize()) {
 Text(title, style = MaterialTheme.typography.headlineSmall, modifier = Modifier.padding(16.dp))
 LazyColumn {
 items(emails, key = { it.id }) { email ->
 Column(Modifier.padding(16.dp)) {
 Text(email.sender, style = MaterialTheme.typography.titleMedium)
 Text(email.subject)
 }
 HorizontalDivider()
 }
 }
 }
}

Run it on a phone emulator and then on a tablet emulator (Device Manager > Create Virtual Device > Tablet). The same code shows a bottom bar on one and a rail or drawer on the other.

✏️ Your Task
Practise what you just learned about Step-by-Step Build 2: Adaptive Navigation with NavigationSuiteScaffold. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 6 of 11
Step-by-Step Build 3: A List-Detail Layout

Navigation chrome is half the story. The other half is the content layout. A mail app on a phone shows the list, and tapping an email opens the full message on a new screen. On a tablet, that wastes space: you want the list on the left and the open email on the right, at the same time. This is the list-detail pattern.

There are two ways to build it:

  1. By hand with a window size branch (what the Reply codelab does, and what you will do now, because it teaches the idea clearly).
  2. With the adaptive layout library (ListDetailPaneScaffold and NavigableListDetailPaneScaffold) which handles back navigation and one-pane/two-pane switching for you. It is the better choice for production apps, and is introduced at the end of this section.

By hand

State lives in a ViewModel so that it survives a window change:

ReplyViewModel.kt
package com.example.reply
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
data class ReplyUiState(
 val mailbox: Mailbox = Mailbox.INBOX,
 val selectedEmailId: Int? = null,
 val isShowingDetailOnly: Boolean = false
)
class ReplyViewModel : ViewModel() {
 private val _uiState = MutableStateFlow(ReplyUiState())
 val uiState: StateFlow<ReplyUiState> = _uiState.asStateFlow()
 fun selectMailbox(mailbox: Mailbox) =
 _uiState.update { it.copy(mailbox = mailbox, selectedEmailId = null, isShowingDetailOnly = false) }
 fun selectEmail(id: Int) =
 _uiState.update { it.copy(selectedEmailId = id, isShowingDetailOnly = true) }
 fun backToList() =
 _uiState.update { it.copy(isShowingDetailOnly = false) }
}

isShowingDetailOnly only matters in compact windows, where one pane is shown at a time. In wide windows both panes show regardless.

The screen chooses by width:

kotlin
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.clickable
@Composable
fun ReplyHomeContent(
 uiState: ReplyUiState,
 emails: List<Email>,
 isWide: Boolean,
 onEmailClick: (Int) -> Unit,
 onBackToList: () -> Unit,
 modifier: Modifier = Modifier
) {
 val selected = emails.firstOrNull { it.id == uiState.selectedEmailId }
 if (isWide) {
 Row(modifier.fillMaxSize()) {
 LazyColumn(Modifier.width(360.dp).fillMaxHeight()) {
 items(emails, key = { it.id }) { email ->
 Column(
 Modifier
 .clickable { onEmailClick(email.id) }
 .padding(16.dp)
 ) {
 Text(email.sender, style = MaterialTheme.typography.titleMedium)
 Text(email.subject)
 }
 HorizontalDivider()
 }
 }
 EmailDetail(selected, Modifier.weight(1f))
 }
 } else if (uiState.isShowingDetailOnly && selected != null) {
 BackHandler(onBack = onBackToList)
 EmailDetail(selected, modifier)
 } else {
 LazyColumn(modifier.fillMaxSize()) {
 items(emails, key = { it.id }) { email ->
 Column(
 Modifier
 .clickable { onEmailClick(email.id) }
 .padding(16.dp)
 ) {
 Text(email.sender, style = MaterialTheme.typography.titleMedium)
 Text(email.subject)
 }
 HorizontalDivider()
 }
 }
 }
}
@Composable
fun EmailDetail(email: Email?, modifier: Modifier = Modifier) {
 Column(modifier.padding(24.dp)) {
 if (email == null) {
 Text("Select an email")
 } else {
 Text(email.subject, style = MaterialTheme.typography.headlineSmall)
 Text("From ${email.sender}", modifier = Modifier.padding(vertical = 8.dp))
 Text(email.body)
 }
 }
}

(Modifier.weight(1f) needs the Row scope, which is why EmailDetail receives the modifier from the Row. Add import androidx.compose.foundation.layout.padding and the Material imports from earlier.)

Notice the duplicated list item markup: you would extract an EmailRow composable in real code. We left it in line so each branch reads on its own.

Test it:

  1. On the phone emulator, tap an email. You see the message alone. Press Back to return to the list.
  2. On the tablet emulator, both appear at once.
  3. Use the resizable emulator (create a virtual device of type Resizable, which lets you switch between phone, foldable, tablet and desktop presets) and switch sizes while the app runs. The selected email must stay selected. If it does not, your state is in the wrong place.

The library way: ListDetailPaneScaffold

The adaptive library provides NavigableListDetailPaneScaffold, which takes a list pane and a detail pane and does the one-pane versus two-pane switch, the back behaviour and the animation for you:

kotlin
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.layout.AnimatedPane
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole
import androidx.compose.material3.adaptive.navigation.NavigableListDetailPaneScaffold
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
import kotlinx.coroutines.launch
import androidx.compose.runtime.rememberCoroutineScope
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun AdaptiveEmailScreen(emails: List<Email>) {
 val navigator = rememberListDetailPaneScaffoldNavigator<Int>()
 val scope = rememberCoroutineScope()
 NavigableListDetailPaneScaffold(
 navigator = navigator,
 listPane = {
 AnimatedPane {
 LazyColumn {
 items(emails, key = { it.id }) { email ->
 Column(
 Modifier
 .clickable {
 scope.launch {
 navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, email.id)
 }
 }
 .padding(16.dp)
 ) { Text(email.subject) }
 }
 }
 }
 },
 detailPane = {
 AnimatedPane {
 val id = navigator.currentDestination?.contentKey
 EmailDetail(emails.firstOrNull { it.id == id })
 }
 }
 )
}

This API is marked experimental in several releases and its exact names have changed between versions, which is why the @OptIn is there. If your version differs, follow the current official guide (search "list-detail layout" in the adaptive apps documentation). The manual approach above works on any version, so you can build and ship with it while the library settles.

✏️ Your Task
Practise what you just learned about Step-by-Step Build 3: A List-Detail Layout. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 7 of 11
Step-by-Step Build 4: The Sports App Practice

The Practice: Build Sports app exercise asks you to use everything above on a new app. Here is the specification to build, with your own data:

  • A list of sports (for example Football, Basketball, Athletics, Table tennis, Ayo), each with a name, a one-line description and a longer description.
  • Compact: a list screen; tapping a sport opens a detail screen; Back returns.
  • Expanded: list on the left, detail on the right, no separate detail screen.
  • A top app bar that shows the app name, or the selected sport name on the detail screen, with an Up arrow when appropriate.
  • A toggle in the top bar to switch the list between list view and grid view (lesson 21).

Use ReplyViewModel's shape for your state: selectedSportId, isShowingDetailOnly, isListView. This is a good moment to try designing the state class before writing any composable.

✏️ Your Task
Practise what you just learned about Step-by-Step Build 4: The Sports App Practice. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 8 of 11
Project Planning: My City

The unit ends with a project: Create a My City app. Write the specification yourself:

  1. Pick a city or town you know (Ibadan, Lagos, Abuja, Kano, or your village).
  2. Define 4 to 5 categories such as Restaurants, Parks, Markets, Historic sites.
  3. Each category has at least 5 recommendations with a name, a short description and a picture or icon.
  4. Navigation: a start screen of categories; a list of places for the chosen category; a detail screen for one place.
  5. Adaptive: compact uses a bottom bar for categories and a list-detail flow. Expanded shows a rail or drawer plus two panes.
  6. The app uses one ViewModel, immutable UI state, previews for every screen, unit tests for the ViewModel and one UI test.
  7. Every image has a meaningful contentDescription (lesson 47).

Resist the urge to start with the visuals. Sketch screens on paper for the compact and expanded layouts, write the state class, then code.

✏️ Your Task
Practise what you just learned about Project Planning: My City. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 9 of 11
Guided Practice Exercises
🎯 Your Challenge

Exercise 1: Responsive column count

Write ResponsiveGrid that shows a LazyVerticalGrid with 1 column on compact width, 2 on medium and 3 on expanded, using the window size class. Then explain why GridCells.Adaptive(minSize = ...) might be a better answer.

kotlin
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material3.Card
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.window.core.layout.WindowSizeClass
@Composable
fun ResponsiveGrid(items: List<String>, modifier: Modifier = Modifier) {
 val size = currentWindowAdaptiveInfo().windowSizeClass
 val columns = when {
 size.isWidthAtLeastBreakpoint(WindowSizeClass.WIDTH_DP_EXPANDED_LOWER_BOUND) -> 3
 size.isWidthAtLeastBreakpoint(WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND) -> 2
 else -> 1
 }
 LazyVerticalGrid(
 columns = GridCells.Fixed(columns),
 modifier = modifier,
 contentPadding = PaddingValues(8.dp)
 ) {
 items(items) { Card(Modifier.padding(4.dp)) { Text(it, Modifier.padding(16.dp)) } }
 }
}

GridCells.Adaptive(minSize = 200.dp) is often better because it reacts to the actual width in dp continuously, not just to three buckets, and needs no window size code at all. Use window size classes when the structure changes (bar versus rail, one pane versus two) and flexible sizing for density.

🎯 Your Challenge

Exercise 2: Preserve selection across folding

A teammate stores selectedEmailId with remember inside ReplyHomeContent. Describe what the user sees when they unfold the phone and how to fix it.

Unfolding changes the window size, which is a configuration change, so the Activity is recreated and remember state is lost: the selected email disappears and the user is returned to an empty detail pane. Fix it by holding selectedEmailId in the ViewModel's UI state (it survives configuration changes) or, for a purely local choice, rememberSaveable. The architecture rule from lessons 23 and 24 applies directly: state that matters must not live in a plain remember.

✏️ Your Task

Create three emulators: a phone, the Resizable device and a tablet. Run Reply on each, and write down which navigation component appears at which width. Then resize the resizable emulator slowly and note the exact moment it changes.

Phase 10 of 11
Common Beginner Mistakes (and How to Fix Them)

Mistake #1: Checking "is it a tablet?". What happens: split-screen and foldables break your assumptions. The fix: use the window size class, which describes the available window.

Mistake #2: Hard-coded widths. The wrong code: Modifier.width(400.dp) for a panel. What happens: it overflows on small windows. The fix: use fillMaxWidth, weight, fractions or size-class branches.

Mistake #3: Forgetting to handle Back in the one-pane detail view. What happens: Back exits the app instead of returning to the list. The fix: a BackHandler that clears the detail-only flag.

Mistake #4: Locking orientation to avoid adaptive work. What happens: on large screens the setting may be ignored or letterboxed, and Google Play may flag poor large-screen quality. The fix: support resizing.

Mistake #5: Selection state lost on resize. The fix: keep it in a ViewModel or rememberSaveable.

Mistake #6: A bottom bar on a wide screen. What happens: the bar stretches across a huge window and its items are unreachable. The fix: rail or drawer from medium width up.

Mistake #7: Two sources of truth for the selected destination. What happens: the drawer shows one thing, the content another. The fix: one currentMailbox value drives both.

Mistake #8: Using experimental APIs blindly. What happens: an update renames a function and the build breaks. The fix: read the release notes, pin versions, and keep experimental API usage in one place.

✏️ Your Task
Practise what you just learned about Common Beginner Mistakes (and How to Fix Them). Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 11 of 11
Knowledge Check

Question 1. What does a window size class describe?

The space available to the app's window (width and height buckets: compact, medium, expanded), not the device type. It can change while the app runs.

Question 2. Which navigation component suits compact, medium and expanded widths?

A bottom navigation bar for compact, a navigation rail for medium, and a navigation drawer (often permanent) for expanded.

Question 3. What is a list-detail layout?

A layout that shows a list and the details of the selected item. On narrow windows they are separate screens; on wide windows they appear side by side.

Question 4. What does NavigationSuiteScaffold do for you?

It displays a navigation bar, rail or drawer depending on window size, using the destinations you declare once, and lays out your content beside or above it.

Question 5. Why must state survive window size changes?

Resizing, folding and rotation are configuration changes that recreate the Activity. State held only in remember is lost, so important state belongs in a ViewModel or rememberSaveable.

Go deeper: official resources

✏️ Your Task
Practise what you just learned about Knowledge Check. Open Android Studio, type the examples by hand, change a value, and observe what happens.
🏗️ Build It — Mini Project
My City: Ibadan Guide

Project goal: Build the My City project for Ibadan or your own city, in miniature: three categories, three places each, adaptive layout, tested ViewModel.

Requirements:

  1. Categories: Food, Landmarks, Markets (or your own). Each has three places with a name and description.
  2. CityViewModel with a CityUiState(category, selectedPlaceId, isShowingDetailOnly).
  3. NavigationSuiteScaffold with one item per category.
  4. List-detail layout: one pane in compact, two panes in expanded.
  5. Back from the detail returns to the list.
  6. Two ViewModel unit tests: changing category clears the selected place; selecting a place sets detail-only.

Stretch goal: display a Card with an image for each place, with a contentDescription that describes it.

CityApp.kt
package com.example.mycity
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Place
import androidx.compose.material.icons.filled.ShoppingCart
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.window.core.layout.WindowSizeClass
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
enum class Category(val label: String, val icon: ImageVector) {
 FOOD("Food", Icons.Filled.Home),
 LANDMARKS("Landmarks", Icons.Filled.Place),
 MARKETS("Markets", Icons.Filled.ShoppingCart)
}
data class CityPlace(val id: Int, val name: String, val description: String, val category: Category)
val places = listOf(
 CityPlace(1, "Amala spot", "Amala with gbegiri and ewedu, a local favourite.", Category.FOOD),
 CityPlace(2, "Suya corner", "Evening suya with spicy yaji.", Category.FOOD),
 CityPlace(3, "Roasted plantain stand", "Boli with groundnut.", Category.FOOD),
 CityPlace(4, "Cocoa House", "A well-known tall building in the city skyline.", Category.LANDMARKS),
 CityPlace(5, "Mapo Hall", "A colonial-era hall on a hill overlooking the city.", Category.LANDMARKS),
 CityPlace(6, "Bower's Tower", "A tower with wide views over the rooftops.", Category.LANDMARKS),
 CityPlace(7, "Bodija Market", "A busy market for foodstuffs.", Category.MARKETS),
 CityPlace(8, "Dugbe Market", "A trading centre in the city.", Category.MARKETS),
 CityPlace(9, "Oje Market", "Known for traditional cloth.", Category.MARKETS)
)
data class CityUiState(
 val category: Category = Category.FOOD,
 val selectedPlaceId: Int? = null,
 val isShowingDetailOnly: Boolean = false
)
class CityViewModel : ViewModel() {
 private val _uiState = MutableStateFlow(CityUiState())
 val uiState: StateFlow<CityUiState> = _uiState.asStateFlow()
 fun selectCategory(category: Category) =
 _uiState.update { CityUiState(category = category) }
 fun selectPlace(id: Int) =
 _uiState.update { it.copy(selectedPlaceId = id, isShowingDetailOnly = true) }
 fun backToList() =
 _uiState.update { it.copy(isShowingDetailOnly = false) }
}
@Composable
fun CityApp(viewModel: CityViewModel = viewModel()) {
 val state by viewModel.uiState.collectAsStateWithLifecycle()
 val isWide = currentWindowAdaptiveInfo().windowSizeClass
 .isWidthAtLeastBreakpoint(WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND)
 NavigationSuiteScaffold(
 navigationSuiteItems = {
 Category.entries.forEach { category ->
 item(
 icon = { Icon(category.icon, contentDescription = category.label) },
 label = { Text(category.label) },
 selected = category == state.category,
 onClick = { viewModel.selectCategory(category) }
 )
 }
 }
 ) {
 val list = places.filter { it.category == state.category }
 val selected = list.firstOrNull { it.id == state.selectedPlaceId }
 if (isWide) {
 Row(Modifier.fillMaxSize()) {
 PlaceList(list, viewModel::selectPlace, Modifier.width(320.dp).fillMaxHeight())
 PlaceDetail(selected, Modifier.weight(1f))
 }
 } else if (state.isShowingDetailOnly && selected != null) {
 BackHandler(onBack = viewModel::backToList)
 PlaceDetail(selected, Modifier.fillMaxSize())
 } else {
 PlaceList(list, viewModel::selectPlace, Modifier.fillMaxSize())
 }
 }
}
@Composable
fun PlaceList(list: List<CityPlace>, onClick: (Int) -> Unit, modifier: Modifier = Modifier) {
 LazyColumn(modifier) {
 items(list, key = { it.id }) { place ->
 Text(
 place.name,
 style = MaterialTheme.typography.titleMedium,
 modifier = Modifier.clickable { onClick(place.id) }.padding(16.dp)
 )
 HorizontalDivider()
 }
 }
}
@Composable
fun PlaceDetail(place: CityPlace?, modifier: Modifier = Modifier) {
 Column(modifier.padding(24.dp)) {
 if (place == null) Text("Choose a place")
 else {
 Text(place.name, style = MaterialTheme.typography.headlineSmall)
 Text(place.description, modifier = Modifier.padding(top = 8.dp))
 }
 }
}
CityViewModelTest.kt
package com.example.mycity
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
class CityViewModelTest {
 @Test
 fun changingCategory_clearsSelectedPlace() {
 val vm = CityViewModel()
 vm.selectPlace(1)
 vm.selectCategory(Category.MARKETS)
 assertNull(vm.uiState.value.selectedPlaceId)
 assertEquals(Category.MARKETS, vm.uiState.value.category)
 }
 @Test
 fun selectingPlace_setsDetailOnly() {
 val vm = CityViewModel()
 vm.selectPlace(4)
 assertTrue(vm.uiState.value.isShowingDetailOnly)
 assertEquals(4, vm.uiState.value.selectedPlaceId)
 }
}

Notes: the place descriptions are short, general statements; check facts about real landmarks and markets yourself before publishing an app about your city. This solution treats medium width and above as "wide" to keep two panes on small tablets; adjust the breakpoint to taste. Icons.Filled.Home, Place and ShoppingCart are in the core icon set, so no extended icon dependency is needed.

1

Open your finished project in Android Studio. Check that the .gitignore file the wizard created is in the project root. It keeps local.properties, build folders and keystores out of Git. Never commit passwords or signing keys.

2

Choose Git > Create Git Repository… (or VCS > Enable Version Control Integration in older versions), select the project folder, then open Git > Commit, tick every file, and write the message “Add Android lesson 27 project”.

3

On github.com click New repository, name it android-lesson-27-adaptive-layouts-and-navigation-drawers, set it to Public, leave Add a README unticked, and click Create repository.

4

Back in Android Studio choose Git > Manage Remotes…, add the repository URL as origin, then choose Git > Push… and push your main branch. Prefer the terminal? Run git remote add origin <your-url> then git push -u origin main.

5

Add a README.md with a screenshot of your app running and a short description. Employers and clients read READMEs first. Your project is now part of your public portfolio. 🎉

Lesson 27 complete! 🎉

You covered:

  • Lesson Introduction
  • Prerequisite Concepts
  • Conceptual Understanding: Window Size Classes
  • Step-by-Step Build 1: Read the Window Size Class
  • Step-by-Step Build 2: Adaptive Navigation with NavigationSuiteScaffold
  • Step-by-Step Build 3: A List-Detail Layout
  • Step-by-Step Build 4: The Sports App Practice
  • Project Planning: My City
  • Guided Practice Exercises
  • Common Beginner Mistakes (and How to Fix Them)
  • Knowledge Check
All 72 lessons in Google Android Studio ·Techbase Academy ·© 2025–2026 Techbase Consultant Services, Ibadan
Esc
↑ ↓ to moveEnter to openEsc to close