Insets handling tips for Android 15’s edge-to-edge enforcement
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.
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.
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:
- Use material components to make handling insets easier
- Draw backgrounds edge-to-edge, and inset critical UI
- Handle display cutout and caption bar insets
- Don’t forget the last list item
- Don’t forget IMEs
- For backwards compatibility, use
enableEdgeToEdge
instead ofsetDecorFitsSystemWindows
- 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.
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
.
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.
After targeting SDK 35, the white box disappears and all content moves to the left. Some content draws underneath the camera cutout.
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:
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.
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.
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
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 -->
<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.
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.
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 -->
<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 setfitsSystemWindows=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 iffitsSystemWindows=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
- Behavior changes for apps targeting Android 15: Edge-to-edge enforcement
- Window insets in Compose
- Cutouts in Compose
- Display content edge-to-edge in your app (Views)
- Support display cutouts (Views)
Videos
- Edge to Edge & Insets (Compose)
- Insets: Compose edition
- 3 things to improve the user experience of your Android app: Handle edge-to-edge enforcements in Android 15 walkthrough (Compose)