Adapting to phones, foldables and tablets
Mandi Bhav has been designed on one phone held upright. Rotate that phone, open the app on a foldable, put it in split screen on a tablet, or run it on a Chromebook, and you get one column of cards stretched across a wide window: long empty stretches between "Min" and "Max", and only two cards visible where five would fit. This lesson teaches the app to ask how much room it has and use it, with the API Google currently recommends, and verifies it on the emulator at three sizes.
You can no longer lock the orientation
The old escape was android:screenOrientation="portrait" in the manifest. For apps targeting
Android 16 (API 36), as Mandi Bhav does, Android ignores orientation, resizability and aspect-ratio
restrictions on displays at least 600 dp wide in their smallest dimension: tablets and unfolded
foldables. The app fills the window whatever the manifest says, and Google has said the temporary
opt-out goes away for apps targeting API 37. So the app has to work at every size, and it might as
well look good at them.
Window size classes
A window size class sorts the space your app has into a few buckets, separately for width and height. These are the breakpoints from the Android documentation:
| Width class | Width | Typical case |
|---|---|---|
| Compact | under 600 dp | A phone in portrait |
| Medium | 600 dp to under 840 dp | A tablet in portrait, an unfolded foldable in portrait |
| Expanded | 840 dp to under 1200 dp | A tablet in landscape, an unfolded foldable in landscape |
| Large | 1200 dp to under 1600 dp | A large tablet |
| Extra-large | 1600 dp and over | A desktop display |
| Height class | Height | Typical case |
|---|---|---|
| Compact | under 480 dp | A phone in landscape |
| Medium | 480 dp to under 900 dp | A phone in portrait, a tablet in landscape |
| Expanded | 900 dp and over | A tablet in portrait |
It measures the window, not the device. A tablet in split screen can give your app a compact window, and an unfolded phone can give it a medium one. Code that asks "is this a tablet?" gets split screen, desktop windows and foldables wrong. Ask how wide the window is instead.
Adding the library
Window size classes for Compose come from Material 3's adaptive library,
androidx.compose.material3.adaptive:adaptive. The Compose BOM in this project manages its
version (1.3.0 for BOM 2026.09.00), so the catalog entry has no version. In
gradle/libs.versions.toml, under [libraries]:
androidx-compose-material3-adaptive = { group = "androidx.compose.material3.adaptive", name = "adaptive" }
and in app/build.gradle.kts:
implementation(libs.androidx.compose.material3.adaptive)
It brings the WindowSizeClass type with it, from androidx.window.core.layout.
The documentation is a version behind
The Android guide to window size classes, read on 8 November 2026, shows this:
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
It compiles, and the compiler warns:
w: PricesScreen.kt:45:40 'fun currentWindowAdaptiveInfo(supportLargeAndXLargeWidth: Boolean = ...): WindowAdaptiveInfo' is deprecated. Please use V2 version of this function to support L and XL width size classes.
In adaptive 1.3 the recommended call is currentWindowAdaptiveInfoV2(), which also reports the
large and extra-large width classes. Two lessons in that. The documentation can lag the
library: when a warning disagrees with a guide, the warning is about the code you are actually
compiling. And read your build's warnings: this one was invisible in a build run with
./gradlew assembleDebug -q, because -q hides warnings as well as noise. It only appeared in a
normal build.
Two columns when there is room
PricesScreen takes the size class as a parameter with a default, so the decision is made in one
place and a preview or a test can supply any size it likes:
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PricesScreen(
prices: List<CommodityPrice>,
modifier: Modifier = Modifier,
windowSizeClass: WindowSizeClass = currentWindowAdaptiveInfoV2().windowSizeClass,
) {
// Module 5 moves this state into a ViewModel; until then it is lost on rotation.
var query by remember { mutableStateOf("") }
var selectedCommodity by remember { mutableStateOf<String?>(null) }
val commodities = commoditiesIn(prices)
val visible = filterPrices(prices, query, selectedCommodity)
val columns = if (
windowSizeClass.isWidthAtLeastBreakpoint(WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND)
) 2 else 1
The Scaffold, search field and chips stay as they were. Replace the loop that emitted one
PriceCard per price with rows of columns cards:
for (rowOfPrices in visible.chunked(columns)) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(IntrinsicSize.Min),
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
for (price in rowOfPrices) {
PriceCard(
price = price,
modifier = Modifier
.weight(1f)
.fillMaxHeight(),
)
}
// An odd last row keeps its card at column width.
repeat(columns - rowOfPrices.size) {
Spacer(modifier = Modifier.weight(1f))
}
}
}
New imports: currentWindowAdaptiveInfoV2 from androidx.compose.material3.adaptive,
WindowSizeClass from androidx.window.core.layout, and IntrinsicSize, Row, Spacer,
fillMaxHeight and height from androidx.compose.foundation.layout.
Line by line:
isWidthAtLeastBreakpoint(WIDTH_DP_MEDIUM_LOWER_BOUND)is true from 600 dp up. Compare against the named constant, never against a number of your own, so the app follows the platform if the breakpoints are ever revised.chunked(columns)turns nine prices into nine lists of one, or five lists of two with one left over. One column is the same code path as two, which keeps the phone layout from rotting.weight(1f)on each card shares the row's width equally (lesson 1).height(IntrinsicSize.Min)withfillMaxHeight()makes the two cards in a row the same height. Without it each card is as tall as its own content, and a market name that wraps onto two lines makes one card taller than its neighbour.IntrinsicSize.Minasks the children how tall they need to be before measuring them, sets the row to the tallest answer, andfillMaxHeight()stretches the other card to match. What it costs: an extra query pass over the children, nothing for two cards, and a reason not to use intrinsics across a whole long list.- The
Spacerwithweight(1f)fills the empty half of an odd last row, so the ninth card is a column wide, not stretched across the screen.
Two previews make both layouts visible in Android Studio. The wide one passes its size class explicitly, so it does not depend on how the preview tool reports its window:
@Preview(name = "Unfolded, 700 dp", showBackground = true, widthDp = 700, heightDp = 840)
@Composable
private fun PricesScreenWidePreview() {
MandiBhavTheme {
// Passed explicitly, so the preview does not depend on how the
// preview tool reports its window size.
PricesScreen(
prices = SamplePrices.all,
windowSizeClass = WindowSizeClass(minWidthDp = 700, minHeightDp = 840),
)
}
}
Why not a grid component? LazyVerticalGrid exists and is what a long list of prices will use.
It is lazy, like LazyColumn, and module 6 introduces both together. For nine cards inside a
scrolling Column, chunked and a Row are enough, and they use nothing new.
Checking it on the emulator
The emulator in this course is a 1080 × 2400 phone at 420 dpi: about 411 dp wide, compact.
Rotate it with the rotate buttons in the emulator's toolbar. In landscape the window is about 914 dp wide, expanded width, and the app showed two columns of cards, with the search field and chips across the full width above them, and the content kept clear of the camera cutout on the left. Rotating also recreates the activity, so a search you had typed is gone; module 5 fixes that.
Resize it to the shape of an unfolded foldable from a terminal:
adb shell wm size 1840x2208
Physical size: 1080x2400
Override size: 1840x2208
At 420 dpi that is about 701 dp wide, medium width. Launched like that, Mandi Bhav showed the chips on one line and the cards in two equal columns, every pair the same height, with Tur dal alone on the last row at column width and empty space beside it. Put the emulator back afterwards:
adb shell wm size reset
The landscape phone deserves a second look. It is expanded width but compact height: about 411 dp tall, of which the status bar, app bar, search field and chips take most. Two columns showed barely one row of cards. Mandi Bhav decides on width alone, as most apps should, but a screen with a tall header is where the height class earns a check too.
Check your work
Why screenOrientation="portrait" is no longer a fix. For apps targeting API 36, Android
ignores it on displays at least 600 dp in their smallest width.
What a window size class measures. The app's window, not the device's screen.
The width at which Mandi Bhav switches to two columns. 600 dp, the start of the medium class.
Why currentWindowAdaptiveInfoV2() and not the function in the guide. The original is
deprecated in adaptive 1.3; the V2 version also reports the large and extra-large classes.
What IntrinsicSize.Min with fillMaxHeight() does here. Makes both cards in a row as tall as
the taller one.
Why the size class is a parameter. So previews and tests can supply any size, and the decision is made in one place.
Practice 5. One answer:
fun columnsFor(windowSizeClass: WindowSizeClass): Int = when {
windowSizeClass.isWidthAtLeastBreakpoint(WindowSizeClass.WIDTH_DP_EXPANDED_LOWER_BOUND) -> 3
windowSizeClass.isWidthAtLeastBreakpoint(WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND) -> 2
else -> 1
}
Check the larger breakpoint first, or every expanded window matches the medium branch. A unit test
can now cover it with WindowSizeClass(minWidthDp = 914, minHeightDp = 411) and get 3, and with
411 dp wide get 1; the constants are 600 and 840. Whether three columns of cards are better in
landscape is a design call: on a landscape phone each card is about 270 dp wide, enough for the
content, but even fewer rows show.
Practice
- Add the dependency, the parameter, the
columnscalculation and the rows. Run it on the phone, then rotate it. - Remove
.height(IntrinsicSize.Min)and.fillMaxHeight(). Find a pair of cards whose heights now differ (change a market name to something long inSamplePricesif none do), then put them back. - Resize the emulator with
adb shell wm size 1840x2208, launch the app, look, andadb shell wm size reset. - Put the app in split screen with another app on the rotated emulator and drag the divider. Watch the layout switch between one and two columns as the window crosses 600 dp.
- Harder: move the decision into
columnsFor(windowSizeClass: WindowSizeClass): Intthat returns 3 from the expanded breakpoint, 2 from medium and 1 otherwise. Write a unit test for it, then decide whether Mandi Bhav should ship three columns. - Break it on purpose: change
currentWindowAdaptiveInfoV2()tocurrentWindowAdaptiveInfo(), build without-q, and find the warning. Then build with-qand confirm it is gone. Put V2 back.
Official documentation
- Use window size classes — The breakpoints, the device percentages behind them, and passing the class down.
- Support different display sizes — Making layout decisions from available space rather than device type.
- Android 16 behaviour changes — Why orientation and resizability locks are ignored on large screens.
- Adaptive app quality guidelines — The checklist Google uses for apps on tablets, foldables and Chromebooks.
- Intrinsic measurements — How
IntrinsicSize.MinandMaxwork, and what they cost.
Next: accessibility, so the same screen works with TalkBack, large touch targets and large fonts.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Selected students who finish a course can join our internship, with real-world projects and code review from a working engineer.
About the internship