0

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.

Kaya
  • 13
  • 2

2 Answers2

0

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();
Boseong
  • 368
  • 11
  • Thanks for answering my question. I didn't know Completer. FlutterNativeSplash is used in initState() of HomeScreen(). The error occurs when `ref.watch(themeModeProvider).mode` in main.dart is accessed and `ThemeMode get mode => _themeMode;` is implemented. So it didn't work. – Kaya Aug 03 '23 at 05:56
  • and FlutterNativeSplash was used in initState, so I could not access `ref`. – Kaya Aug 03 '23 at 05:59
  • if you want to use `ref` in `initState`, you can use `ConsumerState` instead of `State` – Boseong Aug 03 '23 at 06:21
  • I am really appriciate for using your time. I'll try it! – Kaya Aug 05 '23 at 02:00
0

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.

Ruble
  • 2,589
  • 3
  • 6
  • 29
  • Thank you for writing such a thoughtful reply. Must have taken a lot of time, not so much for the smart ones ... To be honest, I don't understand half of what you write here. However, I have run it and found that it works correctly. I've also added code to actually exchange data with Shared preferences. – Kaya Aug 03 '23 at 12:26