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)
)
}
}
}
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.
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.
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)
...
}
}
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.