Skip to content
All posts

Edge-to-edge and predictive back on Android in 2026: what's mandatory now, and how not to break your UI

Edge-to-edge is now enforced on API 35+ and predictive back is the default gesture. A practical guide to Compose insets, WindowInsets, and PredictiveBackHandler.

MFKAPPS 5 min read

Two Android UI changes stopped being optional this cycle. Apps targeting SDK 35+ get edge-to-edge forced on — you can no longer opt out and let the system draw opaque bars for you. And predictive back, the swipe gesture that previews where you’re headed before you commit to it, is now the default interaction on gesture-nav devices instead of an experimental flag. Neither is a redesign. Both will visibly break a UI that wasn’t built for them, and the failure mode is the same for each: it works fine in the emulator you tested on and looks broken on a real phone with a notch or three-button nav turned off.

Here’s what actually changed, and the Compose code that keeps five apps’ worth of screens correct under both.

Edge-to-edge is no longer a choice

Before API 35, enableEdgeToEdge() was something you opted into for a specific screen — a nice-to-have for apps that wanted content to flow under a translucent status bar. Targeting API 35 removes the choice: Window.setDecorFitsSystemWindows(false) behavior is forced, system bars go transparent, and your root content draws behind them whether you called anything or not.

The practical effect: any layout that assumed the status bar and navigation bar reserved their own space now has content starting at physical pixel zero. A top app bar’s title sits under the clock. A bottom nav bar’s icons sit under the gesture pill. This isn’t a visual polish issue — on a real device it means users can’t tap the bottom row of your UI.

The fix isn’t “add padding everywhere.” It’s applying WindowInsets at the right boundary, once, so the system bars reserve space exactly where content needs it and nowhere else.

setContent {
    AppTheme {
        Scaffold(
            contentWindowInsets = WindowInsets.safeDrawing,
            topBar = { TopAppBar(title = { Text("Stocky") }) },
        ) { padding ->
            LazyColumn(contentPadding = padding) {
                items(pantryItems) { PantryRow(it) }
            }
        }
    }
}

Scaffold’s contentWindowInsets already accounts for the top bar and bottom bar it’s given, so most screens need nothing beyond wiring that one parameter through. The trap is doing this twice: Scaffold applies safe-drawing insets to its padding value, and if a child also calls .systemBarsPadding() on top of that, you get double the gap under the status bar — a blank strip that looks like a bug in the design, not the insets code.

Where insets need to be handled by hand

Not every screen goes through Scaffold. Three spots I’ve had to fix by hand across Granyn, Stocky, and Mintly:

  • Bottom sheets and dialogs. A ModalBottomSheet draws its own surface outside the parent Scaffold’s padding, so its content needs its own .navigationBarsPadding() on the innermost column, or the last row sits under the gesture bar.
  • The keyboard. WindowInsets.ime is a separate inset from navigationBars, and it animates as the keyboard opens and closes. A text field pinned to the bottom of a screen needs .imePadding(), not .navigationBarsPadding() — the two overlap when the keyboard is closed and diverge the moment it opens.
  • Full-bleed images or headers. When you deliberately want content to draw under the status bar — a screenshot gallery, a hero image — apply insets to the interactive controls layered on top (back button, share icon) with .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Top)) instead of padding the whole container, or you lose the effect you wanted.

The one habit that catches most of this before a user does: turn on gesture navigation and “Demo mode” status bar customization in the emulator, and check every screen with a notch-simulating cutout enabled. Three-button nav and a plain status bar hide almost every insets bug you’ll ship.

Predictive back: from a hard cut to a preview

The other change is behavioral, not layout. Predictive back lets the system show where a back gesture will land — a shrinking preview of the underlying screen — before the user lifts their finger, so they can see the destination and abort mid-swipe. Apps that haven’t opted in still get a plain, instant screen swap; the gesture works, it just looks like a jump cut next to every other predictive-back-aware app on the device.

Opting in is a manifest flag plus a Compose callback:

<!-- AndroidManifest.xml -->
<application android:enableOnBackInvokedCallback="true">
PredictiveBackHandler(enabled = showDetail) { progress ->
    try {
        progress.collect { backEvent ->
            // backEvent.progress: 0f (start) → 1f (committed)
            scale = 1f - (backEvent.progress * 0.1f)
        }
        showDetail = false // gesture committed
    } catch (e: CancellationException) {
        scale = 1f // gesture aborted — snap back
    }
}

PredictiveBackHandler (from androidx.activity.compose) hands you a Flow of progress events instead of a single callback, which is the part that trips people up coming from BackHandler. You collect it for the duration of the gesture, drive an animation off backEvent.progress, and the CancellationException on an aborted swipe is the normal exit path, not an error — that’s how the system tells you the user let go before committing. I used exactly this for Mintly’s session-detail screen: the timer view now scales down slightly and reveals the list behind it as you swipe back, and snaps cleanly to full size if you release early instead of the flow just vanishing.

What to actually check before shipping

  • Every screen renders correctly with gesture nav on and three-button nav off — that’s the mode most new devices ship in.
  • No double-padded gaps under the status bar (the Scaffold + manual .systemBarsPadding() trap above).
  • Bottom sheets and snackbars clear the gesture bar on a device with no hardware nav buttons.
  • Text fields near the bottom of a screen use .imePadding() and don’t jump when the keyboard opens.
  • At least one screen with real navigation (not just the first one you test) has predictive back wired up, so the gesture doesn’t feel inconsistent between screens.

None of this is hard once you’ve hit it once. It’s just invisible in a stock emulator with three-button navigation, which is exactly the setup most of us leave running by default — and exactly why it ships broken more often than it should.