Techbase Academy
Android · Lesson 58

Adaptive Apps for Phones, Tablets and Foldables

11 phasesClassRegister for Every ScreenAndroid Studio
Phase 1 of 11
Lesson Introduction

In lesson 27 you met window size classes and built a first list-detail layout. This lesson goes further. Your app will not only run on a phone held upright. It will run on tablets, on foldables that open and close, on Chromebooks, in split-screen and freeform windows, on a desktop-sized display, and in a window the user drags to any shape. Android's guidance is simple: do not design for devices, design for window sizes.

This lesson covers Implementing Android apps for all screen sizes and Build adaptive apps from Pathway 5 of the Jetpack Compose for Android developers programme (https://developer.android.com/courses/pathways/jetpack-compose-for-android-developers-5), and the adaptive-layouts material linked from it (for example the codelab at https://codelabs.developers.google.com/jetpack-compose-adaptability).

By the end you will be able to:

  • Explain why window size, not device type or orientation, drives layout.
  • Read window size classes and build breakpoint-based layouts.
  • Apply the canonical layouts: list-detail, supporting pane and feed.
  • Use the Material 3 adaptive scaffolds and navigation suite (with care about their release status).
  • React to foldable postures such as tabletop and book with Jetpack WindowManager.
  • Support resizing, configuration changes, keyboard, mouse and stylus input.
  • Test adaptive layouts on a resizable emulator.

Big idea: a window is a rectangle whose size can change at any time. Write layouts as functions of that rectangle and every device, present and future, is covered.

Honesty note: the adaptive libraries are updated often and some APIs are marked experimental. Class names below match the libraries at the time of writing; check the official release notes for the current names and stability before shipping.

✏️ 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
  • Window size classes, NavigationSuiteScaffold and the hand-built list-detail (lesson 27).
  • ViewModel and state hoisting (lessons 25 and 37).
  • Navigation Compose (lessons 26 and 55).
  • Lazy lists and grids (lessons 21 and 39).
  • Android Studio emulators (lesson 4).
✏️ 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: Why Adaptive?

Large screens are mainstream

Tablets, foldables and ChromeOS devices are a large audience, and on a big screen a stretched phone layout looks lazy: lines of text 1,000 pixels wide, one small button in a sea of white. Google Play also reviews apps against large-screen quality guidelines, and apps that adapt can get better visibility on those devices. (Exact Play surfacing rules change; check the current documentation.)

Orientation is not the signal

An old habit was "landscape means wide". But a phone in split-screen is narrow even in landscape, and a tablet in portrait is still wide. A window can be resized by the user at any time. So the question to ask is "how many dp wide and tall is my window now?", never "what device is this?".

Window size classes

Class Width breakpoint Typical window
Compact below 600 dp Phone in portrait, small split-screen pane
Medium 600 dp up to 840 dp Tablet in portrait, unfolded foldable, large phone in landscape
Expanded 840 dp and above Tablet in landscape, desktop windows

Newer releases of the window-size-class library add larger breakpoints (such as large and extra-large widths for desktop-like windows). Check the current WindowManager documentation for the full set. Height classes (compact, medium, expanded) exist too; width is usually the one that drives navigation and pane layouts.

Canonical layouts

Rather than invent a layout from scratch, use proven patterns:

Layout Idea Example
List-detail A list on one side, the selected item's detail on the other Email, messages, a student register
Supporting pane A main pane plus a smaller related pane A document with comments, a lesson with notes
Feed Content cards in a flexible grid that grows columns with width News, social, product catalogue

Each layout shows one pane on compact windows (navigating between them) and two panes on wide windows.

Adaptive navigation

Window Navigation component
Compact Navigation bar at the bottom
Medium Navigation rail at the side
Expanded Navigation rail or permanent navigation drawer

NavigationSuiteScaffold chooses automatically (lesson 27).

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

The adaptive libraries are separate from Compose Material 3. In libs.versions.toml use placeholders for versions and look up the latest stable ones:

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

Add the entries you need to build.gradle.kts with implementation(libs....). (Coordinates can change as libraries graduate; confirm them in the documentation.)

Calculate the current window size class

kotlin
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
class MainActivity : ComponentActivity() {
 @OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
 override fun onCreate(savedInstanceState: Bundle?) {
 super.onCreate(savedInstanceState)
 enableEdgeToEdge()
 setContent {
 AppTheme {
 val sizeClass = calculateWindowSizeClass(this)
 SchoolApp(widthClass = sizeClass.widthSizeClass)
 }
 }
 }
}

An alternative that does not need the activity is currentWindowAdaptiveInfo() from the adaptive library, which returns the window size class and fold information as a state-backed value:

kotlin
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.window.core.layout.WindowWidthSizeClass
@Composable
fun SchoolApp() {
 val info = currentWindowAdaptiveInfo()
 val widthClass = info.windowSizeClass.windowWidthSizeClass
 val twoPane = widthClass != WindowWidthSizeClass.COMPACT
 // ...
}

(The exact property and type names have shifted between releases, for example towards width-in-dp comparisons such as isWidthAtLeastBreakpoint(...). Check the current API; the idea is stable.)

Rule: pass size information down as parameters (a boolean such as twoPane, or the size class). Composables deep in the tree should not call calculateWindowSizeClass themselves; hoist it once at the top.

✏️ Your Task
Practise what you just learned about Step-by-Step Build 1: Dependencies and a 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: A Master-Detail Register, Properly

Our running example is ClassRegister: a list of students and a detail pane. The state lives in a ViewModel and is shared by both layouts.

kotlin
data class Student(val id: Int, val name: String, val form: String, val notes: String)
data class RegisterUiState(
 val students: List<Student> = emptyList(),
 val selectedId: Int? = null
) {
 val selected: Student? get() = students.firstOrNull { it.id == selectedId }
}
class RegisterViewModel : ViewModel() {
 private val _uiState = MutableStateFlow(
 RegisterUiState(
 students = listOf(
 Student(1, "Adaeze Okoye", "SS2", "Strong in Mathematics."),
 Student(2, "Tunde Bello", "SS1", "Needs support in English."),
 Student(3, "Funke Adebayo", "SS3", "Prefect. Science class.")
 )
 )
 )
 val uiState: StateFlow<RegisterUiState> = _uiState.asStateFlow()
 fun select(id: Int?) = _uiState.update { it.copy(selectedId = id) }
}

A simple adaptive screen without the scaffold

kotlin
@Composable
fun RegisterScreen(
 state: RegisterUiState,
 twoPane: Boolean,
 onSelect: (Int?) -> Unit,
 modifier: Modifier = Modifier
) {
 if (twoPane) {
 Row(modifier.fillMaxSize()) {
 StudentList(
 students = state.students,
 selectedId = state.selectedId,
 onSelect = { onSelect(it) },
 modifier = Modifier.weight(0.4f)
 )
 VerticalDivider()
 StudentDetail(
 student = state.selected,
 modifier = Modifier.weight(0.6f)
 )
 }
 } else {
 // One pane at a time; back returns to the list
 if (state.selected == null) {
 StudentList(state.students, null, { onSelect(it) }, modifier.fillMaxSize())
 } else {
 BackHandler { onSelect(null) }
 StudentDetail(state.selected, modifier.fillMaxSize(), onBack = { onSelect(null) })
 }
 }
}

Observe what is shared: the ViewModel and selectedId. When a user unfolds a phone, twoPane flips to true, and the same selection now appears as the right-hand pane without losing position. That continuity is the heart of an adaptive app.

Why selectedId not selected? An id survives process changes and recomposition cleanly, and the object is looked up from the current list. Storing the whole object can leave stale copies.

✏️ Your Task
Practise what you just learned about Step-by-Step Build 2: A Master-Detail Register, Properly. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 6 of 11
Step-by-Step Build 3: The Material 3 Adaptive Scaffolds

The Material 3 adaptive library provides scaffolds that implement canonical layouts, including animation, dividers and window-aware behaviour:

Scaffold Layout
ListDetailPaneScaffold List-detail
SupportingPaneScaffold Main pane plus a supporting pane
NavigableListDetailPaneScaffold, NavigableSupportingPaneScaffold The above, plus a navigator that manages which pane is shown and back handling

At the time of writing, parts of this library are marked @ExperimentalMaterial3AdaptiveApi; opt in explicitly and expect small renames.

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
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun RegisterAdaptive(students: List<Student>) {
 val navigator = rememberListDetailPaneScaffoldNavigator<Int>() // content key: the student id
 val scope = rememberCoroutineScope()
 NavigableListDetailPaneScaffold(
 navigator = navigator,
 listPane = {
 AnimatedPane {
 StudentList(
 students = students,
 selectedId = navigator.currentDestination?.contentKey,
 onSelect = { id ->
 scope.launch {
 navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, id)
 }
 }
 )
 }
 },
 detailPane = {
 AnimatedPane {
 val id = navigator.currentDestination?.contentKey
 StudentDetail(student = students.firstOrNull { it.id == id })
 }
 }
 )
}

(Depending on the library version, navigateTo is a suspend function, as here, or a regular one; and navigateBack has similar variants. The IDE will tell you.)

How it behaves:

  • Expanded window: both panes appear side by side. Selecting an item updates the detail pane.
  • Compact window: one pane at a time. navigateTo(Detail, id) shows the detail pane, and the system back gesture returns to the list because the scaffold's navigator handles back.
  • Resizing: when the window changes size, the scaffold re-evaluates how many panes fit and keeps the current destination.

For selection to survive configuration changes, the navigator's content key must be saveable: an Int or String works, an arbitrary class needs a custom saver or @Parcelize/@Serializable.

Supporting pane

kotlin
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun LessonScreen(lesson: Lesson) {
 val navigator = rememberSupportingPaneScaffoldNavigator()
 NavigableSupportingPaneScaffold(
 navigator = navigator,
 mainPane = { AnimatedPane { LessonText(lesson) } },
 supportingPane = { AnimatedPane { LessonNotes(lesson) } }
 )
}

On a compact window the user sees the main pane, with a way (a button) to open the supporting pane, which replaces it; on wide windows both are visible.

Adaptive navigation again, with custom behaviour

kotlin
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold
@Composable
fun AppNav(content: @Composable () -> Unit) {
 var current by rememberSaveable { mutableStateOf(Destination.Register) }
 NavigationSuiteScaffold(
 navigationSuiteItems = {
 Destination.entries.forEach { dest ->
 item(
 selected = current == dest,
 onClick = { current = dest },
 icon = { Icon(dest.icon, contentDescription = null) },
 label = { Text(dest.label) }
 )
 }
 }
 ) {
 content()
 }
}

The suite picks bar, rail or drawer from the window's adaptive info. You can override that choice with a layoutType parameter when you have a reason, for example to keep a bottom bar on a medium window.

✏️ Your Task
Practise what you just learned about Step-by-Step Build 3: The Material 3 Adaptive Scaffolds. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 7 of 11
Step-by-Step Build 4: Foldables and Postures

A foldable can be flat, half-open (like a laptop, book posture) or propped like a stand (tabletop posture). Jetpack WindowManager exposes the fold as a FoldingFeature with:

Property Meaning
state FLAT or HALF_OPENED
orientation HORIZONTAL or VERTICAL fold line
occlusionType Whether the fold hides content (FULL) or not (NONE)
isSeparating Whether the fold divides the display into two logical areas
bounds The fold's rectangle in the window

Detect tabletop posture

kotlin
import androidx.window.layout.FoldingFeature
import androidx.window.layout.WindowInfoTracker
@OptIn(ExperimentalContracts::class)
fun isTableTopPosture(feature: FoldingFeature?): Boolean {
 contract { returns(true) implies (feature != null) }
 return feature?.state == FoldingFeature.State.HALF_OPENED &&
 feature.orientation == FoldingFeature.Orientation.HORIZONTAL
}
class MainActivity : ComponentActivity() {
 private val foldState = MutableStateFlow<FoldingFeature?>(null)
 override fun onCreate(savedInstanceState: Bundle?) {
 super.onCreate(savedInstanceState)
 lifecycleScope.launch {
 lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
 WindowInfoTracker.getOrCreate(this@MainActivity)
 .windowLayoutInfo(this@MainActivity)
 .map { info -> info.displayFeatures.filterIsInstance<FoldingFeature>().firstOrNull() }
 .collect { foldState.value = it }
 }
 }
 setContent {
 val fold by foldState.collectAsStateWithLifecycle()
 AppTheme { VideoCallScreen(tabletop = isTableTopPosture(fold)) }
 }
 }
}

(ExperimentalContracts needs @OptIn(kotlin.contracts.ExperimentalContracts::class); the contract is optional and can be removed.)

Use the posture

In tabletop, show the video or content in the top half, and the controls in the bottom half, so the phone stands on a table and the controls lie flat under your hands:

kotlin
@Composable
fun VideoCallScreen(tabletop: Boolean) {
 if (tabletop) {
 Column(Modifier.fillMaxSize()) {
 VideoArea(Modifier.weight(1f)) // top half
 ControlsArea(Modifier.weight(1f)) // bottom half
 }
 } else {
 Box(Modifier.fillMaxSize()) {
 VideoArea(Modifier.fillMaxSize())
 ControlsArea(Modifier.align(Alignment.BottomCenter))
 }
 }
}

Rules for foldables:

  1. Never place critical content under the hinge if occlusionType == FULL.
  2. Keep state across fold and unfold; handle configuration changes yourself or use ViewModel and rememberSaveable.
  3. Test the book posture too: when the fold is vertical and separating, a two-pane layout with the divider on the fold looks natural.
  4. Do not assume a fold exists; FoldingFeature is null on ordinary devices.
✏️ Your Task
Practise what you just learned about Step-by-Step Build 4: Foldables and Postures. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 8 of 11
Step-by-Step Build 5: Resizability, Input and Quality

Do not lock orientation or resizing

On large screens, a manifest that forbids rotation or resizing makes the app letterboxed (a phone-sized column in the middle of the screen) because the system respects it. For apps targeting recent Android versions, the platform increasingly ignores such restrictions on large screens (the exact thresholds and behaviour changed across Android 16 and later; check the current docs). The robust approach is to avoid them:

xml
<activity
 android:name=".MainActivity"
 android:exported="true"
 android:resizeableActivity="true" />
<!-- Avoid: android:screenOrientation="portrait" and android:resizeableActivity="false" -->

Handle configuration changes properly: let the activity recreate and restore from the ViewModel and saved state, or declare android:configChanges only for cases like video playback where you handle them yourself.

Input beyond touch

Large-screen users have a keyboard, mouse, trackpad and stylus. Support:

  • Keyboard shortcuts and focus order (Tab and arrow keys). Compose provides Modifier.onKeyEvent, Modifier.onPreviewKeyEvent and Modifier.focusable().
  • Hover states and tooltips for mouse users, right-click context menus.
  • Drag and drop, text selection with SelectionContainer.
  • Stylus input for notes and drawing.
kotlin
@Composable
fun KeyboardAwareList(onSearch: () -> Unit, content: @Composable () -> Unit) {
 Box(
 Modifier.onPreviewKeyEvent { event ->
 if (event.type == KeyEventType.KeyDown && event.isCtrlPressed && event.key == Key.F) {
 onSearch()
 true // consumed
 } else false
 }
 ) { content() }
}

Layout rules of thumb for wide windows

  • Do not stretch: cap reading width (about 600 to 840 dp for text) using Modifier.widthIn(max = 840.dp) with centred content.
  • Grow columns, not cards: use LazyVerticalGrid(columns = GridCells.Adaptive(minSize = 180.dp)).
  • Use space for context: show the detail beside the list rather than a bigger list.
  • Reachability: on tablets, place frequent actions near edges where hands rest; avoid tiny controls in the far corners.
kotlin
@Composable
fun StudentGrid(students: List<Student>) {
 LazyVerticalGrid(
 columns = GridCells.Adaptive(minSize = 200.dp),
 contentPadding = PaddingValues(16.dp),
 horizontalArrangement = Arrangement.spacedBy(12.dp),
 verticalArrangement = Arrangement.spacedBy(12.dp)
 ) {
 items(students, key = { it.id }) { StudentCard(it) }
 }
}

Test it

  • Android Studio's resizable emulator (phone, foldable, tablet, desktop in one AVD) lets you resize the window live.
  • Use Foldable and Pixel Tablet AVD definitions; the foldable emulator has virtual hinge sensors in the extended controls.
  • In Compose previews, add @PreviewScreenSizes or @Preview(device = Devices.TABLET) to preview several window sizes at once.
  • Large-screen quality checklists from the Android documentation list the tiers (Tier 3 basic, Tier 2 differentiated, Tier 1 optimised). Check the current wording on the developer site.
kotlin
@PreviewScreenSizes
@Composable
fun RegisterPreview() {
 AppTheme { RegisterScreen(sampleState, twoPane = false, onSelect = {}) }
}

(@PreviewScreenSizes renders several window shapes in the preview pane. For a preview to reflect real window classes, make the screen read the window size inside the composable rather than from a hard-coded parameter.)

Automated tests with a fixed window size

In Compose UI tests, you can test a layout at a known width by wrapping it in a Box(Modifier.width(900.dp)) or by parametrising the twoPane boolean, which is another reason to hoist it:

kotlin
@get:Rule val rule = createComposeRule()
@Test
fun twoPane_showsListAndDetail() {
 rule.setContent { AppTheme { RegisterScreen(sampleState.copy(selectedId = 1), twoPane = true, onSelect = {}) } }
 rule.onNodeWithText("Adaeze Okoye").assertExists()
 rule.onNodeWithText("Strong in Mathematics.").assertExists()
}
@Test
fun singlePane_showsOnlyList() {
 rule.setContent { AppTheme { RegisterScreen(sampleState, twoPane = false, onSelect = {}) } }
 rule.onNodeWithText("Strong in Mathematics.").assertDoesNotExist()
}
✏️ Your Task
Practise what you just learned about Step-by-Step Build 5: Resizability, Input and Quality. 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: Choose the layout

For each window, choose the navigation component and number of panes for a list-detail email app: (a) a phone in portrait, (b) the same phone with its app in a 500 dp split-screen pane, (c) a tablet in landscape, (d) an unfolded foldable at 700 dp width.

(a) Compact: bottom navigation bar, one pane (list, then detail). (b) Still compact (500 dp is below 600 dp), so the same as (a), even if the device is a big tablet. (c) Expanded: navigation rail or drawer, two panes (list plus detail). (d) Medium: navigation rail, and two panes if there is room (often yes at 700 dp with a narrow list); otherwise one. Decisions come from the window, not the device.

🎯 Your Challenge

Exercise 2: Stretched text

A reading screen displays a long paragraph full-width on a 1,200 dp window. Fix it so the text is a comfortable width and centred.

kotlin
@Composable
fun Reader(text: String) {
 Box(Modifier.fillMaxSize(), contentAlignment = Alignment.TopCenter) {
 Text(
 text = text,
 style = MaterialTheme.typography.bodyLarge,
 modifier = Modifier
 .widthIn(max = 720.dp)
 .padding(24.dp)
 .verticalScroll(rememberScrollState())
 )
 }
}

Long lines are hard to read: aim for roughly 45 to 90 characters per line. widthIn(max = ...) leaves small windows alone and caps wide ones.

🎯 Your Challenge

Exercise 3: Selection across a fold

The user selects a student on the phone's cover screen (compact), then unfolds the device (expanded). Describe what should happen and which two design choices make it work.

The unfolded layout should show the list with the same student highlighted and their detail on the right, with no reset. It works because (1) state is hoisted into the ViewModel (or the navigator with a saveable content key), so recreation after the configuration change keeps selectedId, and (2) the layout decision is a pure function of window size, so the same state renders as one pane or two.

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

Mistake #1: Branching on device type. Fix: branch on window size.

Mistake #2: Locking orientation or disabling resizing. Fix: support all window shapes.

Mistake #3: Calling the size-class calculation deep in the tree. Fix: hoist and pass down.

Mistake #4: Stretching phone layouts to tablets. Fix: use two panes, adaptive grids and widthIn.

Mistake #5: Losing state on resize or fold. Fix: ViewModel plus saveable state, ids as keys.

Mistake #6: Content under the hinge. Fix: read FoldingFeature bounds and occlusionType.

Mistake #7: Ignoring keyboard and mouse. Fix: focus order, shortcuts, hover.

Mistake #8: Assuming experimental adaptive APIs never change. Fix: pin versions and read release notes.

Mistake #9: Testing only on one phone. Fix: resizable emulator and preview sizes.

Mistake #10: Forgetting back handling in the one-pane case. Fix: use the scaffold navigator or BackHandler.

✏️ Your Task
Practise what you just learned about Common 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 window width is the boundary between compact and medium?

600 dp (compact is below it; medium runs from 600 dp up to 840 dp).

Question 2. Name the three canonical layouts.

List-detail, supporting pane and feed.

Question 3. Why store selectedId rather than the selected object?

An id is small, saveable and always resolves against the current data, so selection survives recreation without stale copies.

Question 4. What does tabletop posture mean, and how would you design for it?

A foldable half-open with a horizontal fold. Put content in the top half and controls in the bottom half.

Question 5. Why avoid screenOrientation="portrait" and resizeableActivity="false"?

They cause letterboxing or are ignored on large screens, and they block multi-window, so the app feels broken on tablets and desktop-style windows.

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
ClassRegister for Every Screen

Project goal: Make the ClassRegister app adapt to phones, tablets and foldables.

Requirements:

  1. A RegisterViewModel holding students and selectedId (as in Build 2).
  2. A hoisted size class, with a bar on compact, rail on medium and rail or drawer on expanded (via NavigationSuiteScaffold).
  3. A list-detail layout using either the hand-built Row or NavigableListDetailPaneScaffold.
  4. A grid on a separate "Gallery" tab with GridCells.Adaptive.
  5. Text content capped with widthIn.
  6. A tabletop-aware screen for any media or camera feature (a stub is fine).
  7. No orientation locks; resizeableActivity="true".
  8. Two UI tests (single-pane and two-pane) and previews with @PreviewScreenSizes.
  9. Verify on the resizable emulator and write down what you saw at each size.

Stretch goal: add keyboard shortcut Ctrl+F to focus a search field, and a mouse hover highlight on rows.

A compact version of the root:

kotlin
@Composable
fun ClassRegisterApp(vm: RegisterViewModel = viewModel()) {
 val state by vm.uiState.collectAsStateWithLifecycle()
 val info = currentWindowAdaptiveInfo()
 val twoPane = info.windowSizeClass.windowWidthSizeClass != WindowWidthSizeClass.COMPACT
 var tab by rememberSaveable { mutableStateOf(Tab.Register) }
 NavigationSuiteScaffold(
 navigationSuiteItems = {
 Tab.entries.forEach {
 item(
 selected = tab == it,
 onClick = { tab = it },
 icon = { Icon(it.icon, contentDescription = null) },
 label = { Text(it.label) }
 )
 }
 }
 ) {
 when (tab) {
 Tab.Register -> RegisterScreen(state, twoPane, onSelect = vm::select)
 Tab.Gallery -> StudentGrid(state.students)
 }
 }
}

Expected behaviour on the resizable emulator (example notes):

text
Phone (compact)   bottom bar, list, tap student -> detail, back returns to list
Foldable unfolded rail on the left, list + detail side by side, selection kept
Tablet landscape  rail, two panes, gallery shows 4-5 columns
Desktop window    same, text capped at 720 dp and centred

Remember to check each result on a real device where you can; layouts are only proven by looking at them.

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 58 project”.

3

On github.com click New repository, name it android-lesson-58-adaptive-apps-for-phones-tablets-and-folda, 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 58 complete! 🎉

You covered:

  • Lesson Introduction
  • Prerequisite Concepts
  • Conceptual Understanding: Why Adaptive?
  • Step-by-Step Build 1: Dependencies and a Window Size Class
  • Step-by-Step Build 2: A Master-Detail Register, Properly
  • Step-by-Step Build 3: The Material 3 Adaptive Scaffolds
  • Step-by-Step Build 4: Foldables and Postures
  • Step-by-Step Build 5: Resizability, Input and Quality
  • Guided Practice Exercises
  • Common 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