Flutter how to get brightness without MediaQuery?
Asked Answered
J

3

15

My goal is to create an app where the user can choose his preferred theme. I'm saving the user's choice with shared preferences so I can load it the next app start. The user can either select: - Dark Mode (Independent from the OS Settings) - Light Mode (Independent from the OS Settings) - System (Changes between Dark Mode and Light mode depending on the OS settings) With the help of BLoC, I almost achieved what I want. But the problem is that I need to pass the brightness inside my Bloc event. And to get the system (OS) brightness I need to make use of

MediaQuery.of(context).platformBrightness

But the Bloc gets initiated before MaterialApp so that MediaQuery is unavailable. Sure I can pass the brightness later(from a child widget of MaterialApp) but then (for example, if the user has dark mode activated) it goes from light to dark but visible for a really short time for the user(Because inside the InitialState I passed in light mode).

class MyApp extends StatelessWidget {
  final RecipeRepository recipeRepository;

  MyApp({Key key, @required this.recipeRepository})
      : assert(recipeRepository != null),
        super(key: key);

  @override
  Widget build(BuildContext context) {
    return MultiBlocProvider(
      providers: [
        BlocProvider<ThemeBloc>(create: (context) =>
        ThemeBloc(),),

      ],
      child: BlocBuilder<ThemeBloc, ThemeState>(
        builder: (context, state){

          return MaterialApp(
            theme: state.themeData,
            title: 'Flutter Weather',
            localizationsDelegates: [
              FlutterI18nDelegate(fallbackFile: 'en',),
              GlobalMaterialLocalizations.delegate,
              GlobalWidgetsLocalizations.delegate
            ],
            supportedLocales: [
              const Locale("en"),
              const Locale("de"),
            ],
            home: Home(recipeRepository: recipeRepository),
          );
        },
      ),
    );
  }
}

ThemeBloc:

class ThemeBloc extends Bloc<ThemeEvent, ThemeState> {
  @override
  ThemeState get initialState =>
      ThemeState(themeData: appThemeData[AppTheme.Bright]);

  @override
  Stream<ThemeState> mapEventToState(
    ThemeEvent event,
  ) async* {
    if (event is LoadLastTheme) {
      ThemeData themeData = await _loadLastTheme(event.brightness);
      yield ThemeState(themeData: themeData);
    }
    if (event is ThemeChanged) {
      await _saveAppTheme(event.theme);
      yield ThemeState(themeData: appThemeData[event.theme]);
    }
  }

  Future<ThemeData> _loadLastTheme(Brightness brightness) async {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    String themeString = prefs.getString(SharedPrefKeys.appThemeKey);
    print("saved theme: $themeString");
    if ((prefs.getString(SharedPrefKeys.appThemeKey) != null) &&
        themeString != "AppTheme.System") {
      switch (themeString) {
        case "AppTheme.Bright":
          {
            return appThemeData[AppTheme.Bright];
          }
          break;

        ///Selected dark mode
        case "AppTheme.Dark":
          {
            return appThemeData[AppTheme.Dark];
          }
          break;
      }
    }

    print("brightness: $brightness");
    if (brightness == Brightness.dark) {
      return appThemeData[AppTheme.Dark];
    } else {
      return appThemeData[AppTheme.Bright];
    }

  }

  Future<void> _saveAppTheme(AppTheme appTheme) async {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    await prefs.setString(SharedPrefKeys.appThemeKey, appTheme.toString());
  }
}
Jacklighter answered 23/2, 2020 at 17:9 Comment(0)
B
17

If you absolutely must do it like this, you can get MediaQuery data directly from the low-level platform object like this:

Before Flutter 3.10

final brightness = MediaQueryData.fromWindow(WidgetsBinding.instance.window).platformBrightness;

Flutter 3.10+:

final brightness = PlatformDispatcher.instance.platformBrightness;

However, I would strongly recommend you consider that if you need access to MediaQuery from within your bloc, you should instead move your BlocProvider to get instantiated after your MaterialApp so you can access MediaQuery normally.


After Flutter 3.10, if for whatever reason you really want to get this value manually and react to system changes, you can listen to the onPlatformBrightnessChanged event:

PlatformDispatcher.instance.onPlatformBrightnessChanged(() => {
  final brightness = PlatformDispatcher.instance.platformBrightness;

  // ... Do something with `brightness`
});

Though at this point, you really are just reinventing the wheel. The option is there for people who need it, but you shouldn't use it unless you are absolutely sure you can't use MediaQuery.

Blackpoll answered 23/2, 2020 at 17:38 Comment(4)
That works! But how can I change the whole app theme if my ThemeBloc gets instantiated after MaterialApp?Jacklighter
@JustAQuestion It depends how you are navigating to different pages in the app. The easiest way is to use onGenerateRoute to wrap every page navigated to in a BlocProvider and keep a canonical reference to the bloc in the class level rather than creating it alongside the provider so you won't be recreating the bloc over and over again.Blackpoll
Okay thanks, I will try. And one last question why wouldn't you recommend the low-level window object?Jacklighter
@JustAQuestion If you pull data directly from the low-level object, your app isn't going to get notified of any system changes. For example, if you have your app set to respect the system theme and the user goes into preferences and changes the system theme, your app wouldn't react to the change. The MediaQuery widget does listen to system changes, so if you use MediaQuery.of your app will automatically be rebuilt reflecting changes to the system theme.Blackpoll
U
10

Starting with Flutter version 3.10, use the following code:

var brightness = WidgetsBinding.instance.platformDispatcher.platformBrightness;
Unbidden answered 15/5, 2023 at 6:5 Comment(0)
M
5

After upgrading from flutter 3.7, there are now 2 options:

final mode = PlatformDispatcher.instance.platformBrightness;

// With context
final mode = View.of(context).platformDispatcher.platformBrightness;

Older, deprecated option

final mode = SchedulerBinding.instance.window.platformBrightness;

error: info: 'window' is deprecated and shouldn't be used. Look up the current FlutterView from the context via View.of(context) or consult the PlatformDispatcher directly instead. Deprecated to prepare for the upcoming multi-window support. This feature was deprecated after v3.7.0-32.0.pre. (deprecated_member_use at [appname] lib/src/settings/settings.dart:88)

Murine answered 22/5, 2023 at 6:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.