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.
Related
When user logs into the app I need to set 'PWD' in the shared_preference variable. I need to get that value in splashcreen of my app so that when user opens the app again it need redirect to only password entering page. How can I do it in flutter.
onPressed: () async {
SharedPreferences prefs = await SharedPreferences.getInstance();
appdata.loginmode = prefs.setString('LOGIN_MODE', 'PWD');
Navigator.push(
context,
MaterialPageRoute(builder: (context) => BottomNavigation()),
);
print('Shared....');
print(prefs.getString('LOGIN_MODE'));
},
This what I am doing when user click login it will set to 'PWD', then I need to call the prefs in splashscree.
Short Answer
Not for splash screen but I am using the same logic for the onboard screen. I hope this answer will help. So, on your main.dart file, create a nullable int onBoardCount, outside of any class, you're gonna need this on your splash screen. Also, instantiate SharedPreferences in main and pass it with onboardcount to you MyApp();
int? onBoardCount;
void main() async {
WidgetsFlutterBinding.ensureInitialized();
SharedPreferences prefs = await SharedPreferences.getInstance();
// Get onboard count from prefs, if it already exists,if not it will return null
onBoardCount = prefs.getInt('onBoardKey');
runApp(MyApp(prefs,onBoardCount));
}
Now, your MyApp file should be something like
class MyApp extends StatefulWidget {
late SharedPreferences prefs;
....
MyApp(this.prefs,this.onBoardCount, ...);
Now in your splash_screen.dart use the following logic.
void onSubmitDone(AppStateProvider stateProvider, BuildContext context) {
await prefs.setInt('onBoardKey', 0);
// Some route logic like route.push("/home");
}
Long Answer
I am using Go Router for routing and Provider for state management so, here's my app's code.
Main.dart
int? onBoardCount;
void main() async {
WidgetsFlutterBinding.ensureInitialized();
SharedPreferences prefs = await SharedPreferences.getInstance();
onBoardCount = prefs.getInt('onBoardKey');
....
runApp(MyApp(prefs, onBoardCount));
}
I have a separate MyApp file to reduce congestion.
my_app.dart
class MyApp extends StatefulWidget {
late SharedPreferences prefs;
int? onBoardCount;
MyApp(this.prefs, this.onBoardCount,..... {Key? key})
: super(key: key);
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
// The appstate provider is handling app level state
late AppStateProvider appStateProvider;
#override
void didChangeDependencies() {
super.didChangeDependencies();
appStateProvider = AppStateProvider(
widget.onBoardCount, widget.prefs,....);
}
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
....
ChangeNotifierProvider(
create: (context) => AppStateProvider(
widget.onBoardCount,
widget.prefs,...)),
Provider(
create: (context) => AppRouter(
appStateProvider: appStateProvider,
onBoardCount: widget.onBoardCount,
prefs: widget.prefs,
),
),
],
child: Builder(
builder: ((context) {
final GoRouter router = Provider.of<AppRouter>(context).router;
return MaterialApp.router(
routeInformationParser: router.routeInformationParser,
routerDelegate: router.routerDelegate);
}),
),
);
}
}
App State Provider File
Create a function to update onboard logic and notify listeners.
class AppStateProvider with ChangeNotifier {
AppStateProvider(this.onBoardCount, this.prefs,..);
int? onBoardCount;
late SharedPreferences prefs;
bool? _isOnboarded;
bool get isOnboard => _isOnboarded as bool;
void hasOnBoarded() async {
await prefs.setInt('onBoardKey', 0);
_isOnboarded = true;
notifyListeners();
}
}
On Router file
class AppRouter {
late AppStateProvider appStateProvider;
late SharedPreferences prefs;
int? onBoardCount;
AppRouter({
required this.appStateProvider,
required this.onBoardCount,
required this.prefs,
});
get router => _router;
late final _router = GoRouter(
refreshListenable: appStateProvider,
initialLocation: "/",
routes: [
...
],
redirect: (state) {
final String onboardLocation =
state.namedLocation("Your Route name");
bool isOnboarding = state.subloc == onboardLocation;
bool? toOnboard = prefs.containsKey('onBoardKey') ? false : true;
print("Is LoggedIn is $isLoggedIn");
if (toOnboard) {
return isOnboarding ? null : onboardLocation;
}
return null;
});
}
Since the router is listening to appStateProvider, it will change once you call hasOnBoarded() on your onboard screen.
OnBoardScreen
void onSubmitDone(AppStateProvider stateProvider, BuildContext context) {
stateProvider.hasOnBoarded();
GoRouter.of(context).go("/");
}
I hope this will help please leave comments. FYI, ... is some other codes that I feel it's not important for this topic.
My goal for this flutter app is to change the theme (Dark, Light, System) based on the stored Shared Preferences data.
I used Provider so that every time the user changes the theme, the entire app will update based on the selected theme. The issue is when the user first starts up the app, it finishes building before we can get the value of the theme from Shared Preference. Therefore we are getting a null value for the theme when the app initially loads. My code only works if the user is updating the theme value after the app finishes loading on startup.
void main() async {
WidgetsFlutterBinding.ensureInitialized();
runApp(ChangeNotifierProvider<PreferencesProvider>(
create: (context) {
return PreferencesProvider();
},
child: MyApp()));
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
print(SettingsSharedPreferences.getThemeString());
return ChangeNotifierProvider<HabitProvider>(
create: (context) {
return HabitProvider();
},
child: MaterialApp(
darkTheme: ThemeData(brightness: Brightness.dark),
themeMode: context.watch<PreferencesProvider>().theme == "System"
? ThemeMode.system
: context.watch<PreferencesProvider>().theme == "Dark"
? ThemeMode.dark
: context.watch<PreferencesProvider>().theme == "Light"
? ThemeMode.light
: ThemeMode.system,
home: MyHomePage(title: 'My App'),
),
);
}
}
This is the Provider class. Note that the variable theme is getting its data from the stored Shared Preferences data.
class PreferencesProvider extends ChangeNotifier {
String? theme = SettingsSharedPreferences.getThemeString();
....
}
Below is the Shared Preference code where we get the value of theme from:
class SettingsSharedPreferences {
static const _keytheme = "theme";
static String _defaultTheme = "System";
static setTheme(String theme) async {
await sharedPreferences.setString(_keytheme, theme);
}
static Future<String?> getTheme() async {
final sharedPreferences = await SharedPreferences.getInstance();
if (sharedPreferences.getString(_keytheme) == null) {
await setTheme(_defaultTheme);
}
return sharedPreferences.getString(_keytheme);
}
static String? getThemeString() {
print("getThemeString");
String? theme;
getTheme().then((val) {
theme = val;
});
return theme;
}
}
I suggest loading the preferences before the app is built and shown to the user:
Future<void> main() async {
...
final preferences = await SharedPreferences.getInstance();
runApp(MainApp(
preferences: preferences,
));
}
In your case, you could pass the theme value to MyApp widget.
My story in short is, I can successfully change app theme dynamically, but I fail when it comes to start my app with the last chosen ThemeData.
Here is the main.dart:
import "./helpers/constants/themeConstant.dart" as themeProfile;
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MultiProvider(
providers: [
//Several ChangeNotifierProviders
],
child: Consumer<AuthenticateProvider>(
builder: (ctx, authData, _) => ChangeNotifierProvider<ThemeChanger>(
create: (_) {
ThemeData themeToBeSet;
themeProfile.setInitialTheme().then((themeData) {
themeToBeSet = themeData;
});
return ThemeChanger(themeToBeSet);
},
child: _MaterialAppWithTheme(authData),
)
)
);}}
The problem is themeToBeSet variable always being null eventhough I set a ThemeData as I do below:
ThemeData selectedTheme;
Future<ThemeData> setInitialTheme() async {
final preferences = await SharedPreferences.getInstance();
if (!preferences.containsKey(ApplicationConstant.sharedTheme)) {
selectedTheme = appThemeDataDark;
final currentThemeInfo = json.encode({
"themeStyle": ApplicationConstant.darkAppTheme
});
preferences.setString(ApplicationConstant.sharedTheme, currentThemeInfo);
return selectedTheme;
}
else {
final extractedThemeInfo = json.decode(preferences.getString(ApplicationConstant.sharedTheme)) as
Map<String, dynamic>;
final chosenTheme = extractedThemeInfo["themeStyle"];
if (chosenTheme == ApplicationConstant.lightAppTheme) {
selectedTheme = appThemeDataLight;
return selectedTheme;
}
else if (chosenTheme == ApplicationConstant.darkAppTheme) {
selectedTheme = appThemeDataDark;
return selectedTheme;
}
else {
selectedTheme = appThemeDataDark;
return selectedTheme;
}}}
Here, I used shared_preferences.dart package to store and retrieve ThemeData info. If I debug this block, I see that my selectedTheme variable is set one of these ThemeData successfully. But, for a reason I couldn't able to find out, themeToBeSet variable on main.dart is not assigned to the result of my setInitialTheme() method.
Is it because of being asynchronous? But, isn't Dart waiting an asynchronous method with .then()?
In order not to leave any questionmarks realated for my other sections, I'm also sharing ThemeChanger class,
class ThemeChanger with ChangeNotifier {
ThemeData _themeData;
ThemeChanger(
this._themeData
);
getTheme() => _themeData;
setTheme(ThemeData theme) {
_themeData = theme;
notifyListeners();
}
}
And, _MaterialAppWithTheme,
class _MaterialAppWithTheme extends StatelessWidget {
final AuthenticateProvider authData;
_MaterialAppWithTheme(
this.authData,
);
Widget build(BuildContext context) {
final theme = Provider.of<ThemeChanger>(context);
return MaterialApp(
title: 'Game Shop Demo',
theme: theme.getTheme(),
home: authData.isLogedin ?
HomeScreen(authData.userId) :
FutureBuilder(
future: authData.autoLogin(),
builder: (ctx, authResult) => authResult.connectionState == ConnectionState.waiting ?
SplashScreen():
LoginScreen()
),
routes: {
//Several named routes
},
);
}
}
As I suspected, I misused .then().
I thought Dart is awaiting when you use .then() but after running into this post, I learnt that it is not awaiting..
So, I carry setInitialTheme() method to ThemeChanger class (it was in a different class previously) and call it in the constructor. Here its final version,
class ThemeChanger with ChangeNotifier {
ThemeData _themeData;
ThemeChanger() {
_setInitialTheme();
}
getTheme() => _themeData;
setTheme(ThemeData theme) {
_themeData = theme;
notifyListeners();
}
Future<ThemeData> _setInitialTheme() async {
final preferences = await SharedPreferences.getInstance();
if (!preferences.containsKey(ApplicationConstant.sharedTheme)) {
_themeData = appThemeDataDark;
final currentThemeInfo = json.encode({
"themeStyle": ApplicationConstant.darkAppTheme
});
preferences.setString(ApplicationConstant.sharedTheme, currentThemeInfo);
return _themeData;
}
else {
final extractedThemeInfo = json.decode(preferences.getString(ApplicationConstant.sharedTheme)) as Map<String, dynamic>;
final chosenTheme = extractedThemeInfo["themeStyle"];
if (chosenTheme == ApplicationConstant.lightAppTheme) {
_themeData = appThemeDataLight;
return _themeData;
}
else if (chosenTheme == ApplicationConstant.darkAppTheme) {
_themeData = appThemeDataDark;
return _themeData;
}
else {
_themeData = appThemeDataDark; //Its better to define a third theme style, something like appThemeDefault, but in order not to spend more time on dummy stuff, I skip that part
return _themeData;
}
}
}
}
Now, as you can see, ThemeChanger class is no longer expecting a ThemeData manually, but setting it automatically whenever its called as setInitialTheme() method is assigned to its constructor. And, of course, MyApp in main.dart is changed accordingly:
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MultiProvider(
providers: [
//Several ChangeNotifierProviders
],
child: Consumer<AuthenticateProvider>(
builder: (ctx, authData, _) => ChangeNotifierProvider<ThemeChanger>(
create: (_) => ThemeChanger(),
child: _MaterialAppWithTheme(authData),
)
)
);
}
}
Now, app is launching just fine with the last selected ThemeData which has a pointer stored in SharedPreferences.
I store bool isDarkTheme variable in my General provider class, I can acces it whenever I want.
The thing I want to do is to save that theme preference of user and whenever user opens app again, instead of again isDarkThem = false I want it to load from preference that I stored in SharedPreferences.
Here is my code of General provider: (I guess it is readable)
import 'package:shared_preferences/shared_preferences.dart';
class General with ChangeNotifier {
bool isDarkTheme = false;
General() {
loadDefaultTheme();
}
void loadDefaultTheme() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
isDarkTheme = prefs.getBool("is_dark_theme") ?? false;
}
void reverseTheme() {
isDarkTheme = !isDarkTheme;
notifyListeners();
saveThemePreference(isDarkTheme);
}
void saveThemePreference(bool isDarkTheme) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setBool("is_dark_theme", isDarkTheme);
}
}
Dart does not support async constructors so I think we should take another approach here. I usually create a splash screen (or loading screen, whatever you call it) to load all basic data right after the app is opened.
But if you only want to fetch theme data, you can use the async/await pair in main method:
void main() async {
WidgetsFlutterBinding.ensureInitialized(); // this line is needed to use async/await in main()
final prefs = await SharedPreferences.getInstance();
final isDarkTheme = prefs.getBool("is_dark_theme") ?? false;
runApp(MyApp(isDarkTheme));
}
After that, we can pass that piece of theme data to the General constructor:
class MyApp extends StatelessWidget {
final bool isDarkTheme;
MyApp(this.isDarkTheme);
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => General(isDarkTheme), // pass it here
child: MaterialApp(
home: YourScreen(),
),
);
}
}
We should change a bit in the General class as well, the loadDefaultTheme method is left out.
class General with ChangeNotifier {
bool isDarkTheme;
General(this.isDarkTheme);
// ...
}
My goal is to create an app where the user can choose his preferred theme.
I'm saving the user's choice with shared preferences so I can load it the next app start.
The user can either select:
- Dark Mode (Independent from the OS Settings)
- Light Mode (Independent from the OS Settings)
- System (Changes between Dark Mode and Light mode depending on the OS settings)
With the help of BLoC, I almost achieved what I want. But the problem is that I need to pass the brightness inside my Bloc event. And to get the system (OS) brightness I need to make use of
MediaQuery.of(context).platformBrightness
But the Bloc gets initiated before MaterialApp so that MediaQuery is unavailable. Sure I can pass the brightness later(from a child widget of MaterialApp) but then (for example, if the user has dark mode activated) it goes from light to dark but visible for a really short time for the user(Because inside the InitialState I passed in light mode).
class MyApp extends StatelessWidget {
final RecipeRepository recipeRepository;
MyApp({Key key, #required this.recipeRepository})
: assert(recipeRepository != null),
super(key: key);
#override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<ThemeBloc>(create: (context) =>
ThemeBloc(),),
],
child: BlocBuilder<ThemeBloc, ThemeState>(
builder: (context, state){
return MaterialApp(
theme: state.themeData,
title: 'Flutter Weather',
localizationsDelegates: [
FlutterI18nDelegate(fallbackFile: 'en',),
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate
],
supportedLocales: [
const Locale("en"),
const Locale("de"),
],
home: Home(recipeRepository: recipeRepository),
);
},
),
);
}
}
ThemeBloc:
class ThemeBloc extends Bloc<ThemeEvent, ThemeState> {
#override
ThemeState get initialState =>
ThemeState(themeData: appThemeData[AppTheme.Bright]);
#override
Stream<ThemeState> mapEventToState(
ThemeEvent event,
) async* {
if (event is LoadLastTheme) {
ThemeData themeData = await _loadLastTheme(event.brightness);
yield ThemeState(themeData: themeData);
}
if (event is ThemeChanged) {
await _saveAppTheme(event.theme);
yield ThemeState(themeData: appThemeData[event.theme]);
}
}
Future<ThemeData> _loadLastTheme(Brightness brightness) async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
String themeString = prefs.getString(SharedPrefKeys.appThemeKey);
print("saved theme: $themeString");
if ((prefs.getString(SharedPrefKeys.appThemeKey) != null) &&
themeString != "AppTheme.System") {
switch (themeString) {
case "AppTheme.Bright":
{
return appThemeData[AppTheme.Bright];
}
break;
///Selected dark mode
case "AppTheme.Dark":
{
return appThemeData[AppTheme.Dark];
}
break;
}
}
print("brightness: $brightness");
if (brightness == Brightness.dark) {
return appThemeData[AppTheme.Dark];
} else {
return appThemeData[AppTheme.Bright];
}
}
Future<void> _saveAppTheme(AppTheme appTheme) async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setString(SharedPrefKeys.appThemeKey, appTheme.toString());
}
}
If you absolutely must do it like this, you can get MediaQuery data directly from the low-level window object like this:
final brightness = MediaQueryData.fromWindow(WidgetsBinding.instance.window).platformBrightness;
However, I would strongly recommend you consider that if you need access to MediaQuery from within your bloc, you should instead move your BlocProvider to get instantiated after your MaterialApp so you can access MediaQuery normally.