Overview
As of Android 15, edge-to-edge layouts are becoming standard. To implement edge-to-edge correctly in Compose apps, it's not enough to call enableEdgeToEdge(); each UI component must properly handle window insets for the status bar, navigation bar, and IME (input method editor). This article proposes a responsibility-sharing pattern for inset handling and explains it with implementation examples.
Background and Issues
On Android 15 (targetSdk 35+), edge-to-edge is enforced by default. Previously, you had to explicitly call setDecorFitsSystemWindows(false), but going forward it will be the default behavior.
When adapting Jetpack Compose to this behavior, several problems arise.
- Content being obscured: If insets are not handled, content can be hidden behind the status bar or navigation bar.
- Double counting: If multiple components handle the same inset, unintended spacing occurs. For example, if Material3's
Scaffoldadds status bar padding andTopAppBaralso handles it, extra space appears. - Interference with IME: The combination of
windowSoftInputMode="adjustResize"andenableEdgeToEdge()causes double application of IME padding on Android 11 and earlier.
To avoid these issues, a design where each component shares responsibility for handling insets is effective.
Solution
1. Where to call enableEdgeToEdge()
Call enableEdgeToEdge() at the start of onCreate(), before super.onCreate(). This ensures edge-to-edge is enabled from the initial frame.
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge() // call before super
super.onCreate(savedInstanceState)
setContent { ... }
}
2. Delegating inset handling responsibilities
With edge-to-edge, the content area extends under system bars. To make effective use of this area, adopt the following responsibility-sharing pattern.
- Status bar: Delegate to
TopAppBar - Navigation bar: Delegate to banner ads (or a fixed footer)
- IME: Use
imePadding()on the parent container of input fields
By having each component focus on a single responsibility, double counting is prevented and the code's intent becomes clearer.
3. Set Scaffold's contentWindowInsets to zero
Material3's Scaffold includes status bar and navigation bar insets in its default paddingValues. When individual components (TopAppBar, BannerAd, etc.) handle insets separately, set the Scaffold's contentWindowInsets to zero to prevent double counting.
Scaffold(
contentWindowInsets = WindowInsets(0, 0, 0, 0),
topBar = { TopAppBar(...) }, // leave TopAppBar's windowInsets as default
) { paddingValues ->
...
}
4. Default behavior of TopAppBar
The default windowInsets for TopAppBar is TopAppBarDefaults.windowInsets (i.e. WindowInsets.statusBars). This causes the TopAppBar background to extend into the status bar area and pushes internal content down by the status bar height. No special changes are required.
5. Handle navigation bar with banner ads
When a banner ad is present, delegating navigation bar handling to the banner is effective. Combine these three modifiers:
height(AdSize.BANNER.height.dp + navigationBarHeight): banner height + navigation bar heightbackground(MaterialTheme.colorScheme.surfaceVariant): set background color for navigation bar areanavigationBarsPadding(): restrict the AdView's drawing to above the navigation bar
6. Avoid double counting for NavigationBar
Material3's NavigationBar adds navigation bar insets as internal padding by default. When a banner ad already handles the navigation bar, set windowInsets = WindowInsets(0) on the NavigationBar to prevent double counting.
7. Control IME only with imePadding()
The combination of windowSoftInputMode="adjustResize" and enableEdgeToEdge() causes double application of IME padding on older Android versions.
Remove windowSoftInputMode="adjustResize" from AndroidManifest.xml, and on the Compose side use only imePadding() on the parent container of input fields.
Surface(modifier = modifier.fillMaxSize()) {
Column(
modifier = Modifier
.fillMaxSize()
.imePadding() // add bottom padding when IME is shown
) {
TextField(...)
LazyColumn(modifier = Modifier.weight(1f)) { ... }
}
}
By giving LazyColumn a weight(1f), when the IME is shown the Column's shrinkage is absorbed by the LazyColumn, preventing the focused text field from being hidden by the keyboard.
Summary
The following four points are important to implement edge-to-edge correctly in Jetpack Compose.
- Call
enableEdgeToEdge()beforesuper.onCreate() - Set Scaffold's
contentWindowInsetsto zero and have each component handle insets individually - Delegate navigation bar handling to banner ads (or a fixed footer), allowing NavigationBar's
windowInsetsto be set to zero - Control IME only with
imePadding()and removewindowSoftInputMode="adjustResize"
Adopting these patterns clarifies inset handling intent and keeps code concise and maintainable. Refer to the example implemented in the Procedure Checker app and try applying these patterns to your own app.