Skip to content

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

Description

@TheJulianJES

Background

#81303 added support for the Edge to Edge fullscreen mode introduced in Android 10 (SDK 29).
flutter-team-archive/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-team-archive/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-team-archive/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

Activity

  1. added
    in triagePresently being triaged by the triage team
    frameworkflutter/packages/flutter repository. See also f: labels.
    platform-androidAndroid applications specifically
    c: proposalA detailed proposal for a change to Flutter
    c: new featureNothing broken; request for a new capability
    and removed
    in triagePresently being triaged by the triage team
    on Sep 15, 2021
  2. rydmike commented on Sep 15, 2021

    @rydmike
    Contributor

    Thank you @TheJulianJES this is an excellent summary of the current situation and improvement suggestions needed to make it even easier for Flutter developers to make apps that follow the Android guidelines for both current and older API levels. It covers all and then some topics I had noticed as well. 👍🏻 💙

  3. droplet-js commented on Nov 4, 2021

    @droplet-js

    it is work for me

            final SystemUiOverlayStyle light = SystemUiOverlayStyle(
              statusBarColor: Colors.transparent, // 23
              statusBarIconBrightness: Brightness.dark, // 23
              systemNavigationBarColor: Colors.transparent, // 27
              systemNavigationBarDividerColor: Colors.transparent.withAlpha(1) /* 不能用全透明 */, // 28
              systemNavigationBarIconBrightness: Brightness.dark, // 27
              systemNavigationBarContrastEnforced: false, // 29
            );
            SystemChrome.setSystemUIOverlayStyle(light);
  4. rydmike commented on Mar 12, 2022

    @rydmike
    Contributor

    @v7lin I agree, that worked well for me up to Android10 API29 too.

    However, in Android 11 API30 when I do basically what you do above, it get this:

    The status bar and system nav bar icons are light and not dark as they should be.

    Code for above example
    void main() {
      runApp(const MyApp());
    }
    
    final AppBarTheme appBarLight = AppBarTheme(
      backgroundColor: Colors.white.withAlpha(0xEE),
      foregroundColor: Colors.black,
      elevation: 0.5,
      systemOverlayStyle: SystemUiOverlayStyle.dark.copyWith(
        statusBarColor: Colors.transparent,
      ),
    );
    
    class MyApp extends StatelessWidget {
      const MyApp({Key? key}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          debugShowCheckedModeBanner: false,
          title: 'Annotated Region Issue',
          theme: ThemeData.from(colorScheme: const ColorScheme.light())
              .copyWith(appBarTheme: appBarLight),
          home: const MyHomePage(title: 'Annotated Region Issue'),
        );
      }
    }
    
    class MyHomePage extends StatelessWidget {
      const MyHomePage({Key? key, required this.title}) : super(key: key);
      final String title;
    
      @override
      Widget build(BuildContext context) {
        final MediaQueryData media = MediaQuery.of(context);
        final double topPadding = media.padding.top + kToolbarHeight;
        final double bottomPadding = media.padding.bottom;
        SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
        return AnnotatedRegion(
          value: SystemUiOverlayStyle(
            systemNavigationBarColor: Colors.transparent,
            systemNavigationBarDividerColor: Colors.transparent.withAlpha(1),
            systemNavigationBarIconBrightness: Brightness.dark,
            systemNavigationBarContrastEnforced: false,
          ),
          child: Scaffold(
            extendBody: true,
            extendBodyBehindAppBar: true,
            appBar: AppBar(
              title: Text(title),
            ),
            body: GridView.builder(
              padding:
                  EdgeInsets.fromLTRB(10, topPadding + 10, 10, bottomPadding + 10),
              gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                crossAxisCount: 3,
                mainAxisSpacing: 8,
                crossAxisSpacing: 8,
                childAspectRatio: 2,
              ),
              itemCount: 100,
              itemBuilder: (context, index) => Card(
                color: Colors.primaries[index % Colors.primaries.length][800]!,
                elevation: 2,
                child: Center(
                  child: Text(
                    'Tile nr ${index + 1}',
                    style: const TextStyle(color: Colors.white, fontSize: 16),
                  ),
                ),
              ),
            ),
          ),
        );
      }
    }

    If I in this sample comment the line:

    //        systemNavigationBarDividerColor: Colors.transparent.withAlpha(1),

    I get the correct brightness on both the system navigation bar icons and the status bar icons also on Android11 API30. The impact on the status bars is very is very odd since the status bar icons are not even touched in this example in the AnnotatedRegion. The status bar is setup in this example via the AppBarTheme, as it should preferably be, imo.

    This is quite strange and annoying. As mentioned, the above code works correctly on Android 10, API29 producing the dark status bar icons and system navigation bar icons even with the systemNavigationBarDividerColor line present.

    I did not yet test how how Android12 API31 behaves in this case. I'm assuming it is like A11 API30, but the way these things changes, it is maybe not a safe assumption.

    I should probably open a separate issue with this finding, but I feel a bit Flutter issue fatigue at the moment. I'll probably get back to it later...


    EDIT: Made an own issue of it here: #100027

    ping: @TheJulianJES @Piinks

  5. droplet-js commented on Mar 15, 2022

    @droplet-js

    @rydmike This is best practice

        <style name="Theme.App" parent="@android:style/Theme.Black.NoTitleBar">
            <item name="android:windowDrawsSystemBarBackgrounds" tools:targetApi="lollipop">true</item>
            <item name="android:windowLayoutInDisplayCutoutMode" tools:targetApi="p">shortEdges</item>
            <!-- SystemUiOverlayStyle -->
            <item name="android:statusBarColor" tools:targetApi="m">@android:color/transparent</item>
            <item name="android:windowLightStatusBar" tools:targetApi="m">true</item>
            <item name="android:enforceStatusBarContrast" tools:targetApi="q">false</item>
            <item name="android:navigationBarColor" tools:targetApi="o_mr1">@android:color/transparent</item>
            <item name="android:navigationBarDividerColor" tools:targetApi="o_mr1">#01000000</item>
            <item name="android:windowLightNavigationBar" tools:targetApi="o_mr1">true</item>
            <item name="android:enforceNavigationBarContrast" tools:targetApi="q">false</item>
        </style>
    
        <style name="Theme.App.Dark" parent="Theme.App">
            <!-- SystemUiOverlayStyle -->
            <item name="android:windowLightStatusBar" tools:targetApi="m">false</item>
            <item name="android:windowLightNavigationBar" tools:targetApi="o_mr1">false</item>
        </style>
    
        <style name="LaunchTheme" parent="Theme.App">
            <!-- SplashScreen -->
            <item name="android:windowSplashScreenBackground" tools:targetApi="s">@android:color/white</item>
            <item name="android:windowSplashScreenAnimatedIcon" tools:targetApi="s">@android:color/transparent</item>
            <item name="android:windowSplashScreenBrandingImage" tools:targetApi="s">@drawable/branding_icon</item>
    
            <!-- Show a splash screen on the activity. Automatically removed when
                 Flutter draws its first frame -->
            <item name="android:windowBackground">@drawable/launch_background</item>
        </style>
    
        <style name="NormalTheme" parent="Theme.App">
            <item name="android:windowBackground">@android:color/white</item>
        </style>
    import android.graphics.Color;
    import android.os.Build;
    import android.os.Bundle;
    import android.view.View;
    import android.view.Window;
    
    import androidx.annotation.Nullable;
    
    import io.flutter.embedding.android.FlutterActivity;
    
    public class MainActivity extends FlutterActivity {
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            final Window window = getWindow();
            final View decorView = window.getDecorView();
            decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);// edgeToEdge
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                window.setStatusBarColor(Color.TRANSPARENT);
                decorView.setSystemUiVisibility(decorView.getSystemUiVisibility() | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
            }
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
                window.setNavigationBarColor(Color.TRANSPARENT);
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                    window.setNavigationBarDividerColor(Color.parseColor("#01000000"));// 不能用全透明
                }
                decorView.setSystemUiVisibility(decorView.getSystemUiVisibility() | View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR);
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                    window.setNavigationBarContrastEnforced(false);
                }
            }
        }
    }
      runZonedGuarded(
        () async {
          WidgetsFlutterBinding.ensureInitialized();
          final SystemUiOverlayStyle dark = SystemUiOverlayStyle(
            statusBarColor: Colors.transparent /*Android=23*/,
            statusBarBrightness: Brightness.light /*iOS*/,
            statusBarIconBrightness: Brightness.dark /*Android=23*/,
            systemStatusBarContrastEnforced: false /*Android=29*/,
            systemNavigationBarColor: Colors.transparent /*Android=27*/,
            systemNavigationBarDividerColor: Colors.transparent.withAlpha(1) /*Android=28,不能用全透明 */,
            systemNavigationBarIconBrightness: Brightness.dark /*Android=27*/,
            systemNavigationBarContrastEnforced: false /*Android=29*/,
          );
          SystemChrome.setSystemUIOverlayStyle(dark);
          await SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
          runApp(Root());
        },
        (Object error, StackTrace stack) {
          // TODO
        },
        zoneSpecification: ZoneSpecification(
          print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
            // TODO
          },
        ),
      );
  6. added
    P2Important issues not at the top of the work list
    on Mar 23, 2022
  7. devchenli commented on May 23, 2022

    @devchenli

    Any update about SystemUiMode.edgeToEdge ? I want it to be the default option for android 10+.
    Thanks.

  8. camsim99 commented on May 26, 2022

    @camsim99
    Contributor

    To address the components of the issue individually:

    Any update about SystemUiMode.edgeToEdge ? I want it to be the default option for android 10+.

    Making SystemUiMode.edgeToEdge the default mode for API 10+ would be a breaking change because it involves not only making the system bars transparent by default, but also having developers lay out their apps to accommodate the insets that will now be drawn in front of some of the app content.

    This migration should be made to be consistent with Android and to avoid difficulty in using this mode for particular Android versions, though. In the meantime, I think it could be useful to provide guidance on how to achieve behavior that emulates the Android behavior across versions. @Piinks any thoughts on this?

    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. (comment)

    I'll take a look at this issue and update here.

    This is quite strange and annoying. As mentioned, the above code works correctly on Android 10, API29 producing the dark status bar icons and system navigation bar icons even with the systemNavigationBarDividerColor line present. (comment)

    This issue with systemNavigationBarDividerColor was fixed with PR #32167.

  9. Piinks commented on May 26, 2022

    @Piinks
    Contributor

    provide guidance on how to achieve behavior that emulates the Android behavior across versions. @Piinks any thoughts on this?

    There are probably some best practices here, but I do not know them myself. The framework does not distinguish between versions of TargetPlatform, and this API is not specific to Android either. I affects iOS as well IIRC.

    I did see recent discussion on a similar issue that a plugin is probably the best approach. It could avoid the breaking change by having users decide to add it, and from there it can provide the automatic defaults that are being requested here.

  10. camsim99 commented on Jun 9, 2022

    @camsim99
    Contributor

    There are two components included in this issue: one concerning setting edge-to-edge mode by default for API 29+ and the other concerning hidden navigation bar icons. I split them into two issues to continue discussion (see links above!) and am closing this issue.

  11. github-actions commented on Jun 23, 2022

    @github-actions

    This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of flutter doctor -v and a minimal reproduction of the issue.

  12. locked as resolved and limited conversation to collaborators on Jun 23, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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