flutterriverpodflutter-sharedpreference

The default theme is displayed for a moment when the app is launched. [Flutter]


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.

Added theme_mode.dart

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

int modeToVal(ThemeMode mode) {
  switch (mode) {
    case ThemeMode.system:
      return 1;
    case ThemeMode.dark:
      return 2;
    case ThemeMode.light:
      return 3;
    default:
      return 0;
  }
}

ThemeMode valToMode(int val) {
  switch (val) {
    case 1:
      return ThemeMode.system;
    case 2:
      return ThemeMode.dark;
    case 3:
      return ThemeMode.light;
    default:
      return ThemeMode.system;
  }
}

Future<void> saveThemeMode(ThemeMode mode) async {
  final pref = await SharedPreferences.getInstance();
  pref.setInt('theme_mode', modeToVal(mode));
  print('saveThemeMode: $mode');
}

Future<ThemeMode> loadThemeMode() async {
  final pref = await SharedPreferences.getInstance();
  final ret = valToMode(pref.getInt('theme_mode') ?? 0);
  print('loadThemeMode: $ret');
  return ret;
}

Here is the actual working code

import 'package:app_name/utils/theme_mode.dart' as theme_mode; // add

class StorageServiceImpl extends StorageService {
  @override
  Future<ThemeMode> loadThemeMode() => Future(() => theme_mode.loadThemeMode()); // update

  @override
  Future<void> saveThemeMode(ThemeMode mode) async {
    theme_mode.saveThemeMode(mode); // add
  }
}

Now the theme changes are perfect, no more momentary initial value display when loading, etc. Thank you so much, I really appreciate it.


Solution

  • 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.