Skip to content
ExponaBLOG
MODERN ANDROID & KOTLIN · DAY 096 / 100

Edge-to-edge display: a practical Android & Kotlin guide

Modern Android layout.

Expona Admin5 min read

Why edge-to-edge display matters now

Your app's layout is bumping into system UI elements like the status bar and navigation bar, wasting valuable screen real estate. Modern Android devices support edge-to-edge layouts, but implementing them correctly requires understanding how to handle window insets—and most teams skip this step or get it wrong. Edge-to-edge display: a practical Android & Kotlin guide walks you through the APIs, patterns, and gotchas we've learned from shipping production apps across devices with notches, punch holes, and gesture navigation.

Understanding enableEdgeToEdge()

The foundation starts with enableEdgeToEdge(), a Material 3 utility that tells Android your activity wants to draw behind the system bars. Before this helper, you'd manually set window flags and configure translucent backgrounds. Now we call it once in onCreate() and let the framework handle the plumbing.

kotlin
import androidx.activity.enableEdgeToEdge

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent(R.layout.activity_main)
    }
}

What does enableEdgeToEdge() actually do? It sets your window to draw behind the status bar and navigation bar, applies transparent backgrounds where needed, and configures the system to use light or dark content colors based on your app theme. On API 29 and above (which covers 99% of production devices), this works seamlessly. Below that, it degrades gracefully by applying padding instead.

One thing to watch: enableEdgeToEdge() doesn't automatically fix your layouts. Your content can still be hidden behind the status bar or gesture navigation. That's where window insets come in.

Handling window insets properly

Window insets tell your app how much space is consumed by system bars, notches, and gesture navigation. We need to read them and apply padding or margins so content doesn't hide behind these elements. The modern approach uses ViewCompat.setOnApplyWindowInsetsListener() or, better yet, lets Compose/Scaffold handle it for us.

In XML layouts, use the android:fitsSystemWindows="true" attribute on your root ViewGroup. This is the simplest path for traditional View-based layouts:

kotlin
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:fitsSystemWindows="true">
    
    <AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        
        <MaterialToolbar
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary" />
    </AppBarLayout>
    
    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />
</LinearLayout>

However, android:fitsSystemWindows only applies padding to the root view. For more granular control—especially when you have multiple fragments or complex hierarchies—manually listen for insets:

kotlin
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat

val myView = findViewById<View>(R.id.my_view)

ViewCompat.setOnApplyWindowInsetsListener(myView) { view, insets ->
    val systemBars = insets.getInsets(
        WindowInsetsCompat.Type.systemBars()
    )
    
    view.setPadding(
        systemBars.left,
        systemBars.top,
        systemBars.right,
        systemBars.bottom
    )
    
    insets
}

Real-world scenario: we had a bottom navigation bar that was getting hidden by gesture navigation on Pixel devices. Using WindowInsetsCompat.Type.navigationBars() separately from systemBars() let us apply different padding to different views. The bottom nav got the full inset, while the main content got only the status bar inset.

Scaffold applies insets for you

If you're using Jetpack Compose, Scaffold is your best friend. It automatically handles window insets for top bars, bottom bars, and floating action buttons. You don't need to manually calculate or apply padding—Scaffold does it:

kotlin
import androidx.compose.material3.Scaffold
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.NavigationBar
import androidx.compose.runtime.Composable

@Composable
fun MyScreen() {
    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text("My App") }
            )
        },
        bottomBar = {
            NavigationBar {
                NavigationBarItem(
                    icon = { Icon(Icons.Default.Home, null) },
                    label = { Text("Home") },
                    selected = true,
                    onClick = {}
                )
            }
        }
    ) { innerPadding ->
        LazyColumn(
            modifier = Modifier.padding(innerPadding)
        ) {
            items(100) { index ->
                Text("Item $index")
            }
        }
    }
}

Scaffold passes innerPadding to its content lambda. Use this for your main content view. The framework automatically accounts for the status bar, navigation bar, and any notches or punch holes. For a large team shipping multiple screens, this consistency is invaluable.

If you need custom bar colors to extend edge-to-edge, configure them in your theme or pass explicit colors to TopAppBar and NavigationBar. Compose will handle the inset calculations behind the scenes.

Testing with gesture navigation

Physical testing is non-negotiable here. Gesture navigation (swipe from edges) is the default on modern Android, and it consumes extra space that traditional button navigation doesn't. The Android emulator lets you toggle this in the settings.

Steps to test in the emulator:

  1. Launch your emulator and open Settings > System > Gestures > System Navigation
  2. Select Gesture navigation (not Three-button navigation)
  3. Run your app and check that content isn't hidden at the bottom
  4. Swipe from the bottom edge—verify your UI doesn't interfere with the gesture

On physical devices, the gesture navigation bar is typically 40-48 dp tall. If your bottom navigation bar or FAB is positioned without accounting for bottom insets, users will tap thin air. We've seen this in production: taps on the bottom nav register but feel delayed because the gesture system intercepts them first.

Use Android Studio's Layout Inspector to visualize insets in real time:

  1. Run your app in the debugger
  2. Open Tools > Layout Inspector
  3. Select your app process
  4. Expand the view hierarchy and inspect padding values

You'll immediately see if your views have the right padding applied.

Common mistakes

Mistake 1: Calling enableEdgeToEdge() without handling insets. The activity will draw behind system bars, but content will be obscured. Always pair enableEdgeToEdge() with inset handling—either via android:fitsSystemWindows or manual listeners.

Mistake 2: Applying android:fitsSystemWindows to nested views. It only works meaningfully on the root view. For nested views, use ViewCompat.setOnApplyWindowInsetsListener() instead.

Mistake 3: Ignoring the IME (input method editor) inset. When the soft keyboard appears, it also generates an inset. If you have a search bar at the bottom, it will be covered by the keyboard. Check for WindowInsetsCompat.Type.ime() separately and adjust your layout if needed.

Mistake 4: Not testing on devices with notches or punch holes. The simulator can show these, but real devices behave differently. Notches on the sides (landscape mode) require special handling for top insets. Test on a Pixel 6 or similar device if possible.

Mistake 5: Using fixed pixel values instead of inset-aware dimensions. Never hardcode padding like android:paddingTop="24dp" when the status bar height varies. Let the inset system do the math.

Key takeaways

  • Call enableEdgeToEdge() in onCreate() to opt into edge-to-edge rendering on Material 3 activities.
  • Use android:fitsSystemWindows="true" on root ViewGroups for automatic inset padding in traditional layouts.
  • For custom layouts, listen for insets with ViewCompat.setOnApplyWindowInsetsListener() and apply padding per view as needed.
  • In Compose, Scaffold handles insets automatically—apply the innerPadding to your content and move on.
  • Always test with gesture navigation enabled in the emulator and on physical devices with notches or punch holes.
  • Use the Layout Inspector to debug inset application in real time; don't guess based on screenshots.
  • Account for IME insets if you have fixed UI at the bottom of the screen.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles