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.
Related
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.
I am trying to implement theme management in my app.
In the GetMaterialApp I have:
themeMode: ThemeService().getThemeMode(),
In the ThemeService class I have:
ThemeMode getThemeMode() {
String theme = getSavedTheme();
print('Loading theme: $theme');
switch (theme) {
case 'dark':
return ThemeMode.dark;
case 'system':
return ThemeMode.system;
default:
return ThemeMode.light;
}
}
String getSavedTheme() {
var value = _getStorage.read(storageKey);
print('Loaded: $value');
return value ?? 'light';
}
When I open the app for the first time, the app always loads in light theme and I get the following output in the console:
I/flutter ( 4252): Loaded: null
I/flutter ( 4252): Loading theme: light
Seems like Getx Storage isn't loading the value from the stored prefs. Checking the file manually, the value is correct in the prefs:
{"savedTheme":"system"}
If I hot restart right after loading the app for the first time, it works properly and changes the app theme.
Any ideas on what I might be doing wrong, please?
Please check GetStorage initialization in Main method
void main() async { // make it async
await GetStorage.init(); // add this
runApp(MyApp());
}
The solution to my problem was to replace
await GetStorage.init();
with
await GetStorage.init('theme');
For completness sake, this is what I had in the ThemeService class
class ThemeService {
final _getStorage = GetStorage('theme');
final themeBrightness = 'savedTheme';
final themeColor = 'themeColor';
ThemeMode getThemeMode() {
String theme = getSavedTheme();
print('Loading theme: $theme');
switch (theme) {
case 'dark':
return ThemeMode.dark;
case 'system':
return ThemeMode.system;
default:
return ThemeMode.light;
}
}
String getSavedTheme() {
var value = _getStorage.read(themeBrightness);
print('Loaded: $value');
return value ?? 'light';
}
}
I have a Flutter Application where an sqflite database stored the user preference of ThemeMode (viz Dark, Light and System). I have created a splash screen using flutter_native_splash which supports dark mode too.
The Problem is this that I want the splash screen to follow the users stored value for theme mode. Currently, the code I am using is as follows:
class MyRoot extends StatefulWidget {
// const MyRoot({Key? key}) : super(key: key);
static ValueNotifier<ThemeMode> themeNotifier = ValueNotifier(ThemeMode.system);
#override
State<MyRoot> createState() => _MyRootState();
}
class _MyRootState extends State<MyRoot> {
DatabaseHelper? databaseHelper = DatabaseHelper.dhInstance;
ThemeMode? tmSaved;
#override
void initState() {
Future.delayed(Duration.zero, () async => await loadData());
super.initState();
}
#override
Widget build(BuildContext context) {
//to prevent auto rotation of the app
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
return ValueListenableBuilder<ThemeMode>(
valueListenable: MyRoot.themeNotifier,
builder: (_, ThemeMode currentMode, __) {
return Sizer(
builder: (context, orientation, deviceType) {
return MaterialApp(
title: 'My Application',
theme: themeLight, //dart file for theme
darkTheme: themeDark, //dart file for theme
themeMode: tmSaved ?? currentMode,
initialRoute: // my initial root
routes: {
// my routes
.
.
.
// my routes
},
);
},
);
},
);
}
Future<void> loadData() async {
if (databaseHelper != null) {
ThemeMode? themeMode= await databaseHelper?.selectStoredTheme(); // function retrieving sqflite stored value and returning ThemeMode value
if (themeMode != null) {
MyRoot.themeNotifier.value = themeMode;
return;
}
}
MyRoot.themeNotifier.value = ThemeMode.system;
}
}
Currently, this shows a light theme splash screen loading, then converts it into dark with a visible flicker.
ValueListenableBuilder<ThemeMode>(... is to enable real time theme change from settings page in my app which working as intended (taken from A Goodman's article: "Flutter: 2 Ways to Make a Dark/Light Mode Toggle".
main.dart has the below code:
void main() {
runApp(MyRoot());
}
Have you tried loading the setting from sqflite in main() before runApp? If you can manage to do so, you should be able to pass the setting as argument to MyRoot and then the widgets would be loaded from the start with the correct theme. I'm speaking in theory, I can't test what I'm suggesting right now.
Something like:
void main() async {
ThemeMode? themeMode= await databaseHelper?.selectStoredTheme(); // function retrieving sqflite stored value and returning ThemeMode value
runApp(MyRoot(themeMode));
}
[...]
class MyRoot extends StatefulWidget {
ThemeMode? themeMode;
const MyRoot(this.themeMode, {Key? key}) : super(key: key);
static ValueNotifier<ThemeMode> themeNotifier = ValueNotifier(ThemeMode.system);
#override
State<MyRoot> createState() => _MyRootState();
}
EDIT
Regarding the nullable value you mentioned in comments, you can change the main like this:
void main() async {
ThemeMode? themeMode= await databaseHelper?.selectStoredTheme(); // function retrieving sqflite stored value and returning ThemeMode value
themeMode ??= ThemeMode.system;
runApp(MyRoot(themeMode!));
}
which makes themeMode non-nullable, and so you can change MyRoot in this way:
class MyRoot extends StatefulWidget {
ThemeMode themeMode;
const MyRoot(required this.themeMode, {Key? key}) : super(key: key);
[...]
}
Regarding the functionality of ValueNotifier, I simply thought of widget.themeMode as the initial value of your tmSaved property in your state, not as a value to be reused in the state logic. Something like this:
class _MyRootState extends State<MyRoot> {
DatabaseHelper? databaseHelper = DatabaseHelper.dhInstance;
late ThemeMode tmSaved;
#override
void initState() {
tmSaved = widget.themeMode;
super.initState();
}
[...]
}
so that your widgets would already have the saved value at the first build.
PS the code in this edit, as well as in the original part, isn't meant to be working by simply pasting it. Some things might need adjustments, like adding final to themeMode in MyRoot.
Make your splashscreen. A main widget which get data from sqlflite
And make splashscreen widget go to the your home widget with remove it using navigation pop-up
for example :
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'ToDo',
color: // color of background
theme: // theme light ,
darkTheme: // darktheme
themeMode: // choose default theme light - dark - system
home: Splashscreen(),// here create an your own widget of splash screen contains futurebuilder to fecth data and return the mainWidget ( home screen for example)
);
}
}
class Splashscreen extends StatelessWidget {
Future<bool> getData()async{
// get info
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: getData(),
builder: (context,snapshot){
// if you want test snapshot
//like this
if(snapshot.hasData) {
return Home();
} else {
return Container(color: /* background color as same as theme's color */);
}
}
);
}
}
Hy here everyone. I am new to flutter and i want to check if User is SignedIn. If so the user navigate to HomeScreen else SplashScreen.
Here is my main.dart
void main() async{
runApp(MyApp());
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
}
// ignore: must_be_immutable
class MyApp extends StatelessWidget {
String initRoute;
User user = FirebaseAuth.instance.currentUser;
getUser(){
if (user != null) {
initRoute = MainScreen.routeName;
} else {
initRoute = SplashScreen.routeName;
}
}
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Instant Tasker',
theme: theme(),
initialRoute: initRoute,
routes: routes,
);
}
}
The proper way that I see is through the Firebase plugin, here is how it looks like.
in main.dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
Firebase.initializeApp().then((value) => print(value));
runApp(MyApp());
}
final FirebaseAuth auth = FirebaseAuth.instance;
String userData() {
final User user = auth.currentUser;
final uid = user.uid;
return uid;
}
if a user is logged in it will return uid(String) otherwise null
The easiest and the best way to identify if a user is already signed in or not is via using Shared Preferences.
Just add this dependency in your pubspec.yaml file.
Every time a user logs in a particular device you can save some value as a key value pair in the phone's storage using Shared Preferences.
Next time whenever a user opens the app you can just check if any value is available in the shared preferences. If so then open the Home Screen else open the Splash Screen.
This is by far the cleanest solution.
You can read more about shared Preferences here.
If you still need a sample code. Drop a comment and I will edit the answer.
Thanks
you should right function getuser with return statement of widget that contains return Statement of mainScreen if user signed in or else splashscreen and use this function in home:
Widget getUser(){
if (user != null) {
mainScreen();
} else {
splashScreen();
}
}
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Instant Tasker',
theme: theme(),
home:getUser(),
);
}
}
Thank you every one i solved my problem. Everyone on stackoverflow was providing complex solutions but still didn't solve the problem. The mistake everyone was making is they were using Firebase before the 'FirebaseApp initialize'. So i simply call the runApp when the FirebaseApp is initialized.cHere is my code
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp().then((value) {
runApp(MyApp());
});
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
User user = FirebaseAuth.instance.currentUser;
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Instant Tasker',
theme: theme(),
initialRoute: user != null ? MainScreen.routeName : SplashScreen.routeName,
routes: routes,
);
}
}
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.