skip to Main Content

Themes are switched using RIverpod; ThemeMode is saved using Shared Preferences. They are working fine.But when I set the default values as follows, the default theme is shown for a moment at the beginning. That is ugly.

late ThemeMode _themeMode = ThemeMode.system;.

If I don’t set the initial value, I get the following error, but the application runs fine without crashing.

late ThemeMode _themeMode;
LateInitialisationError: field '_themeMode@47036434' has not been initialised.

The whole code looks like this.

void main() async {
  WidgetsBinding widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
  FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
  await SharedPreferences.getInstance();

  runApp(const ProviderScope(child: MyApp()));
}


class MyApp extends ConsumerWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Demo App',
      theme: myLightTheme,
      darkTheme: myDarkTheme,
      themeMode: ref.watch(themeModeProvider).mode,
      home: const HomeScreen(),
    );
  }
}

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../utils/theme_mode.dart';

final themeModeProvider = ChangeNotifierProvider((ref) => ThemeModeNotifier());

class ThemeModeNotifier extends ChangeNotifier {

  late ThemeMode _themeMode = ThemeMode.system;

  ThemeModeNotifier() {
    _init();
  }

  ThemeMode get mode => _themeMode;

  void _init() async {
    _themeMode = await loadThemeMode(); // get ThemeMode from shared preferences
    notifyListeners();
  }

  void update(ThemeMode nextMode) async {
    _themeMode = nextMode;
    await saveThemeMode(nextMode); // save ThemeMode to shared preferences
    notifyListeners();
  }
}

I would like to somehow prevent this error from appearing.
Please, I would appreciate it if you could help me.

I tryed to delete "late" and be nullable. But it didn’t work.

2

Answers


  1. You’re using FlutterNativeSplash, then you can remove splash screen after the theme mode loaded.

    Completer can be helpful for this situation.

    class ThemeModeNotifier extends ChangeNotifier {
      late ThemeMode _themeMode;
      final completer = Completer<void>();
    
      ThemeModeNotifier() {
        _init();
      }
    
      ThemeMode get mode => _themeMode;
    
      void _init() async {
        _themeMode = await loadThemeMode(); // get ThemeMode from shared preferences
        completer.complete();
        notifyListeners();
      }
    
      void update(ThemeMode nextMode) async {
        _themeMode = nextMode;
        await saveThemeMode(nextMode); // save ThemeMode to shared preferences
        notifyListeners();
      }
    }
    

    and where removing the SplashScreen,

    // ...
    await ref.read(themeModeProvider).completer.future;
    FlutterNativeSplash.remove();
    
    Login or Signup to reply.
  2. The point is that you have to somehow wait somewhere for the asynchronous code to load your ThemeMode. Along with this, I recommend that you stop using ChangeNotifierProvider in favor of Notifier.

    0️⃣ This is what your ThemeModeNotifier will look like now:

    final themeModeProvider = NotifierProvider<ThemeModeNotifier, ThemeMode>(ThemeModeNotifier.new);
    
    class ThemeModeNotifier extends Notifier<ThemeMode> {
      late StorageService _storageService;
    
      @override
      ThemeMode build() {
        _storageService = ref.watch(storageServiceProvider);
    
        return ThemeMode.system;
      }
    
      Future<void> update(ThemeMode nextMode) async {
        state = nextMode;
        await _storageService.saveThemeMode(nextMode);
      }
    }
    

    In the build method, you can safely listen to watch any of your providers, and this method will be restarted every time your dependencies change.

    You can also insure yourself with a side effect for the future (when new dependencies appear that may change):

      @override
      ThemeMode build() {
        _storageService = ref.watch(storageServiceProvider);
        _storageService.loadThemeMode().then((value) => state = value);
        return ThemeMode.light;
      }
    

    1️⃣ Now, as you can see, StorageService has appeared. This is a neat dependency injection to use later on this instance in the update method. And here’s what it looks like:

    abstract class StorageService {
      Future<ThemeMode> loadThemeMode();
      Future<void> saveThemeMode(ThemeMode mode);
    }
    
    final storageServiceProvider = Provider<StorageService>((ref) {
      return StorageServiceImpl(); // create an instance here
    });
    

    I have used abstract code for brevity of explanation and no bugs in my ide.

    2️⃣ Next, your main method now looks like this:

    void main() async {
      WidgetsBinding widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
      FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
      await SharedPreferences.getInstance();
    
      final container = ProviderContainer();
      final StorageService _storageService = container.read(storageServiceProvider);
      container.read(themeModeProvider.notifier).state =
          await _storageService.loadThemeMode();
    
      runApp(
        UncontrolledProviderScope(
          container: container,
          child: MyApp(),
        ),
      );
    }
    

    It is in it that the asynchronous initialization of your state will now take place.

    P.s. descendants, when Riverpod >2.x.x is able to override again with overrideWithValue for NotifierProvider, use this method. Follow the situation in this issue.


    Here is a complete example of a working application, in order to understand how it works. Try running directly in dartpad.dev:

    import 'package:flutter/material.dart';
    import 'package:flutter_riverpod/flutter_riverpod.dart';
    
    void main() async {
      final container = ProviderContainer();
      final StorageService _storageService = container.read(storageServiceProvider);
      container.read(themeModeProvider.notifier).state =
          await _storageService.loadThemeMode();
    
      runApp(
        UncontrolledProviderScope(
          container: container,
          child: const MyApp(),
        ),
      );
    }
    
    class MyApp extends ConsumerWidget {
      const MyApp({super.key});
    
      @override
      Widget build(BuildContext context, WidgetRef ref) {
        final mode = ref.watch(themeModeProvider);
        print('build $MyApp - $mode');
        return MaterialApp(
          theme: ThemeData.light(),
          darkTheme: ThemeData.dark(),
          themeMode: mode,
          home: Scaffold(
            body: Center(child: Text('Now $mode')),
            floatingActionButton: FloatingActionButton(
              child: const Icon(Icons.mode_night_outlined),
              onPressed: () => ref
                  .read(themeModeProvider.notifier)
                  .update(mode == ThemeMode.dark ? ThemeMode.light : ThemeMode.dark),
            ),
          ),
        );
      }
    }
    
    abstract class StorageService {
      Future<ThemeMode> loadThemeMode();
      Future<void> saveThemeMode(ThemeMode mode);
    }
    
    class StorageServiceImpl extends StorageService {
      @override
      Future<ThemeMode> loadThemeMode() => Future(() => ThemeMode.dark);
    
      @override
      Future<void> saveThemeMode(ThemeMode mode) async {}
    }
    
    final storageServiceProvider =
        Provider<StorageService>((ref) => StorageServiceImpl());
    
    final themeModeProvider =
        NotifierProvider<ThemeModeNotifier, ThemeMode>(ThemeModeNotifier.new);
    
    class ThemeModeNotifier extends Notifier<ThemeMode> {
      late StorageService _storageService;
    
      @override
      ThemeMode build() {
        print('build $ThemeModeNotifier');
        _storageService = ref.watch(storageServiceProvider);
    
        return ThemeMode.light;
      }
    
      Future<void> update(ThemeMode nextMode) async {
        state = nextMode;
        await _storageService.saveThemeMode(nextMode);
      }
    }
    

    Pay particular attention to the fact that StorageServiceImpl.loadThemeMode returns ThemeMode.dark. And ThemeMode.light is returned in ThemeModeNotifier.build. But when the application starts, the theme will be exactly dark.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search