Insets handling tips for Android 15’s edge-to-edge enforcement

Ash Nohe
Android Developers
Published in
13 min readSep 3, 2024

--

This blog post is part of our series: Spotlight Week on Android 15, where we provide resources — blog posts, videos, sample code, and more — all designed to help you prepare your apps and take advantage of the latest features in Android 15. You can read more in the overview of Spotlight Week: Android 15 here, which will be updated throughout the week.

Users prefer edge-to-edge over non edge-to-edge screens for both gesture navigation and three button navigation, according to an internal Google user study.

Figure 1. Top or Left: an edge-to-edge app. The app’s background draws under the status bar at the top and the navigation bar at the bottom. Bottom or Right: an app that is not edge-to-edge. The app’s background and content avoid the status bar and navigation bar.

Android 15 enforces edge-to-edge

Before target SDK 35 (Android 15), your app does not draw edge-to-edge without explicit code changes to intentionally go edge-to-edge. After setting targetSdk=35 or higher, the system will draw your app edge-to-edge by default on Android 15 and later devices. While this change can make it easier for many apps to go edge-to-edge, critical UI elements may also be inaccessible to users. Your app must handle insets to ensure critical UI elements remain accessible.

Figure 2. App targeting SDK 34 on an Android 15 device. The app is not edge-to-edge.
Figure 3. App targeting SDK 35 on an Android 15 device. The app is edge-to-edge but insets are not handled. The status bar, navigation bar, and the display cutout obscure UI.
Figure 4. App targeting SDK 35 on an Android 15 device. The app is edge-to-edge and insets are handled so that critical UI isn’t obscured.

If your app is already edge-to-edge or in immersive mode, you’re unaffected. However, it’s still worth testing your app for breakages.

Insets handling tips

There are various APIs and attributes you can use to handle insets to avoid the System UI and display cutout.

This article explains the following tips that are applicable to Compose and Views:

  1. Use material components to make handling insets easier
  2. Draw backgrounds edge-to-edge, and inset critical UI
  3. Handle display cutout and caption bar insets
  4. Don’t forget the last list item
  5. Don’t forget IMEs
  6. For backwards compatibility, use enableEdgeToEdge instead of setDecorFitsSystemWindows
  7. Background protect system bars only when necessary

It also provides these tips that are specific to Compose:

8. Use Scaffold’s PaddingValues

9. Use high level WindowInset APIs

Finally, it includes these tips specific to Views:

10. Prefer ViewCompat.setOnApplyWindowInsetsListener over fitsSystemWindows=true

11. Apply insets based on app bar height on bar layout

1. Use material components to make handling insets easier

Many material components automatically handle insets, meaning the component’s background draws inside the system bar region and pads critical UI (see tip #2). Some material components do not automatically handle insets, but instead provide methods to make handling insets easier.

Material 3 Components (androidx.compose.material3) that automatically handle insets:

  • BottomAppBar
  • CenterAlignedTopAppBar
  • DismissibleDrawerSheet
  • LargeTopAppBar
  • MediumTopAppBar
  • ModalBottomSheet
  • ModalDrawerSheet
  • NavigationBar
  • NavigationRail
  • NavigationSuiteScaffold
  • PermanentDrawerSheet
  • TopAppBar
  • SmallTopAppBar

Material 2 Components (androidx.compose.material) don’t automatically handle insets themselves by default, unlike Material 3 components. Consider updating to Material 3 Components. Otherwise, many Material 2 components support specifying the window insets to apply with the windowInsets parameter, like the BottomAppBar, TopAppBar, BottomNavigation, and NavigationRail. Likewise, use the contentWindowInsets parameter for Scaffold. Otherwise, apply the insets as padding.

Material View Components (com.google.android.material) that automatically handle insets:

  • BottomAppBar
  • BottomNavigationView
  • NavigationRailView
  • NavigationView

2. Draw backgrounds edge-to-edge, and inset critical UI.

Oftentimes, apps will set the system bars to the same color as the app bars to appear edge-to-edge, which is an anti-pattern.

Figure 5. App targeting SDK 34 on an Android 15 device. The app has set the status bar and navigation bar colors, which is no longer supported after targeting SDK 35. The status and navigation bar color are intentionally set to a slightly different shade so you can distinguish the system bars from the app bars, but usually apps will use the same colors for the system and app bars to appear edge-to-edge.

Apps using this anti-pattern do not appear edge-to-edge all the time, especially on large screen devices.

To improve this app’s UX after edge-to-edge is enforced, extend the background of the app bars so they draw underneath the transparent system bars, then inset the text and buttons to avoid the system bars. Material 3 components TopAppBars and BottomAppBars do this automatically. For Material 2 components, use the windowInsets parameter. For Views, some Material components handle this automatically. Otherwise, you’ll use fitsSystemWindows or ViewCompat.setOnApplyWindowInsetsListener.

Figure 6. Edge-to-edge enforced. Left: Insets not handled. Right: Insets handled. App bars draw under transparent system bars and text and icons avoid the system bars.

3. Handle display cutout and caption bar insets

While handling insets, account for the display cutouts and caption bars in addition to the status and navigation bars.

Display cutouts

A display cutout often contains the camera. After targeting SDK 35, your app might have important UI under the display cutout, especially when the cutout is on the left or right edges of the device.

For example, prior to targeting SDK 35, your app might have a large white box to account for a camera cutout in landscape.

Figure 7. App targeting SDK 34 on an Android 15 device.

After targeting SDK 35, the white box disappears and all content moves to the left. Some content draws underneath the camera cutout.

Figure 8. App targeting SDK 35 on an Android 15 device. The camera hides Sheep’s photo.

Handling display cutouts in Compose

In Compose, easily handle display cutouts using WindowInsets.safeContent, WindowInsets.safeDrawing, or WindowInsets.safeGestures. Or, use WindowInsets.displayCutout for fine-grained control. Unfortunately, at the time of writing this blog, Scaffold’s PaddingValues does not yet account for the display cutout.

// Shows using WindowInsets.displayCutout for fine-grained control.
// But, it's often sufficient and easier to instead use
// WindowInsets.safeContent, WindowInsets.safeDrawing, or WindowInsets.safeGestures
@Composable
fun ChatRow(...) {
val layoutDirection = LocalLayoutDirection.current
val displayCutout = WindowInsets.displayCutout.asPaddingValues()
val startPadding = displayCutout.calculateStartPadding(layoutDirection)
val endPadding = displayCutout.calculateEndPadding(layoutDirection)
Row(
modifier = modifier.padding(
PaddingValues(
top = 16.dp,
bottom = 16.dp,
// Ensure content is not hidden by display cutouts
// when rotating the device.
start = startPadding.coerceAtLeast(16.dp),
end = endPadding.coerceAtLeast(16.dp)
)
),
...
) { ... }

After handling display cutouts, the app looks like this:

Figure 9. App targeting SDK 35 on an Android 15 device. Display cutout insets handled. Note: ideally “Chats” should also be padded to the right.

You can test various display cutout configurations on the Developer options screen under Display cutout.

Learn more about this example in the codelab, Handle edge-to-edge enforcements in Android 15.

Handling display cutouts in Views

In Views, handle display cutouts using WindowInsetsCompat.Type.displayCutout. See the documentation for a code sample.

Note: If your app has a non-floating window (for example, an Activity) that is using LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT, LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER or LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES, Android will interpret these cutout modes to be LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS starting in Android 15, causing your window to draw into the display cutout region. Ensure to test your screens that use DEFAULT, NEVER, or SHORT_EDGES.

Caption bars

When your app appears in a desktop emulator or in a free form window, it has a caption bar instead of a status bar.

Figure 10. The caption bar in a desktop emulator.

While handling insets in Compose, use either Scaffold’s PaddingValues, WindowInsets.safeContent, WindowInsets.safeDrawing, or WindowInsets.systemBars, which all include the caption bar. Don’t use WindowInsets.statusBars.

In your app’s View-based layouts, use WindowInsetsCompat.Type.systemBars. Don’t use WindowInsetsCompat.Type.statusBars.

4. Don’t forget the last list item

After targeting SDK 35, the last item of your lists might be inaccessible, especially if it’s behind three-button navigation or the taskbar.

Handling the last list item in Compose

To handle the last list item in Compose, use LazyColumn’s contentPadding to add a space to the last item unless you are using TextField:

Scaffold { innerPadding ->
LazyColumn(
contentPadding = innerPadding
) {
// Content that does not contain TextField
}
}

For TextField, use a Spacer to draw the last TextField in a LazyColumn. For an in-depth overview of handling insets with lists, see Inset consumption.

LazyColumn(
Modifier.imePadding()
) {
// Content with TextField
item {
Spacer(
Modifier.windowInsetsBottomHeight(
WindowInsets.systemBars
)
)
}
}

Handling the last list item in Views

Adding clipToPadding=”false” can ensure the last list item appears above the navigation bar for RecyclerView or NestedScrollView. Take this app for example. After targeting SDK 35, the app’s list UI shows the last list item underneath the navigation bar.

Figure 11. App with Recycler View. Left: targeting SDK 34. Last list item appears above three-button navigation. Right: targeting SDK 35. Last list item draws underneath three-button navigation.

We can use the window inset listener so that all list items, including the last list item, are padded above the navigation bar.

Figure 12. App has handled insets, but feels less immersive because content does not scroll behind system bars.
// Figure 12
ViewCompat.setOnApplyWindowInsetsListener(
findViewById(R.id.recycler_view)
) { v, insets ->
val innerPadding = insets.getInsets(
// Notice we're using systemBars, not statusBar
WindowInsetsCompat.Type.systemBars()
// Notice we're also accounting for the display cutouts
or WindowInsetsCompat.Type.displayCutout()
// If using EditText, also add
// "or WindowInsetsCompat.Type.ime()"
// to maintain focus when opening the IME
)
v.setPadding(
innerPadding.left,
innerPadding.top,
innerPadding.right,
innerPadding.bottom)
insets
}

However, now the app looks less immersive. To get the result we want, add clipToPadding=false to ensure the last list item sits above the navigation bar and the list is visible while scrolling behind the navigation bar (and status bar).

Figure 13. App displays edge-to-edge and the last list item is fully visible. This is the result we want.
<!-- Figure 13 -->
<RecyclerView
...
android:clipToPadding="false" />

5. Don’t forget IMEs

Set android:windowSoftInputMode=”adjustResize” in your Activity’s AndroidManifest.xml entry to make room for the IME (or soft keyboard) on screen.

Handling IMEs insets in Compose

Account for the IME using Modifier.imePadding(). For example, this can help maintain focus on a TextField in a LazyColumn when the IME opens. See the Inset consumption section for a code example and explanation.

Handling IMEs insets in Views

Before targeting SDK 35, using android:windowSoftInputMode=”adjustResize” was all you needed to maintain focus on — for example — an EditText in a RecyclerView when opening an IME. With “adjustResize”, the framework treated the IME as the system window, and the window’s root views were padded so content avoids the system window.

After targeting SDK 35, you must also account for the IME using ViewCompat.setOnApplyWindowInsetsListener and WindowInsetsCompat.Type.ime() because the framework will not pad the window’s root views. See Figure 12's code example.

6. For backward compatibility, use enableEdgeToEdge instead of setDecorFitsSystemWindows

After your app has handled insets, make your app edge-to-edge on previous Android versions. For this, use enableEdgeToEdge instead of setDecorFitsSystemWindows. The enableEdgeToEdge method encapsulates the about 100 lines of code you need to be truly backward compatible.

7. Background protect system bars only when necessary

In many cases, keep the new Android 15 defaults. The status bar and gesture navigation bar should be transparent, and three button navigation translucent after targeting SDK 35 (see Figure 1).

However, there are some cases where you wish to preserve the background color of the system bars, but the APIs to set the status and navigation bar colors are deprecated. We are planning to release an AndroidX library to support this use case. In the meantime, if your app must offer custom background protection to 3-button navigation or the status bar, you can place a composable or view behind the system bar using WindowInsets.Type#tappableElement() to get the 3-button navigation bar height or WindowInsets.Type#statusBars.

For example, to show the color of the element behind the 3-button navigation in Compose, set the window.isNavigationBarContrastEnforced property to false. Setting this property to false makes 3-button navigation fully transparent (note: this property does not affect gesture navigation).

Then, use WindowInsets.tappableElement to align UI behind insets for tappable system UI. If non-0, the user is using tappable bars, like three button navigation. In this case, draw an opaque view or box behind the tappable bars.

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
window.isNavigationBarContrastEnforced = false
MyTheme {
Surface(...) {
MyContent(...)
ProtectNavigationBar()
}
}
}
}
}


// Use only if required.
@Composable
fun ProtectNavigationBar(modifier: Modifier = Modifier) {
val density = LocalDensity.current
val tappableElement = WindowInsets.tappableElement
val bottomPixels = tappableElement.getBottom(density)
val usingTappableBars = remember(bottomPixels) {
bottomPixels != 0
}
val barHeight = remember(bottomPixels) {
tappableElement.asPaddingValues(density).calculateBottomPadding()
}

Column(
modifier = modifier.fillMaxSize(),
verticalArrangement = Arrangement.Bottom
) {
if (usingTappableBars) {
Box(
modifier = Modifier
.background(MaterialTheme.colorScheme.background)
.fillMaxWidth()
.height(barHeight)
)
}
}
}

Inset handling tips for UIs that use Compose

The following tips apply only for apps that use Jetpack Compose. See additional Compose-related tips in this video: Edge-to-edge and insets | Compose Tips.

8. Use Scaffold’s PaddingValues

For Compose, use Scaffold instead of Surface to organize your app’s UI with TopAppBar, BottomAppBar, NavigationBar, and NavigationRail. Use Scaffold’s PaddingValues parameter to inset your critical UI. In most cases, that’s all you need to do.

However, there are cases where applying Scaffold’s PaddingValues will cause unexpected results. Scaffold’s PaddingValues includes insets for the top, bottom, start and end edges of the screen. You may need values for only certain edges. One approach is to make a copy of the parameter and manually adjust top, bottom, start and end insets so as to not apply too much padding.

Figure 14. Left: The input field at the bottom is obscured by the system’s navigation bar after targeting SDK 35. Middle: Scaffold’s PaddingValues applied to the input field. The system uses the size of the status bar and the top app bar to calculate the top padding value, which creates excess padding above the input field. Right: Scaffold’s PaddingValues applied but with top padding manually removed.

Here’s the incorrect code, causing the excess padding seen in the middle image of Figure 14.

// Causes excess padding, seen in the middle image of Figure 14.
Scaffold { innerPadding -> // innerPadding is Scaffold's PaddingValues
InputBar(
...
contentPadding = innerPadding
) {...}
}

Here’s the corrected code that generates proper padding, as seen in the right-side image of Figure 14.

// Function to make a copy of PaddingValues, using existing defaults unless an
// alternative value is specified
​​private fun PaddingValues.copy(
layoutDirection: LayoutDirection,
start: Dp? = null,
top: Dp? = null,
end: Dp? = null,
bottom: Dp? = null,
) = PaddingValues(
start = start ?: calculateStartPadding(layoutDirection),
top = top ?: calculateTopPadding(),
end = end ?: calculateEndPadding(layoutDirection),
bottom = bottom ?: calculateBottomPadding(),
)

// Produces correct padding, seen in the right-side image of Figure 14.
Scaffold { innerPadding -> // innerPadding is Scaffold's PaddingValues
val layoutDirection = LocalLayoutDirection.current
InputBar(
...
contentPadding = innerPadding.copy(layoutDirection, top = 0.dp)
) {...}
}

9. Use high level WindowInsets APIs

Similar to Scaffold’s PaddingValues, you can also use the high-level WindowInset APIs to easily and safely draw critical UI elements. These are:

See Inset fundamentals to learn more.

Inset handling tips for UIs that use Views

The following apply only for Views-based apps.

10. Prefer ViewCompat.setOnApplyWindowInsetsListener over fitsSystemWindows=true

You could use fitsSystemWindows=true to inset your app’s content. It’s an easy 1-line code change. However, don’t use fitsSystemWindows on a View that contains your entire layout (including the background). This will make your app look not edge-to-edge because fitsSystemWindows handles insets on all edges.

Figure 15. Edge-to-edge enforced. Left: fitsSystemWindows=false (the default). Right: fitsSystemWindows=true. This is not the result we want because the app does not look edge-to-edge.
Figure 16. Edge-to-edge enforced, fitsSystemWindows=true. The gap on the left edge is due to a display cutout, which is not visible here. This is not the result we want because the app doesn’t look edge-to-edge.

fitsSystemWindows can create an edge-to-edge experience if using CoordinatorLayouts or AppBarLayouts.Add fitsSystemWindows to the CoordinatorLayout and the AppBarLayout, and the AppBarLayout draws edge-to-edge, which is what we want.

Figure 17. Edge-to-edge enforced. Left: AppBarLayout does not automatically handle insets. Right: Add fitsSystemWindows to AppBarLayout and CoordinatorLayout to draw edge-to-edge.
<!-- Figure 17 -->
<CoordinatorLayout
android:fitsSystemWindows="true"
...>
<AppBarLayout
android:fitsSystemWindows="true"
...>
<TextView
android:text="App Bar Layout"
.../>
</AppBarLayout>
</CoordinatorLayout>

In this case, AppBarLayout used fitsSystemWindows to draw underneath the status bar rather than avoiding it, which is the opposite of what we might expect. Furthermore, AppBarLayout with fitsSystemWindows=true only applies padding for the top and not the bottom, start, or end edges.

The CoordinatorLayout and AppBarLayout objects have the following behavior when overriding fitsSystemWindows:

  • CoordinatorLayout: backgrounds of child views draw underneath the system bars if those views also set fitsSystemWindows=true. Padding is automatically applied to the content of those Views (e.g. text, icons, images) to account for system bars and display cutouts.
  • AppBarLayout: draws underneath the system bars if fitsSystemWindows=true and automatically applies top padding to content.

In most cases, handle insets with ViewCompat.setOnApplyWindowInsetsListener because it allows you to define which edges should handle insets and has consistent behavior. See tips #4 and #11 for a code example.

11. Apply insets based on app bar height during the layout phase

If you find that your app’s content is hiding underneath an app bar, you might need to apply insets after the app bar is laid out, taking the app bar height into account.

For example, if you have scrolling content underneath an AppBarLayout in a FrameLayout, you could use code like this to ensure the scrolling content appears after the AppBarLayout. Notice padding is applied within doOnLayout.

val myScrollView = findViewById<NestedScrollView>(R.id.my_scroll_view)
val myAppBar = findViewById<AppBarLayout>(R.id.my_app_bar_layout)


ViewCompat.setOnApplyWindowInsetsListener(myScrollView) { scrollView, windowInsets ->
val insets = windowInsets.getInsets(
WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout()
)
myAppBar.doOnLayout { appBar ->
scrollView.updatePadding(
left = insets.left,
right = insets.right,
top = appBar.height,
bottom = insets.bottom
)
}
WindowInsetsCompat.CONSUMED
}

Likewise, if you have scrolling content that should sit above a BottomNavigationView, you’ll want to account for the BottomNavigationView’s height once it is laid out.

Need more time to migrate?

It might take significant work to properly support an edge-to-edge experience. Before you target SDK 35, consider how long you need to make the necessary changes in your app.

If you need more time to handle insets to be compatible with the system’s default edge-to-edge behavior, you can temporarily opt-out using R.attr#windowOptOutEdgeToEdgeEnforcement. But do not plan to use this flag indefinitely as it will be non-functional in the near future.

The flag might be particularly helpful for apps that have tens to hundreds of Activities. You might opt-out each Activity, then — make your app edge-to-edge one Activity at a time.

Here’s one approach to using this flag. Assuming your minSDK is less than 35, this attribute must be in values-v35.xml.

<!-- In values-v35.xml -->
<resources>
<!-- TODO: Remove once activities handle insets. -->
<style name="OptOutEdgeToEdgeEnforcement">
<item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>
</style>
</resources>

Create an empty style for past versions in values.xml:

<!-- In values.xml -->
<resources>
<!-- TODO: Remove once activities handle insets. -->
<style name="OptOutEdgeToEdgeEnforcement">
<!-- android:windowOptOutEdgeToEdgeEnforcement
isn't supported before SDK 35. This empty
style enables programmatically opting-out. -->
</style>
</resources>

Call the style before accessing the decor view in setContentView:

class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {

// Call before the DecorView is accessed in setContentView
theme.applyStyle(R.style.OptOutEdgeToEdgeEnforcement, /* force */ false)

super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
...
}
}

Resources

Android 15 AOSP released today. Our team has created blogs, videos, and codelabs to help get your app ready to handle the Android 15 edge-to-edge enforcement. What follows is a list of old and new resources for further learning.

Documentation

Videos

Codelabs

Handle edge-to-edge enforcements in Android 15 (Compose)

--

--