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