Getx theme management - flutter

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';
}
}

Related

Init values in class extending ChangeNotifier

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.

Setting theme based on SharedPreference data

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.

Late initialization of a Theme in Flutter

I'm trying to implement dark and light themes in my Flutter app. To do this, I'm using a ViewModel approach to notify the whole application when the theme changes. When a user changes the theme, I'm saving it using shared_preferences. When the application starts again, I'm loading the saved theme from shared preferences:
main.dart
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Consumer<ThemeViewModel>(
builder: (context, themeViewModel, _) => MaterialApp(
theme: themeViewModel.getTheme(),
...
theme_view_model
class ThemeViewModel extends ChangeNotifier {
final darkTheme = ThemeData(...;
final lightTheme = ThemeData(...);
late ThemeData _themeData;
ThemeData getTheme() => _themeData;
ThemeViewModel() {
StorageManager.readData('themeMode').then((value) {
var themeMode = value ?? 'light';
if (themeMode == 'light') {
_themeData = lightTheme;
} else {
_themeData = darkTheme;
}
notifyListeners();
});
}
...
}
However, when I start the app, I get the error screen for a few seconds (probably before the theme data is loaded from the shared preferences):
How can this be solved? How could I display e.g. a loading spinner until the theme is loaded?
There are a few ways to solve it.
1-You can define an async initialize method for your ThemeViewModel and wait for it in your main method.
void main() async {
final viewModel = ThemeViewModel();
await viewModel.init();
...
}
class ThemeViewModel extends ChangeNotifier {
final darkTheme = ThemeData(...;
final lightTheme = ThemeData(...);
late ThemeData _themeData;
ThemeData getTheme() => _themeData;
Future init() async {
themeMode = await StorageManager.readData('themeMode') ?? 'light';
if (themeMode == 'light') {
_themeData = lightTheme;
} else {
_themeData = darkTheme;
}
}
}
2-You can provide a default theme to use when the _themeData is null
class ThemeViewModel extends ChangeNotifier {
final darkTheme = ThemeData(...;
final lightTheme = ThemeData(...);
ThemeData? _themeData;
ThemeData getTheme() => _themeData ?? lightTheme;
ThemeViewModel() {
StorageManager.readData('themeMode').then((value) {
var themeMode = value ?? 'light';
if (themeMode == 'light') {
_themeData = lightTheme;
} else {
_themeData = darkTheme;
}
notifyListeners();
});
}
...
}

Navigation in flutter without context

I created a service folder and made a file in it called request. dart, here I intend to place all requests I make into a class called AuthService, with the login request below I want to be able to navigate to the home screen once response.statusCode == 200 or 201 but I am unable to do that because navigation requires a context and my class is neither a Stateful nor Stateless widget, is there any way I can navigate without the context??
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter/material.dart';
class AuthService {
login(email, password) async {
SharedPreferences sharedPreferences = await SharedPreferences.getInstance();
if (email == "" && password == "") {
return;
}
try {
Map data = {'email': email, 'password': password};
var jsonResponse;
var response = await http
.post('https://imyLink.com/authenticate', body: data);
if (response.statusCode == 200 || response.statusCode == 201) {
//I want to navigate to my home screen once the request made is successful
jsonResponse = json.decode(response.body);
if (jsonResponse != null) {
await sharedPreferences.setString("userToken", jsonResponse["token"]);
var token = sharedPreferences.getString("userToken");
print('Token: $token');
print(jsonResponse);
print("Login successful");
}
} else {
print(response.statusCode);
print('Login Unsuccessful');
print(response.body);
}
} catch (e) {
print(e);
}
}
First, create a class
import 'package:flutter/material.dart';
class NavigationService{
GlobalKey<NavigatorState> navigationKey;
static NavigationService instance = NavigationService();
NavigationService(){
navigationKey = GlobalKey<NavigatorState>();
}
Future<dynamic> navigateToReplacement(String _rn){
return navigationKey.currentState.pushReplacementNamed(_rn);
}
Future<dynamic> navigateTo(String _rn){
return navigationKey.currentState.pushNamed(_rn);
}
Future<dynamic> navigateToRoute(MaterialPageRoute _rn){
return navigationKey.currentState.push(_rn);
}
goback(){
return navigationKey.currentState.pop();
}
}
In your main.dart file.
MaterialApp(
navigatorKey: NavigationService.instance.navigationKey,
initialRoute: "login",
routes: {
"login":(BuildContext context) =>Login(),
"register":(BuildContext context) =>Register(),
"home":(BuildContext context) => Home(),
},
);
Then you can call the function from anywhere in your project like...
NavigationService.instance.navigateToReplacement("home");
NavigationService.instance.navigateTo("home");
OPTION 1
If you will be calling the login method in either a Stateful or Stateless widget. You can pass context as a parameter to the login method of your AuthService class.
I added a demo using your code as an example:
class AuthService {
// pass context as a parameter
login(email, password, context) async {
SharedPreferences sharedPreferences = await SharedPreferences.getInstance();
if (email == "" && password == "") {
return;
}
try {
Map data = {'email': email, 'password': password};
var jsonResponse;
var response = await http
.post('https://imyLink.com/authenticate', body: data);
if (response.statusCode == 200 || response.statusCode == 201) {
//I want to navigate to my home screen once the request made is successful
Navigator.of(context).push(YOUR_ROUTE); // new line
jsonResponse = json.decode(response.body);
if (jsonResponse != null) {
await sharedPreferences.setString("userToken", jsonResponse["token"]);
var token = sharedPreferences.getString("userToken");
print('Token: $token');
print(jsonResponse);
print("Login successful");
}
} else {
print(response.statusCode);
print('Login Unsuccessful');
print(response.body);
}
} catch (e) {
print(e);
}
}
OPTION 2
You can access your app's Navigator without a context by setting the navigatorKey property of your MaterialApp:
/// A key to use when building the [Navigator].
///
/// If a [navigatorKey] is specified, the [Navigator] can be directly
/// manipulated without first obtaining it from a [BuildContext] via
/// [Navigator.of]: from the [navigatorKey], use the [GlobalKey.currentState]
/// getter.
///
/// If this is changed, a new [Navigator] will be created, losing all the
/// application state in the process; in that case, the [navigatorObservers]
/// must also be changed, since the previous observers will be attached to the
/// previous navigator.
final GlobalKey<NavigatorState> navigatorKey;
Create the key:
final GlobalKey<NavigatorState> navigatorKey = new GlobalKey<NavigatorState>();
Pass it to MaterialApp:
new MaterialApp(
title: 'MyApp',
navigatorKey: key,
);
Push routes (both named and non-named routes work):
navigatorKey.currentState.pushNamed('/someRoute');
Find more details about option 2 by following the github issue below: https://github.com/brianegan/flutter_redux/issues/5#issuecomment-361215074
You can use flutter Get package.
Here is link.
you can use this plugin to skip the required context
https://pub.dev/packages/one_context
// go to second page using named route
OneContext().pushNamed('/second');
// go to second page using MaterialPageRoute
OneContext().push(MaterialPageRoute(builder: (_) => SecondPage()));
// go back from second page
OneContext().pop();
Is there a way to use S.R Keshav method to access pages and giving them an argument ?
routes: {
"sce": (BuildContext context, {args}) => MatchConversation(args as int),
"passport": (BuildContext context, {dynamic args}) => Passport(),
},
It looks that the arg is lost when Navigator goes in _pushEntry method. The navigated Page is accessed, but no initial arguments are loaded.
Simple and clean solution without any plugin/package.
Create global variable:
final GlobalKey<NavigatorState> navKey = GlobalKey<NavigatorState>();
Add this global key to the MaterialApp:
child: MaterialApp(
title: 'MyApp',
navigatorKey: navKey,
));
Now you have 2 ways to use it. Either define routes and use route names or use non-named route (this is the only way if you do not want to use global variables and pass parameters directly to a widget).
a) Option 1. Define routes and then use route names:
// Define route names
MaterialApp(
title: 'MyApp',
navigatorKey: navKey,
routes: {
"login": (BuildContext context) => LoginPage(),
"register": (BuildContext context) => RegisterPage(),
);
// Now anywhere inside your code change widget like this without context:
navKey.currentState?.pushNamed('login');
b) Option 2. Push non-named routes to the navigator:
navKey.currentState?.push(MaterialPageRoute(builder: (_) => LoginPage()));
This way allows to pass parameters directly to widget without global variable:
navKey.currentState?.push(MaterialPageRoute(builder: (_) => HomePage('yourStringValue', 32)));

How to set Provider's data to data that stored in SharedPreferences in Flutter?

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);
// ...
}