Search code examples
fluttersharedpreferencesflutter-change-notifier

Init values in class extending ChangeNotifier


I am new to Flutter, and I have been trying to make a very basic example: changing the theme at runtime from dark to light.

So far so good, it works using ChangeNotifier, but now I'd like to initialize my _isDarkMode variable at startup, by using SharedPreferences.

My solution feels like a hack, and is completely wrong: it seems to load from the preferences, but the end result is always dark mode.

This is what I did. First, I modified the class with an init function, and added the necessary calls to SharedPreferences:

class PreferencesModel extends ChangeNotifier {
  static const _darkModeSetting = "darkmode";

  bool _isDarkMode = true; // default, overridden by init()

  bool get isDarkMode => _isDarkMode;

  ThemeData get appTheme => _isDarkMode ? AppThemes.darkTheme : AppThemes.lightTheme;

  void init() async {
    final prefs = await SharedPreferences.getInstance();
    final bool? dark = prefs.getBool(_darkModeSetting);
    _isDarkMode = dark ?? false;
    await prefs.setBool(_darkModeSetting, _isDarkMode);
  }

  void setDarkMode(bool isDark) async {
    print("setting preferences dark mode to ${isDark}");
    final prefs = await SharedPreferences.getInstance();
    await prefs.setBool(_darkModeSetting, isDark);
    _isDarkMode = isDark;
    notifyListeners();
  }
}

Then, in the main I call the init from the create lambda of the ChangeNotifierProvider:

void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (context) {
          var prefs = PreferencesModel();
          prefs.init(); // overrides dark mode
          return prefs;
        })
      ],
      child: const MyApp(),
    )
  );
}

The State creating the MaterialApp initializes the ThemeMode based on the preferences:

class _MyAppState extends State<MyApp> {
  @override
  Widget build(BuildContext context) {
    return Consumer<PreferencesModel>(
      builder: (context, preferences, child) {
        return MaterialApp(
          title: 'MyApp',
          home: MainPage(title: 'MyApp'),
          theme: AppThemes.lightTheme,
          darkTheme: AppThemes.darkTheme,
          themeMode: preferences.isDarkMode ? ThemeMode.dark : ThemeMode.light,
        );
      }
    );
  }
}

Of course if I change the settings in my settings page (with preferences.setDarkMode(index == 1); on a ToggleButton handler) it works, changing at runtime from light to dark and back. The initialization is somehow completely flawed.

What am I missing here?


Solution

  • Unconventionally, I answer my own question.

    The solution is to move the preferences reading to the main, changing the main to be async.

    First, the PreferencesModel should have a constructor that sets the initial dark mode:

    class PreferencesModel extends ChangeNotifier {
      static const darkModeSetting = "darkmode";
    
      PreferencesModel(bool dark) {
        _isDarkMode = dark;
      }
    
      bool _isDarkMode = true;
      // ...
    

    Then, the main function can be async, and use the shared preferences correctly, passing the dark mode to the PreferencesModel:

    void main() async {
      WidgetsFlutterBinding.ensureInitialized();
    
      final prefs = await SharedPreferences.getInstance();
      final bool dark = prefs.getBool(PreferencesModel.darkModeSetting) ?? false;
      print("main found dark as ${dark}");
    
      runApp(
        MultiProvider(
          providers: [
            ChangeNotifierProvider(create: (context) => PreferencesModel(dark))
          ],
          child: const RecallableApp(),
        )
      );
    }
    

    Please note the WidgetsFlutterBinding.ensureInitialized(); call, otherwise the shared preferences won't work and the app crashes.