Skip to main content

Handling Enforced Edge-to-Edge in Android 15 (Target SDK 35)

 Upgrading your application's targetSdk to 35 (Android 15) introduces a significant visual breaking change. Upon compilation, you may notice your TopAppBar colliding with the system clock, or your bottom navigation buttons obscured by the device's gesture handle.

This is not a rendering bug; it is the new standard. Android 15 enforces an "Edge-to-Edge" layout by default for all apps targeting API 35. The operating system deprecated the ability to opt-out of this behavior, meaning window.setDecorFitsSystemWindows(true) is effectively ignored.

This guide provides a rigorous technical breakdown of why this shift occurred and details production-ready solutions for both Jetpack Compose and legacy XML View systems.

The Root Cause: Why Layouts Break on API 35

Historically, the Android Window class managed a concept called "fitting system windows." When enabled (the previous default), the framework calculated the height of the status bar and the navigation bar, then applied a corresponding padding to your root view. This ensured your content sat safely between the system bars.

With Android 15, Google aims to modernize the platform UI by maximizing screen real estate.

The Architectural Shift

When you target SDK 35:

  1. Implicit Edge-to-Edge: The framework forces the content view to extend behind the status bar and navigation bar.
  2. Transparent System Bars: The default colors for system bars are set to transparent.
  3. Inset Consumption: The system no longer automatically consumes window insets. Your layout hierarchy receives the raw coordinates of the entire screen, including the areas obstructed by system UI.

If your code does not explicitly listen for and handle these WindowInsets, your interactive elements will render underneath system overlays, rendering them unclickable or visually cluttered.

Solution 1: Handling Insets in Jetpack Compose

Jetpack Compose was designed with edge-to-edge in mind. However, older implementations might rely on Scaffold defaults that behave differently in SDK 35, or manual layouts that lack inset awareness.

Step 1: Activity Setup

Ensure you call enableEdgeToEdge before setting content. While Android 15 does this implicitly, calling it explicitly ensures backward compatibility with older Android versions.

// MainActivity.kt
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge // Requires androidx.activity:activity-ktx:1.8.0+

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        // This handles the status bar transparency and 
        // ensures edge-to-edge works on Android 14 and below.
        enableEdgeToEdge()

        setContent {
            MyAppTheme {
                MainScreen()
            }
        }
    }
}

Step 2: Utilizing Scaffold Defaults

The Material 3 Scaffold component automatically handles insets, but only if you respect the contentPadding parameter it provides.

Incorrect Implementation: Ignoring the padding parameter causes the list content to clip behind the navigation bar.

// ❌ BAD PRACTICE
Scaffold(
    topBar = { MyTopBar() }
) { _ -> // Ignoring padding
    LazyColumn { /* items */ }
}

Correct Implementation: Apply the padding provided by the Scaffold to your content container.

// ✅ BEST PRACTICE
import androidx.compose.material3.Scaffold
import androidx.compose.foundation.layout.padding

Scaffold(
    topBar = { MyTopBar() }
) { innerPadding ->
    // innerPadding contains the calculated safe offsets
    LazyColumn(
        modifier = Modifier.padding(innerPadding)
    ) {
        // List items
    }
}

Step 3: Granular Inset Control

If you are not using Scaffold (e.g., a full-screen image with overlaying buttons), you must apply insets manually using Modifiers.

Use Modifier.windowInsetsPadding to apply padding only where necessary.

import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.layout.statusBars
import androidx.compose.ui.Modifier

@Composable
fun FullScreenImageWithOverlay() {
    Box(modifier = Modifier.fillMaxSize()) {
        // Background Image stretches edge-to-edge
        AsyncImage(
            model = "...",
            contentDescription = null,
            contentScale = ContentScale.Crop,
            modifier = Modifier.fillMaxSize()
        )

        // Overlay button avoids status bar
        IconButton(
            onClick = { /*...*/ },
            modifier = Modifier
                .align(Alignment.TopEnd)
                // Applies top padding equivalent to status bar height
                .windowInsetsPadding(WindowInsets.statusBars) 
                .padding(16.dp)
        ) {
            Icon(Icons.Default.Close, contentDescription = "Close")
        }

        // Bottom action button avoids navigation bar
        Button(
            onClick = { /*...*/ },
            modifier = Modifier
                .align(Alignment.BottomCenter)
                // Applies bottom padding for gesture nav/3-button nav
                .windowInsetsPadding(WindowInsets.safeDrawing)
                .padding(16.dp)
        ) {
            Text("Confirm")
        }
    }
}

Solution 2: Fixing Legacy XML / View System

For existing XML-based apps, the upgrade to SDK 35 is often more jarring because XML layouts traditionally relied on android:fitsSystemWindows="true". While this attribute still functions in some contexts, it is brittle in Android 15. The robust solution is ViewCompat.setOnApplyWindowInsetsListener.

Step 1: Update Dependencies

Ensure you are using the latest version of androidx.core:core-ktx.

implementation("androidx.core:core-ktx:1.13.0") // Check for latest version

Step 2: Apply Insets Programmatically

In your Activity or Fragment, locate the root view of your layout and apply a listener. This listener extracts the exact pixel values of the system bars and applies them as padding to the view.

import android.os.Bundle
import android.view.View
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding

class XmlLayoutActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        // 1. Enable Edge-to-Edge
        enableEdgeToEdge()
        
        setContentView(R.layout.activity_xml_layout)

        // 2. Find the root view (ConstraintLayout, CoordinatorLayout, etc.)
        val rootView = findViewById<View>(R.id.root_container)

        // 3. Apply the listener
        ViewCompat.setOnApplyWindowInsetsListener(rootView) { view, windowInsets ->
            // Extract the insets for status bars and navigation bars
            val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())

            // Apply the insets as padding to the view
            // We use updatePadding to preserve existing padding (optional)
            view.updatePadding(
                left = insets.left,
                top = insets.top,
                right = insets.right,
                bottom = insets.bottom
            )

            // Return CONSUMED if you don't want siblings/children to receive these insets
            // Usually, returning windowInsets is safer for nested hierarchies
            WindowInsetsCompat.CONSUMED 
        }
    }
}

Avoiding "Double Padding"

A common error occurs when both the Activity and a Fragment try to apply insets. If your architecture is Single-Activity, apply the insets listener only in the Fragment that needs to handle the UI drawing, or apply it to the FragmentContainerView in the Activity but ensure fragments don't re-apply it unnecessarily.

Deep Dive: Handling 3-Button Navigation vs. Gesture Navigation

Android 15 handles the visual presentation of the navigation bar differently depending on user settings.

  1. Gesture Navigation: The navigation bar is effectively 0dp height visually (transparent), but the WindowInsets.safeDrawing will return bottom insets to prevent touch conflicts with the swipe-up gesture.
  2. 3-Button Navigation: This bar is still solid or translucent in many cases. The insets API will return the exact height of this button bar.

If you hardcode padding (e.g., paddingBottom="48dp"), your UI will look correct on one device and broken on another. Always use WindowInsetsCompat.Type.systemBars() or WindowInsets.safeDrawing.

Common Pitfalls and Edge Cases

1. Keyboard (IME) Overlap

Standard system bar insets do not include the keyboard. If you have an input field at the bottom of the screen, upgrading to SDK 35 might cause the keyboard to cover it.

The Fix: Combine systemBars with ime insets.

Compose:

Modifier.windowInsetsPadding(
    WindowInsets.safeDrawing.union(WindowInsets.ime)
)

XML:

val combinedInsets = windowInsets.getInsets(
    WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.ime()
)

2. Dialogs and BottomSheets

Standard Android Dialogs create a new Window. By default, these windows might not inherit the edge-to-edge configuration of the Activity.

If you see a black bar behind your BottomSheet on Android 15, ensure your Dialog theme sets:

<item name="android:windowEdgeToEdge">true</item> <!-- API 30+ -->

Or programmatically call enableEdgeToEdge() within the Dialog's context if you are creating custom dialogs.

Conclusion

The mandatory edge-to-edge enforcement in Android 15 is a push towards more immersive, modern application design. While it forces a refactor for apps targeting SDK 35, the resulting codebase is generally cleaner and more predictable than relying on the opaque behavior of fitsSystemWindows.

By leveraging WindowInsets in Compose and ViewCompat listeners in XML, you ensure your application respects the user's device configuration, handling everything from pinhole cameras to gesture navigation bars with pixel-perfect precision.