Provider does not update a variable while fetching data - flutter

Firstly, I have a login screen and I want to change button with CircularProgressIndicator when button is clicked. I am using Provider to handle state management. Here is my Provider class.
class UserProvider with ChangeNotifier {
bool _loading = false;
bool get loading => _loading;
UserViewModel? user;
bool isLoggedOut = false;
TokenRequestModel tokenRequestModel = TokenRequestModel(
username: '', password: '', environment: '', deviceToken: '');
login(TokenRequestModel requestModel) async {
_loading = true;
user = await UserService().login(requestModel);
_loading = false;
notifyListeners();
}
}
I change loading value between my Api call. Then I want to catch it in my login screen to change widget. With that way I prevent button clicked multiple times and my login screen looks like this.
Column(
children: [
//textfield widgets
context.watch<UserProvider>().loading == false
? button(context, userProvider)
: Container(
height: 35.h,
width: 280.w,
decoration: BoxDecoration(color: blueLogo),
child: const CircularProgressIndicator(
color: Colors.white,
),
)
],
),
I have tried multiple methods like Consumer, userProvider = Provider.of<UserProvider>(context); but nothing happens. What am I missing or what is the mistake of my method to handle state management? Would you help me to find right way to do? Thanks

Seems to me that you are notifying listeners when _loading has gone back to being false again, but you never notified them when it turned true!
Try this, in your provider file:
class UserProvider extends ChangeNotifier {
bool _loading = false;
bool get loading => _loading;
UserViewModel? user;
bool isLoggedOut = false;
TokenRequestModel tokenRequestModel = TokenRequestModel(
username: '', password: '', environment: '', deviceToken: '');
login(TokenRequestModel requestModel) async {
_loading = true;
notifyListeners(); // <- The crucial line, right here!
user = await UserService().login(requestModel);
_loading = false;
notifyListeners();
}
}
I noticed you had used with ChangeNotifier instead of extends ChangeNotifier... but I don't know what that does! I always use extends ChangeNotifier and that works for me.

Related

Automatically set State of Button WITHOUT pressing it

I have got a State Management Problem I couldn't get rid of and I want to reach out to you.
Basically, I activate with the Buttons a game and I am sending a String to the uC. The uC does its stuff and sends a response to Flutter including gameFinished=true (that works).
Now I want to reset the State of the Button to the init state WITHOUT pressing the Button. Following are some things I tried that didn't work.
#override
void initState() {
super.initState();
setState(() {
gameAktivated = false;
gameStarted = false;
});
}
void asyncSetState() async {
setState(() async {
gameAktivated = false;
gameStarted = false;
});
}
I am changing the style from "Start" to "Stop" when the Button is pressed and I send Data to the uC. (Works)
Edit: Ofc I have a second button that triggers gameAktivated=true :)
ElevatedButton(
onPressed: () {
if (gameAktivated) {
setState(() {
gameStarted = !gameStarted;
});
if (gameStarted) {
//Send Data to uC
} else if (!gameStarted) {
//Send Data to uC
}
}
},
child:
!gameStarted ? const Text('Start') : const Text('Stop'),
),
Button Displays Stop now.
Following I am receiving a String from the uC that I jsonEncode and I receive gameFinished=true. (Works)
Container(
child: streamInit
? StreamBuilder<List<int>>(
stream: stream,
builder: (BuildContext context,
AsyncSnapshot<List<int>> snapshot) {
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
}
if (snapshot.connectionState ==ConnectionState.active) {
// getting data from Bluetooth
var currentValue =const BluetoothConnection().dataParser(snapshot.data);
config.jsonDeserializeGameFinished(currentValue);
if(config.gameFinished){
setState(() {
gameAktivated = false;
gameStarted = false;
});
asyncSetState();//Tested both methods seperate!
}
return Column(
children: [
Text(config.time.toString()),
],
);
} else {
return const Text(
'Check the stream',
textAlign: TextAlign.center,
);
}
},
): const Text("NaN",textAlign: TextAlign.center,),
),
When I try to reset the state like in the code above this error occures:
Calling setState Async didnt work for me either.
Where and how can I set the state based on the response from the uC?
Is it possible without using Provider Lib?
Thanks in advance Manuel.
Actually this error is not about the changing the state of button. Its a common mistake to update the widget state when its still building the widget tree.
Inside your StreamBuilder, you are trying to update the state before creating the UI which is raising this issue.
if(config.gameFinished){
setState(() {
gameAktivated = false;
gameStarted = false;
});
This will interrupt the build process of StreamBuilder as it will start updating the whole page. You need to move it out of the StreamBuilder's builder method.
To do that simply convert your stream to a broadcast, which will allow you to listen your stream multiple time.
var controller = StreamController<String>.broadcast();
Then inside the initState of the page you can setup a listener method to listen the changes like this
stream.listen((val) => setState((){
number = val;
}));
Here you can change the state values because from here it will not interrupt the widget tree building cycle.
For more details see this example I created
https://dartpad.dev/?id=a7986c44180ef0cb6555405ec25b482d
If you want to call setState() immediately after the build method was called you should use:
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
// this method gets called once directly after the previous setState() finishes.
});
Answer to my own Question:
In initState()
added this:
stream.listen((event) {
String valueJSON = const BluetoothConnection().dataParser(event);
config.jsonDeserializeGameFinished(valueJSON);
if (config.gameFinished) {
setState(() {
gameAktivated = false;
gameStarted = false;
});
}
});
The Code above listens to the stream, UTF-8 Decodes and JSON-Decodes the data. After this you can access the variable to set a state.

User state managment with firebase and provider

My goal is to provide all the data of the logged in user throughout the app using provider and use it to customize the UI based on the user.
The problem is that the data takes time to arrive from Firestore and at the start of the application the provider is not able to provide the widgets of the home screen, so I get the next error:
Exception has occurred.
_CastError (Null check operator used on a null value)
When I click "continue" in debug options then the interface get the data from the user correctly and all works fine, so I understand that I need a way to wait the data to be available.
I know that the returns of type Future have methods to deal with the asynchronous response from Firestore, but in order to use provider I have "synchronized" that data using try{}catch(e){} as shown in the code.
class CurrentUserProvider extends ChangeNotifier {
UserModel _currentUser = UserModel();
UserModel get getCurrentUser => _currentUser;
void updateStateFromFirebase(String uid) async {
try {
_currentUser = await OurDatabase().getUserInfo(uid);
notifyListeners();
} catch (e) {
print(e);
}
}
}
getUserInfo(uid){} is the async function that download the current user data from firestore using the uid provided by Authentification Firebase after the user logged in.
To my knowledge this implies that _currentUser is not going to be async anymore, so I cannot create an alternative to represent something on the screen while the data is arriving.
This is the code where I receive the data from the provider and try to render _currentUser.uid as text on the screen.
import 'package:beeteam/providers/currentUserProvider.dart';
import 'package:flutter/material.dart';
import 'package:beeteam/models/user_model.dart';
import 'package:provider/provider.dart';
class MyTeams extends StatefulWidget {
#override
State<MyTeams> createState() => _MyTeamsState();
}
class _MyTeamsState extends State<MyTeams> {
#override
Widget build(BuildContext context) {
UserModel? _currentUser =
Provider.of<CurrentUserProvider>(context, listen: true).getCurrentUser;
return Scaffold(
body: Container(
margin: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 20),
child: Text(_currentUser.uid!),
//Text(_currentUser.uid!),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
backgroundColor: Color.fromARGB(255, 190, 158, 62),
onPressed: () {
Navigator.of(context).pushNamed('/join_team_screen');
}));
}
I got an error if I dont code ! at the end of _currentUser.uid.
This is the UserModel code.
class UserModel {
String? uid;
String? email;
String? firstName;
String? secondName;
String? userName;
String? teamSelected;
List<String>? teamsMemberUid;
List<String>? notifications;
UserModel(
{this.uid,
this.email,
this.firstName,
this.secondName,
this.userName,
this.teamSelected,
this.teamsMemberUid,
this.notifications});
Do you have any idea how to solve this problem?
Make currentUser nullable allowing null value before loading is finished.
class CurrentUserProvider extends ChangeNotifier {
UserModel? _currentUser;
UserModel? get currentUser => _currentUser;
void updateStateFromFirebase(String uid) async {
try {
_currentUser = await OurDatabase().getUserInfo(uid);
notifyListeners();
} catch (e) {
print(e);
}
}
}
Show loading state while currentUser is null.
class MyTeams extends StatelessWidget {
#override
Widget build(BuildContext context) {
UserModel? currentUser =
Provider.of<CurrentUserProvider>(context, listen: true).currentUser;
return Scaffold(
body: currentUser == null
? const Center(child: CircularProgressIndicator())
: Container(
margin:
const EdgeInsets.symmetric(horizontal: 10.0, vertical: 20),
child: Text(currentUser.uid!),
),
...
}
}

I am not able to save retrieve data using shared preferences in Flutter

I am using flutter localizations for changing language in my flutter app. I want to change my app's language in real time and have implemented logic for that. Now, I want that when user closes app and restarts it, he gets same language he chose before, i.e. language should not set back to default after user closes the app. For this purpose, I was using shared preferences to save the code of language that user selected and then retrieve it in the beginning of the app.
app_locale.dart -
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
class AppLocale extends ChangeNotifier {
Locale? _locale;
Locale get locale => _locale ?? Locale('en');
void getLocale() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String code = prefs.getString("code")??"en";
Locale newLocale = Locale(code);
if(newLocale == Locale('bn')) {
_locale = Locale('bn');
} else if(newLocale==Locale('gu')){
_locale = Locale('gu');
} else if(newLocale==Locale('en')){
_locale = Locale('en');
} else if(newLocale==Locale('pa')){
_locale = Locale('pa');
}
}
void changeLocale(Locale newLocale) async {
if(newLocale == Locale('bn')) {
_locale = Locale('bn');
} else if(newLocale==Locale('gu')){
_locale = Locale('gu');
} else if(newLocale==Locale('en')){
_locale = Locale('en');
} else if(newLocale==Locale('pa')){
_locale = Locale('pa');
}
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setString("code", _locale?.countryCode??"en");
notifyListeners();
}
}
I am calling getLocale in main.dart -
class MyApp extends StatelessWidget {
GlobalKey<ScaffoldMessengerState> scaffoldMessengerKey=GlobalKey<ScaffoldMessengerState>();
Locale? defaultLanguage;
#override
Widget build(BuildContext context) {
var language = Provider.of<AppLocale>(context);
language.getLocale();
return Consumer<AppLocale>(
........
........
And in my language selection screen, I am changing language like this -
var language = Provider.of<AppLocale>(context);
child: Column(
children: [
LanguageTile(
shortForm: "Pa",
fullName: "ਪੰਜਾਬੀ",
isSelected: selectedLanguage==0,
onTap: () {
changeSelectedLanguage(0);
language.changeLocale(Locale('pa'));
},
),
LanguageTile(
shortForm: "GU",
fullName: "ગુજરાતી",
isSelected: selectedLanguage==1,
onTap: () {
changeSelectedLanguage(1);
language.changeLocale(Locale('gu'));
},
),
LanguageTile(
shortForm: "বা",
fullName: "বাংলা",
isSelected: selectedLanguage==2,
onTap: () {
changeSelectedLanguage(2);
language.changeLocale(Locale('bn'));
},
),
LanguageTile(
shortForm: "A",
fullName: "English",
isSelected: selectedLanguage==3,
onTap: () {
changeSelectedLanguage(3);
language.changeLocale(Locale('en'));
},
),
//Text(AppLocalizations.of(context)!.helloWorld),
],
),
Please someone guide me for this.
Heading
You have to call the get language method in initState. Or show a loading or pop up while the data is loading in background. Sometimes it happens because data is not loaded yet and build context already create the screen and the ui. I hope this will work.
the data is not coming because when loading data from
SharedPreferences it take time.so method is not in void it in Future.
please paste the below code
Future getLocale() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String code = prefs.getString("code")??"en";
Locale newLocale = Locale(code);
if(newLocale == Locale('bn')) {
_locale = Locale('bn');
} else if(newLocale==Locale('gu')){
_locale = Locale('gu');
} else if(newLocale==Locale('en')){
_locale = Locale('en');
} else if(newLocale==Locale('pa')){
_locale = Locale('pa');
}
}
class MyApp extends StatelessWidget {
GlobalKey<ScaffoldMessengerState> scaffoldMessengerKey=GlobalKey<ScaffoldMessengerState>();
Locale? defaultLanguage;
var language;
void initmethod(context)async
{
language = await Provider.of<AppLocale>(context).getLocale();
}
#override
Widget build(BuildContext context) {
initmethod(context);
return Consumer<AppLocale>(
........
........
why yout dont use ListView().builder or Grid().builder to habe less code and use a list with all entries and les
child: ListView.builder(
itemCount: list.length,
itemBuilder: (context, i){
return LanguageTile(
shortForm: list[i].shortForm,
fullName: list[i].fullName,
onSelected: (value) {
changeSelectedLanguage(value);
language.changeLocale(Locale('en'));
},
),
}
},
),

Go back to login when logged out from the drawer, no matter what

I need to redirect user to login page when he clicks on logout button from drawer (wherever he is). The problem is that when I click on the logout button, the screen remains the same.
According to this post: Flutter provider state management, logout concept
I have:
void main() async {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider<Profile>(
create: (final BuildContext context) {
return Profile();
},
)
],
child: MyApp(),
),
);
}
MyApp:
class _MyAppState extends State<MyApp> {
#override
void initState() {
super.initState();
initPlatformState();
}
/// Platform messages are asynchronous, so we initialize in an async method.
Future<void> initPlatformState() async {
if (!mounted) return;
}
#override
Widget build(BuildContext context) {
return new MaterialApp(
initialRoute: '/',
navigatorKey: navigatorKey,
// ...
home: Consumer<Profile>(
builder: (context, profile, child){
return profile.isAuthenticated ? SplashScreen() : AuthScreen();
}
)
);
}
}
The part of the drawer where there is the logout button:
ListTile(
leading: Icon(Icons.logout),
title: Text(AppLocalizations.of(context)!.logout),
onTap: () async {
SharedPreferences preferences =
await SharedPreferences.getInstance();
await preferences.clear();
final Profile profile =
Provider.of<Profile>(context, listen: false);
profile.isAuthenticated = false;
}),
As I said, when I click on the logout button from the drawer, the user is correctly logged out, but the screen remains the same.
UPDATE
This is the profile class:
class Profile with ChangeNotifier {
bool _isAuthenticated = false;
bool get isAuthenticated {
return this._isAuthenticated;
}
set isAuthenticated(bool newVal) {
this._isAuthenticated = newVal;
this.notifyListeners();
}
}
I think you are using provider class incorrectly.
use your profile class like this.
class Profile with ChangeNotifier {
bool _isAuthenticated = true;
bool get getIsAuthenticated => _isAuthenticated;
set setIsAuthenticated(bool isAuthenticated) {
_isAuthenticated = isAuthenticated;
notifyListeners();//you must call this method to inform lisners
}
}
in set method call notifyListners();
in your listTile
replace profile.isAuthenticated = false to profile.isAuthenticated = false;
Always use getters and setters for best practice.
I hope this is what you were looking for.
Add Navigator.of(context).pushReplacementNamed("/routeName") in LogOut onTap() Section.
For more information : https://api.flutter.dev/flutter/widgets/Navigator/pushReplacementNamed.html
Make sure to have logout route set in MyApp file, and i'd edit logout button file as such:
ListTile(
leading: Icon(Icons.logout),
title: Text(AppLocalizations.of(context)!.logout),
onTap: () async {
SharedPreferences preferences =
await SharedPreferences.getInstance();
await preferences.clear();
final Profile profile =
Provider.of<Profile>(context, listen: false);
profile.isAuthenticated = false;
// add login file route here using Navigator.pushReplacementNamed() ;
}),
Navigator push named -> logout route?

Flutter: Remove ChangeNotifier Boilerplate `notifyListeners()`

Is there something like #annotation to remove the boilerplate when creating a model class that extend ChangeNotifier?
Boilerplate
class MyModel extends ChangeNotifier{
bool _isLoading = true;
bool get isLoading => _isLoading;
set isLoading(bool value) {
_isLoading = value;
notifyListeners();
}
}
Using something like #annotation
class MyModel extends ChangeNotifier{
#Notify
bool _isLoading = true;
}
You can try Get for your state management: https://pub.dev/packages/get
Reactive programming with Get is as easy as using setState.
First you can create a new controller:
import 'package:get/get.dart';
class FormController extends GetxController {
FormController();
Rx<bool> _isLoading = false.obs;
set setLoading(value) => this._isLoading.value = value;
bool get loading => this._isLoading.value;
}
And in the UI, when you want to show that value and update the screen whenever the values changes, simply do this:
Obx(
() => formController.loading
? Center(
child: CircularProgressIndicator(),
)
: ElevatedButton(
onPressed: formController.googleLoading ||
formController.anonymousLoading
? null
: () async {
formController.setLoading = true;
if (_formKey.currentState!.validate()) {
await userController
.loginWithEmailAndPassword(
formController.email,
formController.password);
}
formController.setLoading = false;
},
child: Text('login'.tr),
),
),
See an more in-depth explanation of state management here. There you will see more examples and also the difference between the simple state manager and the reactive state manager
You will get a good idea of GetX power.