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