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();
});
}
...
}
Related
I am trying to set the theme of my app on the response of login data after getting the role but my theme is not updating as per expectation. this is how my main() looks. my code is showing no error and I tried to debug nothing seems wrong.
Widget build(BuildContext context) {
return ChangeNotifierProvider<ThemeModel>(
create: (_) => ThemeModel(),
child: Consumer<ThemeModel>(
builder: (context, ThemeModel themeNotifier, child) {
return Sizer(builder: (context, orientation, deviceType) {
return MaterialApp(
theme: themeNotifier.theme == 'consultant'
? counsultantApptheme()
: themeNotifier.theme == 'rmo'
? rmoApptheme()
: counsultantApptheme(),
navigatorKey: navigatorKey,
debugShowCheckedModeBanner: false,
initialRoute: startroute.toString(),
routes: routes,
);
});
}));
and this how I am updating after response of login API
if (snapshot.data!.data!.consultantYN == 'Y') {
Provider.of<ThemeModel>(context, listen: false).theme =
'consultant';
} else {
Provider.of<ThemeModel>(context, listen: false).theme = 'rmo';
}
and this is my function where I am setting theme and calling notifyListeners() in class extends by ChangeNotifier
//theme_model.dart
import 'package:flutter/material.dart';
import 'package:nmc/widgets/theme_config/theme_preference.dart';
class ThemeModel extends ChangeNotifier {
late String _theme;
late ThemePreferences _preferences;
String get theme => _theme;
ThemeModel() {
_theme = 'default';
_preferences = ThemePreferences();
getPreferences();
}
//Switching themes in the flutter apps - Flutterant
set theme(String value) {
_theme = value;
_preferences.setTheme(value);
notifyListeners();
}
getPreferences() async {
_theme = await _preferences.getTheme();
notifyListeners();
}
}
How can I access a variable in main.dart to other pages in flutter with Getx state management, Here I want to make the localMemberid in main.dart as Global to access from anywhere or pass it to other pages and is it the right way to use secure storage for storing the data
main.dart
void main() {
SecureStorage secureStorage = SecureStorage();
var localMemberid; // i would like to make this varial global or pass this value to other pages
runApp(
ScreenUtilInit(
builder: (BuildContext context, Widget? child) {
return GetMaterialApp(
title: "onyx",
initialRoute: AppPages.INITIAL,
getPages: AppPages.routes,
theme: ThemeData(primarySwatch: MaterialColor(0xFF0456E5, color)),
);
},
),
);
SecureStorage.readLocalSecureData('memberid')
.then((value) => localMemberid = value);
}
Login Controller
class LoginController extends GetxController {
final AuthenticationRepo _authRepe = AuthenticationRepo();
final SecureStorage secureStorage = SecureStorage();
String? localMemberid; // i would like to get the localMemberid from the main.dart
//TODO: Implement LoginController
#override
void onInit() {
super.onInit();
}
#override
void onReady() {
super.onReady();
}
#override
void onClose() {}
var userid;
var password;
onSinginButton() async {
var res = await _authRepe.login(username: userid, password: password);
if (res.status == ApiResponseStatus.completed) {
print(res.data);
await SecureStorage.writeLocalSecureData('memberid', res.data!.memberid);
localMemberid == null
? Get.toNamed(Routes.LOGIN)
: Get.toNamed(Routes.HOME);
} else {
Get.defaultDialog(title: res.message.toString());
}
}
}
Uplift your variable from the main function and make it Rx:
var localMemberid=Rxn<String>(); // i would like to make this varial global or pass this value to other pages
void main() {
SecureStorage secureStorage = SecureStorage();
.......
SecureStorage.readLocalSecureData('memberid')
.then((value) => localMemberid.value = value);
}
And then on your LoginController remove String? localMemberid; // and import main.dart:
localMemberid.value == null
? Get.toNamed(Routes.LOGIN)
: Get.toNamed(Routes.HOME);
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';
}
}
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.
When using shared_preferences on flutter in main.dart in order to change the initialRoute depending on if user have seen the first page or if user is logged in I am getting the boolean which is created throughout the app and added to shared_preferences, every time I start app, I get the initialRoute string correct when debugging, but I still end up getting on the first page, regardless the conditions.
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:developer';
import './pages/registration.dart';
import './pages/login_page.dart';
import './pages/confirmation.dart';
import './pages/lang_page.dart';
import './pages/main_page.dart';
import './pages/user_data.dart';
import './provider/provider.dart';
void main() => runApp(CallInfoApp());
class CallInfoApp extends StatefulWidget {
#override
_CallInfoAppState createState() => _CallInfoAppState();
}
class _CallInfoAppState extends State<CallInfoApp> {
SharedPreferences prefs;
void getSPInstance() async {
prefs = await SharedPreferences.getInstance();
}
dynamic langChosen;
dynamic isLoggedIn;
String initialRoute;
void dataGetter() async {
await getSPInstance();
setState(() {
langChosen = prefs.getBool('langChosen');
// print(langChosen);
isLoggedIn = prefs.getBool('isLoggedIn');
});
}
void getRoute() async {
await dataGetter();
debugger();
if (langChosen == true && isLoggedIn != true) {
setState(() {
initialRoute = '/login_page';
});
} else if (isLoggedIn == true) {
initialRoute = '/main_page';
} else {
setState(() {
initialRoute = '/';
});
}
}
#override
void initState() {
super.initState();
debugger();
getRoute();
}
#override
Widget build(BuildContext context) {
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
]);
return ChangeNotifierProvider<AppData>(
create: (context) => AppData(),
child: MaterialApp(
title: 'Call-INFO',
theme: ThemeData(
primarySwatch: Colors.blue,
),
initialRoute: initialRoute,
routes: {
'/': (context) => LanguagePage(),
'/registration_page': (context) => RegistrationPage(),
'/login_page': (context) => LoginPage(),
'/confirmation_page': (context) => ConfirmationPage(),
'/user_data_page': (context) => UserDataPage(),
'/main_page': (context) => MainPage(),
},
),
);
}
}
Since SharedPreference.getInstance() is an async function it will need some time until the instance is available. If you want to use it for initial route you have to make your main function async and preload it there before your MaterialApp is build.
SharedPreference prefs; //make global variable, not best practice
void main() async {
prefs = await SharedPreference.getInstance();
runApp(CallInfoApp());
}
And remove getSPInstance() from dataGetter
Also keep in midn that prefs.getBool('langChosen') will return null and not false if no entry is made into shared preference so use
langChosen = prefs.getBool('langChosen')??false;
isLoggedIn = prefs.getBool('isLoggedIn')??false;
While this solution will work it's not really good practice. I would recommend to have the initialRoute fixed to a splash screen and handle forwarding to the right page from there. A simple splash screen could look like that:
class SplashScreen extends StatefulWidget {
#override
_SplashScreenState createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> {
#override
Widget build(BuildContext context) {
return Scaffold(body: Center(child: CircularProgressIndicator()));
}
#override
void initState() {
initSplash();
super.initState();
}
Future<void> initSplash() async {
final prefs = await SharedPreferences.getInstance();
final langChosen = prefs.getBool("lang_chosen") ?? false;
final isLoggedIn = prefs.getBool("logged_in") ?? false;
if (langChosen == true && isLoggedIn != true) {
Navigator.of(context).pushReplacementNamed('/login_page');
} else if (isLoggedIn == true) {
Navigator.of(context).pushReplacementNamed('/main_page');
} else {
Navigator.of(context).pushReplacementNamed('/');
}
}
}
Use initState to derive the data your logic is based on (i.e. fetching shared pref info). And use await keyword so that program will wait until the data is fetched from SharedPrefs. Adding the following code to class _CallInfoAppState should help
#override
void initState() {
super.initState();
dataGetter();
}
void dataGetter() async {
await getSPInstance();
setState(() {
langChosen = prefs.getBool('langChosen');
isLoggedIn = prefs.getBool('isLoggedIn');
});
}