The Lock on Your Activity Stopped Holding
android:screenOrientation="portrait", resizeableActivity="false", a max aspect ratio. On a tablet the system used to honour it and pillarbox the app in a phone-shaped column with black bars down both sides. Ugly, but contained.
That containment is gone. Google's Android 16 behavior changes are blunt about it: "For apps targeting Android 16 (API level 36), orientation, resizability, and aspect ratio restrictions no longer apply on displays with smallest width >= 600dp. Apps fill the entire display window, regardless of aspect ratio or a user's preferred orientation, and pillarboxing isn't used."
Read that as an engineering fact, not a design suggestion. The day you bump targetSdk to 36, every layout you only ever tested at 411dp wide gets stretched across a foldable's inner display, a 13-inch tablet in landscape, and a desktop window the user can drag to any size. A Column of full-width buttons becomes a column of 1,200dp-wide buttons. A bottom sheet becomes a letterbox. A camera preview locked to portrait rotates with the device.
There is an escape hatch, and it has an expiry date. The manifest property PROPERTY_COMPAT_ALLOW_RESTRICTED_RESIZABILITY opts an app out for now, but the same page warns that "the opt-out is temporary and won't apply when targeting API level 37." Google has already published the rest of the schedule: Android 17 removes the opt-out, and "for Google Play, new apps and updates will be required to target API level 37, making this behavior mandatory for distribution in August 2027." Games declared through android:appCategory and screens narrower than 600dp are exempt. Everything else gets stretched.Why Google Is Forcing the Issue
| Signal | What Google publishes | Source |
|---|---|---|
| Installed base | "More than 300 million Android large screen devices" in use | Get started with adaptive apps |
| Updated installed base | "over 580 million large screen devices in the hands of users" | Android Developers Blog, May 2026 |
| Revenue | "Multi-device users spend 9x more on average than phone only users" | Android Developers Blog, May 2026 |
| Ranking | "Play ranks apps and games optimized for large screens higher than unoptimized apps" | Get started with adaptive apps |
| Store listing | Apps below the bar "display a warning on the app details page" | Get started with adaptive apps |
Window Size Classes: Branch on Space, Never on Device
isTablet() helpers that read the device model, the density bucket, or Configuration.orientation all answer "what hardware is this?" when the layout needs to know "how much space do I have right now?" A foldable is one device with two answers. A tablet in split screen is a large device with a phone-sized window. A desktop window changes answer every time the user drags an edge.
Window size classes are Google's answer: a small set of breakpoints computed from the current window, not the device. The breakpoints are opinionated on purpose:
| Width class | Range | Google's characterisation |
|---|---|---|
| Compact | width < 600dp | 99.96% of phones in portrait |
| Medium | 600dp to 840dp | 93.73% of tablets in portrait, most large unfolded inner displays in portrait |
| Expanded | 840dp to 1200dp | 97.22% of tablets in landscape |
| Large | 1200dp to 1600dp | Large tablet displays |
| Extra-large | 1600dp and up | Desktop displays |
currentWindowAdaptiveInfo() from androidx.compose.material3.adaptive, which recomposes whenever the window crosses a boundary. The Large and Extra-large classes are opt-in: the docs say to pass supportLargeAndXLargeWidth = true. The old windowWidthSizeClass enum is deprecated in favour of isWidthAtLeastBreakpoint() against the WIDTH_DP_*_LOWER_BOUND constants, which is also the safer shape: "at least medium" stays correct when a new, larger class is added above it, where an exhaustive when over an enum would silently fall through.
Hoist the class once, near the root, and pass decisions down. A leaf composable should receive "show the detail pane beside the list", not a window size class it has to interpret.@Composable
fun AppRoot() {
val sizeClass = currentWindowAdaptiveInfo(supportLargeAndXLargeWidth = true).windowSizeClass
// Ask "how much room do I have?", never "is this a tablet?"
val layout = when {
sizeClass.isWidthAtLeastBreakpoint(WindowSizeClass.WIDTH_DP_EXPANDED_LOWER_BOUND) ->
AppLayout.TwoPane
sizeClass.isWidthAtLeastBreakpoint(WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND) ->
AppLayout.ListWithRail
else -> AppLayout.SinglePane
}
InboxScreen(layout = layout)
}
enum class AppLayout { SinglePane, ListWithRail, TwoPane }Canonical Layouts: Stop Inventing a Tablet UI
| Canonical layout | Use it for | Compose API |
|---|---|---|
| List-detail | Inbox, settings, catalogue, chat list | ListDetailPaneScaffold, NavigableListDetailPaneScaffold |
| Supporting pane | Document plus comments, video plus related | SupportingPaneScaffold |
| Feed | Equivalent items in a grid | Lazy grids with adaptive columns |
NavigableListDetailPaneScaffold "wraps ListDetailPaneScaffold and adds built-in navigation and predictive back animations." On a compact window it behaves like two screens with a back gesture between them; on an expanded window it shows both panes; the navigator decides, so your code never branches on width at all.
Navigation chrome follows the same rule. NavigationSuiteScaffold switches navigation UI by window size class, a bottom bar on a compact window and a rail on a wider one, "including dynamically changing the UI during runtime window size changes." A bottom navigation bar stretched across a landscape tablet is the single most recognisable sign of an app that was never adapted; this removes it in about ten lines.@Composable
fun InboxListDetail(messages: List<Message>) {
val navigator = rememberListDetailPaneScaffoldNavigator<Long>()
val scope = rememberCoroutineScope()
NavigableListDetailPaneScaffold(
navigator = navigator,
listPane = {
AnimatedPane {
MessageList(
messages = messages,
onOpen = { id ->
scope.launch {
navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, id)
}
},
)
}
},
detailPane = {
AnimatedPane {
navigator.currentDestination?.contentKey
?.let { id -> MessageDetail(messageId = id) }
?: EmptyDetailPlaceholder()
}
},
)
}State Has to Survive the Fold
remember does not.
The failure is easy to recognise once you have seen it: a user selects a message on the outer display, unfolds the phone, and the detail pane opens empty because the selection lived in a remember that was thrown away. A video restarts from zero. A half-written reply vanishes.
Google's guidance is one sentence long and worth enforcing in review: "Use ViewModel for data and business logic, and use rememberSaveable for UI-level state." Selection, scroll position, expanded or collapsed panes, draft text: rememberSaveable, or state in a ViewModel. The list-detail navigator above already saves its own destination, which is one more reason to use it rather than hand-rolling a selectedId variable.
The cheapest test you can add is a recreation test. Compose's StateRestorationTester emulates a save-and-restore cycle without recreating the Activity, so "the selection survives a configuration change" becomes an assertion in CI instead of a bug report from a foldable owner.@Composable
fun ReplyComposer(threadId: Long) {
// Survives rotation, fold/unfold and window resize.
var draft by rememberSaveable(threadId) { mutableStateOf("") }
var attachmentsExpanded by rememberSaveable { mutableStateOf(false) }
OutlinedTextField(value = draft, onValueChange = { draft = it })
AttachmentTray(expanded = attachmentsExpanded, onToggle = { attachmentsExpanded = !attachmentsExpanded })
}
@Test
fun draftSurvivesRecreation() {
val tester = StateRestorationTester(composeTestRule)
tester.setContent { ReplyComposer(threadId = 7) }
composeTestRule.onNode(hasSetTextAction()).performTextInput("On my way")
tester.emulateSavedInstanceStateRestore()
composeTestRule.onNode(hasSetTextAction()).assertTextEquals("On my way")
}A Migration Order That Actually Ships
screenOrientation, resizeableActivity="false" and aspect-ratio caps, then run on a resizable emulator and a foldable profile. Fix what breaks: usually hard-coded widths, fillMaxWidth() on content that should cap its width, and camera or media code that assumed portrait.
2. Adopt NavigationSuiteScaffold. One change, every screen benefits, and the stretched bottom bar disappears.
3. Move the primary flow to list-detail. Pick the screen users spend the most time in. It is almost always a list.
4. Make state configuration-proof. Audit every remember on those screens; add a StateRestorationTester case for each piece of user-entered state.
5. Then input. Keyboard navigation, focus order and pointer hover are what separate Tier 2 from Tier 3, and they are easier once the layout is stable.
The calendar is not negotiable. Google Play's target API requirements move new apps and updates to API 36 on August 31, 2026, with the temporary opt-out still available; API 37 removes it, and Play requires API 37 in August 2027. That is the whole runway. Spend it on steps 1 to 3 now, while the opt-out is a safety net rather than the only thing standing between your manifest and a stretched layout.Key Takeaways
- 1For apps targeting API 36, Android ignores orientation, resizability and aspect-ratio restrictions on displays 600dp and wider; the temporary opt-out disappears at API 37.
- 2Google Play ranks large-screen-optimized apps higher and shows a warning on listings that miss its large-screen quality bar.
- 3Branch on window size classes from currentWindowAdaptiveInfo(), never on device model, density or orientation.
- 4Use isWidthAtLeastBreakpoint() with the WIDTH_DP_*_LOWER_BOUND constants; the windowWidthSizeClass enum is deprecated.
- 5Reach for the canonical layouts first: NavigableListDetailPaneScaffold and NavigationSuiteScaffold cover most screens without bespoke tablet design.
- 6Fold, unfold and resize are configuration changes: keep UI state in rememberSaveable or a ViewModel, and assert it with StateRestorationTester.
Frequently Asked
Does Android 16 ignore screenOrientation for every app?
No. It applies to apps targeting API 36 on displays whose smallest width is at least 600dp. Games declared with android:appCategory and smaller screens are exempt, and a temporary opt-out exists until API 37.
Should I check whether the device is a tablet?
No. Read the window size class from currentWindowAdaptiveInfo(). A foldable, a split-screen tablet and a resizable desktop window all change the available space without changing the device.
What is the fastest route to Google’s Tier 3 “Adaptive ready”?
Remove orientation and resizability locks, fix what breaks on a resizable emulator, and keep UI state in rememberSaveable or a ViewModel so a fold or resize does not lose it.
Ready to architect your next Android app?
ANDROID-ARCHITECT generates production-ready Kotlin code, architecture blueprints, and CI/CD configurations from plain-language descriptions. Start building for free.