Retrieving Runtime Changed ThemeData Problem - flutter

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.

Related

Change notifier provider is not updating the consumer in main

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();
}
}

ChangeNotifierProvider does not update the model

i am quite new with flutter. I am trying to add a ChangeNotifierProvider into my app. I use flutter_azure_b2c to log in a user, in order to handle to login outcome I have the following code:
AzureB2C.registerCallback(B2COperationSource.POLICY_TRIGGER_INTERACTIVE,
(result) async {
if (result.reason == B2COperationState.SUCCESS) {
List<String>? subjects = await AzureB2C.getSubjects();
if (subjects != null && subjects.isNotEmpty) {
B2CAccessToken? token = await AzureB2C.getAccessToken(subjects[0]);
if (!mounted || token == null) return;
final encodedPayload = token.token.split('.')[1];
final payloadData =
utf8.fuse(base64).decode(base64.normalize(encodedPayload));
final claims = Claims.fromJson(jsonDecode(payloadData));
var m = Provider.of<LoginModel>(context);
m.logIn(claims);
}
}
});
The problem is that when it arrives to var m = Provider.of<LoginModel>(context); the execution stops with out errors without executing m.logIn(claims);, so the model is not changed and the consumer is not called.
Any idea?
This is my consumer:
class App extends StatelessWidget {
const App({super.key});
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => LoginModel(),
child: MaterialApp(
debugShowCheckedModeBanner: false,
theme: appTheme,
home: Consumer<LoginModel>(
builder: (context, value, child) =>
value.claims != null ? const Home() : const Login(),
)),
);
}
}
class LoginModel extends ChangeNotifier {
Claims? _claims;
logIn(Claims claims) {
_claims = claims;
notifyListeners();
}
logOut() {
_claims = null;
notifyListeners();
}
Claims? get claims => _claims;
}
My LoginWidget:
class Login extends StatefulWidget {
const Login({super.key});
#override
LoginState createState() => LoginState();
}
class LoginState extends State<Login> {
B2CConfiguration? _configuration;
checkLogin(BuildContext context) async {
List<String>? subjects = await AzureB2C.getSubjects();
if (subjects != null && subjects.isNotEmpty) {
B2CAccessToken? token = await AzureB2C.getAccessToken(subjects[0]);
if (!mounted || token == null) return;
final encodedData = token.token.split('.')[1];
final data =
utf8.fuse(base64).decode(base64.normalize(encodedData));
final claims = Claims.fromJson(jsonDecode(data));
var m = Provider.of<LoginModel>(context, listen: true);
m.logIn(claims); //<-- debugger never reaches this line
}
}
#override
Widget build(BuildContext context) {
// It is possible to register callbacks in order to handle return values
// from asynchronous calls to the plugin
AzureB2C.registerCallback(B2COperationSource.INIT, (result) async {
if (result.reason == B2COperationState.SUCCESS) {
_configuration = await AzureB2C.getConfiguration();
if (!mounted) return;
await checkLogin(context);
}
});
AzureB2C.registerCallback(B2COperationSource.POLICY_TRIGGER_INTERACTIVE,
(result) async {
if (result.reason == B2COperationState.SUCCESS) {
if (!mounted) return;
await checkLogin(context);
}
});
// Important: Remeber to handle redirect states (if you want to support
// the web platform with redirect method) and init the AzureB2C plugin
// before the material app starts.
AzureB2C.handleRedirectFuture().then((_) => AzureB2C.init("auth_config"));
const String assetName = 'assets/images/logo.svg';
final Widget logo = SvgPicture.asset(
assetName,
);
return SafeArea(
child: //omitted,
);
}
}
I opened an issue as well, but it did not help me.
Try this
var m = Provider.of<LoginModel>(context, listen: false)._claims;
You are using the Provider syntax but not doing anything really with it. You need to set it like this Provider.of<LoginModel>(context, listen: false).login(claims) and call it like this Provider.of<LoginModel>(context, listen: false)._claims;
I fixed it, moving the callback registrations from the build method to the initState method.

Creating StreamProvider in flutter app needs correction

I am learning about StreamProviders and ChangeNotifierProvider and how to use them in a flutter app.
The problem I am having is when I create the StreamProvider in main.dart. I am getting this error
Instance member 'getAgencyTrxn' can't be accessed using static access. (Documentation)
as designated by a red line under getAgencyTrxn(). I have been following a tutorial and also some posts here but none of them quite match what I am doing.
How do I fix this error?
Here is what I have so far:
main.dart
Widget build(BuildContext context) {
Provider.debugCheckInvalidValueType = null;
globals.newTrxn = true;
return MultiProvider(
providers: [
ChangeNotifierProvider<TrxnProvider>(create: (context) => TrxnProvider()),
StreamProvider<TrxnProvider>(
create: (context) => TrxnProvider.getAgencyTrxn(),
initialData: []),
],
child: MaterialApp(
debugShowCheckedModeBanner: false,
home: LoginScreen(),
),
);
}
trxn_provider.dart
class TrxnProvider extends ChangeNotifier {
final firestoreService = FirestoreService();
String? _clientFName;
String? _clientLName;
// Getters
String? get clientFName => _clientFName;
String? get clientLName => _clientLName;
// Setters
changeclientFName(String value) {
_clientFName = value;
notifyListeners();
}
changeclientLName(String value) {
_clientLName = value;
notifyListeners();
}
loadValues(QueryDocumentSnapshot trxns) {
_clientFName = trxns['clientFName'];
_clientLName = trxns['clientLName'];
}
getAgencyTrxn() {
return firestoreService.getAgencyTrxns();
}
saveTrxn() {
if (globals.newTrxn == true) {
_trxnId = uuId.v4();
globals.newTrxn = false;
}
var newTrxn = Trxns(
clientFName: clientFName,
clientLName: clientLName);
firestoreService.saveTrxn(newTrxn);
}
deleteTrxn(String trxnId) {
firestoreService.deleteTrxn(trxnId);
}
}
firestore_service.dart
class FirestoreService {
FirebaseFirestore _db = FirebaseFirestore.instance;
Stream<QuerySnapshot> getAgencyTrxns() async* {
yield* FirebaseFirestore.instance
.collection('agency').doc(globals.agencyId)
.collection('trxns')
.where('trxnStatus', isNotEqualTo: 'Closed')
.snapshots();
}
}
I found the solution. I needed to change this
create: (context) => TrxnProvider.getAgencyTrxn()
to this
create: (context) => TrxnProvider().getAgencyTrxn()

How to fectch a value from shared preferences?I have future and await problems

My code here:
//get data
Future<Object> sharedGetData(String key) async{
SharedPreferences prefs=await SharedPreferences.getInstance();
return prefs.get(key);
}
setdata part :
sharedAddData(String key,Object dataType,Object data) async{
SharedPreferences prefs=await SharedPreferences.getInstance();
switch(dataType){
case bool:
prefs.setBool(key, data as bool);break;
case double:
prefs.setDouble(key, data as double);break;
case int:
prefs.setInt(key, data as int);break;
case String:
prefs.setString(key, data as String);break;
case List:
prefs.setStringList(key, data as List<String>);break;
default:
prefs.setString(key, data as String);break;
}
}
And then I added one and want to fetch one value:
//add
sharedAddData(Application.USER_LOGIN, bool, true);
//How to fetch this?
sharedGetData(Application.USER_LOGIN) return a future type,but I want to check its value.
I have tried this way:
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context){
sharedGetData(Application.USER_LOGIN).then((v){
if(v==true){
return MaterialApp(
home: DashboardScreen(),
);
}
else {
return MaterialApp(
home: LoginScreen(),
);
}});
}
}
But it reports that it lacks some return ways.
Could anyone help me? thanks
your sharedGetData returns future so you can only get it with await. Now await needs function to be async which we cannot make build method. so we need to use FutureBuilder.
Try something on this line
class MyWidget extends StatelessWidget {
#override
Widget build(context) {
return FutureBuilder<String>(
future: sharedGetData(Application.USER_LOGIN),
builder: (context, AsyncSnapshot<String> snapshot) {
if (snapshot.hasData) {
if(v==true){
return MaterialApp(
home: DashboardScreen(),
);
}
else {
return MaterialApp(
home: LoginScreen(),
);
} else {
return CircularProgressIndicator();
}
}
);
}
}

How to setup ChangeNotifier for loading locale files

I am new to flutter.
I am building a multi language app.
Before app start it needs to load current locale file.
Then every time user changes the locale it needs to load the new file.
At least in theory, I think this can be done using "ChangeNotifierProvider" , ProxyProvider or something similar.
So I have AppLanguage class load the correct locale file based on language code
class AppLanguage extends ChangeNotifier {
String _appLocale = 'en';
Map<String, String> _localizedStrings;
Map<String, String> get localeData => this._localizedStrings;
Future<bool> getLocaleData() async {
var prefs = await SharedPreferences.getInstance();
if (prefs.getString('language_code') == null) {
_appLocale = 'en';
await prefs.setString('language_code', _appLocale);
} else {
_appLocale = prefs.getString('language_code');
}
String jsonString = await rootBundle.loadString('i18n/$_appLocale.json');
Map<String, dynamic> jsonMap = json.decode(jsonString);
_localizedStrings = jsonMap.map((key, value) {
return MapEntry(key, value.toString());
});
return true;
}
Future<void> changeLanguage(String locale) async {
var prefs = await SharedPreferences.getInstance();
_appLocale = locale;
await prefs.setString('language_code', locale);
notifyListeners();
}
}
getLocaleData() function read the data and changeLanguage change current locale and fires notifyListeners
class Translator {
final Map<String, String> localizedStrings;
Translator(this.localizedStrings);
String translate(String key) {
return localizedStrings[key];
}
}
the Widgets will use translator class to get the correct translations.
The problem I have is, how to wire this up in main. I am stuck at how to setup the providers.
void main() async {
WidgetsFlutterBinding.ensureInitialized();
AppLanguage appLanguage = AppLanguage();
await appLanguage.getLocaleData();
runApp(MyApp(appLanguage: appLanguage));
}
class MyApp extends StatelessWidget {
final AppLanguage appLanguage;
MyApp({this.appLanguage});
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ProxyProvider<AppLanguage, Translator>(
update: (context, appLanguage, trans) =>
Translator(appLanguage.localeData),
),
],
child: MaterialApp(
title: 'Language Demo',
home: MyHomePage(),
),
);
}
}
class MyHomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Hello'),
),
body: Container(),
);
}
}
Can someone kindly provide some help to wire this up?
Or maybe provide a better way of doing this?
A few things, ProxyProvider (or any type of ProxyProvider, ChangeNotifierProxyProvider, etc.) updates its value when the provider it depends on changes too, but you created the AppLanguage in main, without an inject dependency, just like a simple class (it's not really provided in the context) so it would be easier to just use a ChangeNotifierProvider in this case.
There is a parameter called window.locale that return the language the device it's using at that time, at the start of the app you can use it to know the language of the device if you're don't have it the sharedPreference the first time. Advante of this it's that in your example if there is not preference saved it will use the default 'en' for English, but you also support japanase, so if someone has its device in japanese and download your app for the first time it would be nice to use japanese since the beginning.
Future<Locale> _getLocaleData() async {
var prefs = await SharedPreferences.getInstance();
String languageCode = prefs.getString('language_code');
if (languageCode == null) {
return window.locale;
} else {
return Locale(languageCode);
}
}
void main() async {
Locale locale = await _getLocaleData();
runApp(MyApp(
appLanguage: locale,
));
}
class MyApp extends StatelessWidget {
final Locale language;
MyApp({this.language});
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider<AppLanguage>(
builder: (_) => AppLanguage(language),
child: Consumer<AppLanguage>(builder: (context, model, _) {
return MaterialApp(
locale: model.appLocal,
supportedLocales: [
Locale('en', 'US'),
Locale('ja', ''),
],
localizationsDelegates: [
AppLocalizations.delegate, //create your AppLocalizations just like the article you shared
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
home: MyWidget(),
);
}),
);
}
}
class AppLanguage extends ChangeNotifier {
AppLanguage(Locale locale) : _appLocale = locale;
Locale _appLocale;
Locale get appLocal => _appLocale;
Future<void> changeLanguage(String locale) async {
var prefs = await SharedPreferences.getInstance();
_appLocale = Locale(locale);
await prefs.setString('language_code', locale);
notifyListeners();
}
}