Skip to content

[Android] Streamline Edge-to-Edge and Navigation Bar behavior across Android versions #90098

@TheJulianJES

Description

@TheJulianJES

Background

#81303 added support for the Edge to Edge fullscreen mode introduced in Android 10 (SDK 29).
flutter/engine#28616 limited the support for Edge to Edge to Android 10 (SDK 29) and higher.

In the following, "Android 10+" will refer to "Android 10 (SDK 29) and upwards".

Expectations

The guidelines over at https://developer.android.com/training/gestures/edge-to-edge show the following:
image
With Android 10+, apps should basically always set a transparent navigation bar color.
Setting a SystemUiOverlayStyle in Flutter with systemNavigationBarColor: Colors.transparent and systemNavigationBarContrastEnforced: true (the enforced contrast already defaults to on) enables Android 10+ to (dynamically) show a scrim behind the navigation bar to make the buttons clearly visible. This can be seen on the left image.

When the user chooses gesture navigations, it's expected that apps do not show a a color/overlay behind the bar. This can be seen on the right image. (This is automatically done by Android.)

In both images, the transparent navigation bar color takes effect. In the first image, the white scrim is created by Android (as the transparent navigation bar color possibly couldn't provide enough contrast). In the second image (with gesture navigation), no overlay is shown (as the transparent navigation bar color fully takes effect and no overlay is needed with only the "gesture bar").

Reality

Currently, Flutter apps do the following by default:
(Taken from: flutter/gallery#643)
image
This looks especially weird on devices with rounded corners when the user has gesture navigations on.

SDK >= 29

Here, darkMode defines if the user has currently selected a dark theme.

SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); // Enable Edge-to-Edge on Android 10+
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
  systemNavigationBarColor: Colors.transparent, // Setting a transparent navigation bar color
  systemNavigationBarContrastEnforced: true, // Default
  systemNavigationBarIconBrightness: darkMode ? Brightness.light : Brightness.dark, // This defines the color of the scrim
));

When this is called on Android 10+, the behavior is perfect and as expected:

  • Navigation Bar: transparent scrim is shown
  • Gesture Navigations: no scrim or other overlay is shown -> fully transparent navigation bar

However, when this is called on Android versions below 10 (below SDK 29), the following happens:
While the Edge-to-Edge fullscreen mode will be ignored because of flutter/engine#28616, it still causes the following issues:

  • SDK 26-28: The typical white on black navigation bar is shown. However, in these SDK versions, it's possible to set the navigation bar color with Flutter. So, if a light theme is applied, a white navigation bar with dark navigation bar icons should be shown (and the other way around).
  • SDK 19-25: The typical white on black navigation bar is shown. This is fine.

SDK < 29

For everything older than Android 10 (below SDK 29), the following code produces the expected behavior:
Here, darkMode defines if the user has currently selected a dark theme.

SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
  systemNavigationBarColor: darkMode ? Colors.black : Colors.white,
  systemNavigationBarIconBrightness: darkMode ? Brightness.light : Brightness.dark,
));

Like expected, if a light theme is shown, the user sees a white navigation bar with dark buttons (and the other way around).
For Android 10+, the user gets the weird "navigation bar overlay" even when using gesture navigations (because edge-to-edge is not enabled). (-> the current "default" behavior in Flutter)

Combining?

One could make the assumption that the code can be combined without doing any Android version checking in Dart:

SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); // Enable Edge-to-Edge on Android 10+
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
  systemNavigationBarColor: Colors.transparent, // Setting a transparent navigation bar color
  systemNavigationBarContrastEnforced: true, // Default
  systemNavigationBarIconBrightness: darkMode ? Brightness.light : Brightness.dark, // This defines the color of the scrim
));

However, this causes issues on everything older than Android 10 (below SDK 29):

  • SDK 26-28: Because the transparent navigation bar is still set, it will show a completely black navigation bar with black navigation bar buttons (which are completely invisible) if light mode is selected. This is behavior that needs to be fixed.
  • SDK 19-25: The typical white on black navigation bar is shown. This is fine.

Correct Usage

The following code should produce expected behavior on all Android verisons.

Future<void> redoSystemStyle(bool darkMode) async {
  if (Platform.isAndroid) {
    final AndroidDeviceInfo androidInfo = await deviceInfoPlugin.androidInfo;
    final bool edgeToEdge = androidInfo.version.sdkInt != null && androidInfo.version.sdkInt! >= 29;

    // The commented out check below isn't required anymore since https://github.com/flutter/engine/pull/28616 is merged
    // if (edgeToEdge)
    SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);

    SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
      statusBarColor: Colors.transparent, // Not relevant to this issue
      systemNavigationBarColor: edgeToEdge
          ? Colors.transparent
          : darkMode
              ? Colors.black
              : Colors.white,
      systemNavigationBarContrastEnforced: true,
      systemNavigationBarIconBrightness: darkMode ? Brightness.light : Brightness.dark,
    ));
  } else {
    SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
      statusBarColor: Colors.transparent, // Not relevant to this issue
    ));
  }

Caveats:

  • DeviceInfoPlugin required for checking Android version -> makes this "easy task" kind of complicated
  • DeviceInfoPlugin call is asynchronous and will cause a brief flash (of the navigation bar) when starting the app:
    This can be avoided by calling if (Platform.isAndroid) await deviceInfoPlugin.androidInfo; in main.dart (before "starting the app") on a globally defined DeviceInfoPlugin instance:
    final DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin();, as the plugin caches these results in the instance

Use case

Make it easier for users to implement the new Edge-to-Edge fullscreen behavior in Android 10 and onwards while maintaining proper (navigation bar) behavior on older SDK versions without much effort.
Basically, it shouldn't be required to check the Android SDK version in Dart logic for the expected/default Android 10+ behavior (and not break on older Android versions).

Proposal

There are different possibilities to solve this issue:

  • let enabling SystemUiMode.edgeToEdge always set the navigation bar color to transparent
    -> This seems like the easiest solution but it could potentially confuse users as to why their custom systemNavigationBarColor is ignored on Android 10+. (Although I do not see a use-case where you could ever want that?)
    Currently, some users are confused that enabling the edge-to-edge mode requires a transparent navigation bar color in order to do anything really. I'd assume that because of the few people using edge-to-edge behavior in Flutter at the moment, this change might be feasible. (stable branch still activates it for more SDK versions) (also, nobody sets a custom navigation bar color when wanting to use proper edge-to-edge behavior?)
  • make an (official) Flutter package which can take of Android edge-to-edge/navigation bar handling
    -> This seems too complicated for a "simple styling issue".
  • introduce another method (or modify the existing methods?) in SystemChrome to handle this
    -> This is worded very openly and could mean anything
    -> Perhaps introduce a fallback navigation bar color (black/white for older Android versions) but still require to explicitly set the transparent navigation bar color for Android 10+?
  • ...?

Notes

Maintainers, feel free to edit/update this issue with with your ideas.
To everyone else, I'll try to update this issue with your ideas/feedback to collect the "main ideas".

cc @Piinks

Metadata

Metadata

Assignees

Labels

P2Important issues not at the top of the work listc: new featureNothing broken; request for a new capabilityc: proposalA detailed proposal for a change to Flutterframeworkflutter/packages/flutter repository. See also f: labels.platform-androidAndroid applications specifically

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions