edge_to_edge_plus 0.1.0 copy "edge_to_edge_plus: ^0.1.0" to clipboard
edge_to_edge_plus: ^0.1.0 copied to clipboard

PlatformAndroid

Typed Android window insets, tappableElement-aware system bar protection and diagnostics for the mandatory edge-to-edge enforcement in Android 15 and 16.

edge_to_edge_plus #

Typed Android window insets for Flutter, and a system bar background that knows the difference between gesture and three-button navigation.

pub package pub points license: MIT CI

void main() => runApp(
  const EdgeToEdgeScope(          // 1. wrap the app
    child: MaterialApp(home: HomePage()),
  ),
);

// 2. draw a background behind the navigation bar — but only when there is one
Column(
  mainAxisSize: MainAxisSize.min,
  children: [
    MyFooter(),
    SystemBarProtection.bottom(color: footerColour),
  ],
)

That is the whole integration. No MainActivity.kt edits, no theme attributes, no manifest changes.


Why this exists #

Android 15 (API 35) draws apps targeting API 35+ edge-to-edge by default. Android 16 removed the opt-out entirely: windowOptOutEdgeToEdgeEnforcement is deprecated and disabled for apps targeting API 36.

At the same time the tools you used to reach for stopped working. Window.setStatusBarColor and Window.setNavigationBarColor are deprecated on API 35+; the status bar is transparent and ignores its colour outright, and the navigation bar colour now only reaches three-button navigation.

Google's own answer, from "Insets handling tips for Android 15's edge-to-edge enforcement", is to stop setting colours and draw your own view behind the bar, sized from WindowInsets.Type#tappableElement() for the three-button navigation bar and WindowInsets.Type#statusBars() for the status bar. They are shipping a library that does this — for Compose. There is no Flutter equivalent.

This package is that equivalent, and it is deliberately small.

The gap, precisely #

MediaQuery gives you padding, viewPadding, viewInsets and systemGestureInsets — four values that have already been merged together. It does not expose tappableElement, and that is the only correct signal for "is there a three-button navigation bar, and how tall is it".

Here is the same Android 16 device (API 36, emulator), measured by this package in both navigation modes:

Inset Gesture navigation Three-button navigation
statusBars.top 24.0 24.0
navigationBars.bottom 24.0 48.0
tappableElement.bottom 0.0 48.0
systemGestures L 29.7 · R 29.7 · B 32.0 B 48.0

Both modes reserve space at the bottom, so navigationBars is non-zero in both. Only three-button navigation actually swallows taps there, and only tappableElement says so. Size your bottom chrome from navigationBars and you get a dead band of colour under gesture navigation on half the devices in the world.

SystemBarProtection.bottom reads tappableElement, so it renders a 48pt band in the right-hand column and nothing at all in the left-hand one.


What Flutter already handles #

Read this before adding the dependency. If everything you need is in this table, you do not need this package.

You need Already in Flutter Use
Keep content out from under the bars SafeArea
Merged padding / view padding / keyboard inset MediaQuery.paddingOf, viewPaddingOf, viewInsetsOf
Edge-to-edge turned on at all ✅ since Flutter 3.27 it is the default on Android 15+
Per-screen system bar icon brightness AnnotatedRegion<SystemUiOverlayStyle>
Keyboard-avoiding layout Scaffold.resizeToAvoidBottomInset
Gesture vs three-button, measured SystemInsets.tappableElement
A background behind a system bar SystemBarProtection
displayCutout / captionBar / mandatorySystemGestures SystemInsets
The nav bar's resting height while the IME is up navigationBarsIgnoringVisibility
Being told your config is silently broken EdgeToEdge.diagnose()

This package is not a SafeArea replacement, not a theming system, and not a keyboard-avoidance layer. It exposes the insets Flutter merges away, and draws one widget with them.


Install #

dependencies:
  edge_to_edge_plus: ^0.1.0

Runtime dependencies: flutter and meta — and meta is not a design choice, it is what Pigeon's generated code imports. Nothing else.


Recipes #

The footer colour should continue behind a three-button navigation bar, and the system's contrast scrim should get out of the way because your own surface already provides the contrast.

EdgeToEdgeRegion.solidBottom(
  backgroundBrightness: Theme.of(context).brightness,
  child: Scaffold(
    body: content,
    bottomNavigationBar: Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        MyFooter(),
        SystemBarProtection.bottom(color: footerColour),
      ],
    ),
  ),
)

Under gesture navigation SystemBarProtection.bottom collapses to zero height and the footer sits flush against the gesture strip, which is what you want.

A page whose content scrolls under the bars #

Leave the system scrim enabled: arbitrary content can end up behind the three-button bar and the icons still have to be readable.

EdgeToEdgeRegion.scrollingBottom(
  backgroundBrightness: Brightness.dark,
  child: Scaffold(
    body: ListView(
      padding: EdgeInsets.only(
        bottom: EdgeToEdgeScope.insetsOf(context).systemBars.bottom,
      ),
      children: panels,
    ),
  ),
)

A scrim under the status bar #

Fade content out under a transparent status bar instead of cutting it off:

SystemBarProtection.top(
  gradient: const LinearGradient(
    begin: Alignment.topCenter,
    end: Alignment.bottomCenter,
    colors: [Color(0xB3000000), Color(0x00000000)],
  ),
  child: myScrollingContent,
)

The keyboard #

This package reports the IME inset and stops there — Scaffold already handles avoidance. What it adds is the resting navigation bar height, so bottom chrome does not jump when the keyboard opens:

final insets = EdgeToEdgeScope.insetsOf(context);
insets.ime.bottom;                              // keyboard height
insets.imeVisible;                              // reliable during the animation
insets.navigationBars.bottom;                   // 0 while the IME covers it
insets.navigationBarsIgnoringVisibility.bottom; // the stable resting height

Fixing the default MaterialApp gives you #

SystemUiOverlayStyle.light and .dark both set systemNavigationBarColor to opaque black with light icons, and MaterialApp applies one of them from your theme's brightness. On a light app that is a black bar with invisible icons. Override it once:

MaterialApp(
  theme: ThemeData(
    appBarTheme: const AppBarTheme(
      systemOverlayStyle: SystemBarStyles.solidBottomOnLight,
    ),
  ),
)

Checking your configuration #

for (final issue in await EdgeToEdge.diagnose(context: context)) {
  debugPrint('${issue.severity.name}: ${issue.title}\n${issue.explanation}');
}

Reports a dead windowOptOutEdgeToEdgeEnforcement, a targetSdk below 35, system bar colours the OS is ignoring, and a missing EdgeToEdgeScope. Every one of those fails silently on its own.

Seeing what is going on #

EdgeToEdgeDebugOverlay(child: MyApp())

Draws a labelled band for every inset type plus a summary card. Compiles out of release builds entirely — the guard is kReleaseMode, a compile-time constant.


Compatibility #

Platform Behaviour
Android 16+ (API 36) Full support. Edge-to-edge is enforced and cannot be opted out of for apps targeting 36.
Android 15 (API 35) Full support. Enforced for apps targeting 35+; the opt-out attribute still works here.
Android 10–14 (API 29–34) Full support. All inset types are reported; contrast enforcement setters work.
Android 7–9 (API 24–28) Insets reported. Contrast enforcement is a no-op (the platform API is API 29+).
iOS, web, macOS, Windows, Linux Safe no-op. isSupported is false, insets fall back to MediaQuery, widgets render their child unchanged. Nothing throws.

minSdk is 24, matching the current Flutter default.

On non-Android platforms the MediaQuery fallback fills statusBars, navigationBars, systemBars, ime and systemGestures, and leaves tappableElement, displayCutout, mandatorySystemGestures and captionBar at zero — because MediaQuery genuinely does not know them. The practical effect is deliberate: SystemBarProtection draws nothing off Android, which is the right answer on a platform with no three-button navigation bar.


This package will never recommend windowOptOutEdgeToEdgeEnforcement #

Other packages tell you to add this to res/values-v35/styles.xml:

<!-- Do not do this. -->
<item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>

It is deprecated and disabled for apps targeting API 36 on Android 16. Adding it buys you one release cycle and leaves you with a layout that has never been tested against the behaviour every user will get. EdgeToEdge.diagnose() reports it as an error when it detects the attribute on a device that is ignoring it.

The same goes for wording: this package does not "fix" systemNavigationBarColor. That setter is gone, and nothing can bring it back. What this package does is draw the background yourself, correctly sized — which is the migration Google documented.


API #

Symbol What it is
EdgeToEdgeScope Wrap your app; streams insets and rebuilds dependents. Falls back to MediaQuery if you forget it.
SystemInsets One EdgeInsets per WindowInsets.Type, in logical pixels.
SystemBarProtection.top / .bottom A background behind a system bar, sized from statusBars / tappableElement.
EdgeToEdgeRegion.solidBottom / .scrollingBottom / .custom Per-page system bar style over AnnotatedRegion.
SystemBarStyles The same styles as plain constants, for AppBarTheme.systemOverlayStyle.
SystemNavigationMode threeButton · twoButton · gesture · unknown.
EdgeToEdgeCapabilities API level, target SDK, whether enforcement applies, whether the opt-out flag is set and honoured.
EdgeToEdgeDiagnostics The silent-misconfiguration checks.
EdgeToEdgeDebugOverlay The visualiser.
EdgeToEdge Static, context-free accessors for one-shot reads.

Naming note. The enum is SystemNavigationMode, not NavigationMode: Flutter already exports a NavigationMode from media_query.dart, and shipping the shorter name would make ambiguous_import errors for anyone importing both this package and package:flutter/material.dart.


Example app #

example/ is a five-screen demo, not a counter:

  1. Inset inspector — every inset type live; rotate or open the keyboard.
  2. Solid bottom — sticky footer with SystemBarProtection.bottom.
  3. Scrolling bottom — full-bleed content with a status bar gradient.
  4. Before / after — one switch that turns the handling off so the overlap is visible.
  5. DiagnosticsEdgeToEdge.diagnose() for the device you are on.
cd example && flutter run

To see the three-button case on an emulator:

adb shell cmd overlay enable com.android.internal.systemui.navbar.threebutton

Contributing #

The Dart ↔ Android boundary is generated from pigeons/messages.dart by Pigeon. Never hand-edit lib/src/messages.g.dart or android/src/main/kotlin/tr/com/kerembas/edge_to_edge_plus/Messages.g.kt.

dart run pigeon --input pigeons/messages.dart && dart format .

The dart format half is required — Pigeon does not emit formatted Dart, and CI checks both "regeneration produces no diff" and "everything is formatted".

Before opening a PR:

dart format --output=none --set-exit-if-changed .
dart analyze --fatal-infos --fatal-warnings
flutter test --coverage
(cd example/android && ./gradlew :edge_to_edge_plus:test)

DECISIONS.md records why things are the way they are — including the ones that look wrong until you read the reason. Add an entry when you make a non-obvious call.


License #

MIT © Kerem Baş

1
likes
160
points
83
downloads

Documentation

API reference

Publisher

verified publisherkerembas.com.tr

Weekly Downloads

Typed Android window insets, tappableElement-aware system bar protection and diagnostics for the mandatory edge-to-edge enforcement in Android 15 and 16.

Repository (GitHub)
View/report issues

Topics

#android #edge-to-edge #insets #system-ui #safe-area

License

MIT (license)

Dependencies

flutter, meta

More

Packages that depend on edge_to_edge_plus

Packages that implement edge_to_edge_plus