In my project, when ChangeNotifier class receives a status it sets up a boolean and calls notifyListeners(). In my main build() function, I then check the pending-boolean and display the dialog accordingly - but I am only able to display the dialog by giving it a small delay in the build method - it seems the context is missing.
TL;DR:
Is there any way to display a dialog from within the ChangeNotifier class?
Even if you could do that by just passing a BuildContext, you shouldn't, because you'd be coupling your ChangeNotifier to specific cases only.
Let's say this is your model:
class Model extends ChangeNotifier {
bool _loading = false;
bool get loading => _loading;
void update(bool value) {
_loading = value;
notifyListeners();
}
}
And say, you're updating loading value on a button press using:
final model = Provider.of<Model>(context, listen: false);
model.update(true);
You should perform your logic here itself, or maybe you are listening to this model somewhere else in your project with:
final model = Provider.of<Model>(context);
You then should show dialog by checking:
if (model.loading) {
// show dialog
}
Related
I am new to GetX and want to get some concepts right. This is my process to get the groups that a current user is in using Firebase Realtime Database:
Create an AuthController to get current user id (works perfectly)
class AuthController extends GetxController {
FirebaseAuth auth = FirebaseAuth.instance;
Rx<User?> firebaseUser = Rx<User?>(FirebaseAuth.instance.currentUser);
User? get user => firebaseUser.value;
#override
void onInit() {
firebaseUser.bindStream(auth.authStateChanges());
super.onInit();
}
} ```
Create a UserController to get ids of groups that the user has (working partially)
class FrediUserController extends GetxController {
Rx<List<String>> groupIdList = Rx<List<String>>([]);
List<String> get groupIds => groupIdList.value;
#override
void onInit() {
User? user = Get.find<AuthController>().user;
if (user != null) {
String uid = user.uid;
groupIdList.bindStream(DatabaseManager().userGroupIdsStream(uid));
print(groupIds); //prints [] when it should be populated
}
super.onInit();
}
}
Create a GroupsController to get the groups from those ids (not working) --> Dependant on UserController to have been populated with the id's.
class FrediGroupController extends GetxController {
Rx<List<FrediUserGroup>> groupList = Rx<List<FrediUserGroup>>([]);
List<FrediUserGroup> get groups => groupList.value;
void bindStream() {}
#override
void onInit() {
final c = Get.find<FrediUserController>();
List<String> groupIds = c.groupIds;
print(groupIds); //prints [], when it should have ids
groupList.bindStream(DatabaseManager().groupsStream(groupIds)); //won't load anything without id's
super.onInit();
}
}
Observations
Get.put is called sequentially in the main.dart file:
Get.put(AuthController());
Get.put(FrediUserController());
Get.put(FrediGroupController());
Inside my HomePage() Stateful Widget, if I call the UserController, the data loads correctly:
GetX<FrediUserController>(
builder: (controller) {
List<String> groups = controller.groupIds;
print(groups); //PRINTS the list of correct ids. THE DATA LOADS.
return Expanded(
child: ListView.builder(
shrinkWrap: true,
itemCount: groups.length,
itemBuilder: (context, index) {
return Text('${groups[index]}',
style: TextStyle(color: Colors.white));
},),); },),
QUESTION
It is as if the stream takes some time to populate, but the UserController doesn't wait for it and initializes the controller as empty at first, but after some time it populates (not in time to pass the data to the GroupController.
How can I fix this? I have tried async but not with much luck.
Behaviour I would Like:
Streams may/may not be ready, so it can initialize as empty or not.
HOWEVER, if the stream arrives, everything should be updted, including the initialization of controllers that depend on UserController like GroupController.
Consequently, the UI is rebuilt with new values.
THANK YOU!
There are two things that you can add:
Future Builder to show some loading screen while it fetch data from RTDB
ever function
class AuthController extends GetxController {
late Rx<User?> firebaseuser;
#override
void onReady() {
super.onReady();
firebaseuser = Rx<User?>(FirebaseAuth.instance.currentUser);
firebaseuser.bindStream(FirebaseAuth.instance.idTokenChanges());
ever(firebaseuser, _setInitialScreen);
}
_setInitialScreen(User? user) {
if (user != null) {
//User Logged IN
} else {
//User Logged out
}
}
}
You only take the user once, in the method onInit. You are not getting user changes. To get every change you would have to use "ever" function. For example, "firebaseUser.value" is like a photography of the firebaseUser observable in the moment.
If I can make a sugestion, don't mistake controllers with providers. Think Firebase as a provider and the controller as a mid point between the UI and the provider. You can listen to Firebase Streams at the controller to update UI and make calls from the UI change parameters in your Firebase provider. Separate your concerns into two distinct classes and you'll, potentially, have a better design.
Use of "ever" function example:
ever(firebaseUser, (user) {
// do something
});
"Ever" assigned function runs whenever the observable emits a new value
Structure of code:
I have a function, that on a Button click returns a stateful widget A.
In a separate file, I have a ChangeNotifier class B, which A
needs (it needs the decoded json file) and I use the ChangeNotifier class as a general memory container which all the different widgets have access to.
B:
class Database_Controller extends ChangeNotifier {
Future<void> _readVeryLargeJsonFrame() async {
final String response =
await rootBundle.loadString('assets/tables/largejson.json');
final data = await json.decode(response);
return data;
}
Other functions to give back entries of data
}
Problem:
I would like to execute _readVeryLargeJsonFrame as soon as A is called (potentially with a loading spinner for the user) and before A is loaded (or at least in parallel).
How do I call the ChangeNotifier function in the "init" part of a stateful widget? Peter Koltai mentioned the initState method. But how do I call the ChangeNotifier function from this?
(2. Context problem: So far, I would be using Provider.of<Database_Controller>(context,listen: false)._readVeryLargeJsonFrame(); but how do I get the context argument here?)
(3. Is the Future<void> ... async nature of the _readVeryLargeJsonFrame function a problem here?)
void initState() {
super.initState();
WidgetsBinding.instance
.addPostFrameCallback((_) => yourFunction(context));
}
So let's say I have a Counter class like this
class Counter extends ChangeNotifier {
int _i = 0;
int get myCounter => _i;
void increment() {
_i++;
notifyListeners();
}
void decrement() {
_i--;
notifyListeners();
}
}
I want to write a test file for it, so I expose its instance like this. The problem is, after I expose it, how do I access the instance of the class I just created? Like say, I increased _i value through a button, how will I access the instance that is created by Provider in order to test it?
I was looking to do the same but then I found this answer https://stackoverflow.com/a/67704136/8111212
Basically, you can get the context from a widget, then you can use the context to get the provider state
Btw, you should test a public variable like i instead of _i
Code sample:
testWidgets('showDialog', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(home: Material(child: Container())));
final BuildContext context = tester.element(find.byType(Scaffold)); // It could be final BuildContext context = tester.element(find.byType(Container)) depending on your app
final Counter provider = Provider.of<Counter>(context, listen: false);
expect(provider.i, equals(3));
});
You first initialize the Provider in your main.dart file using
ChangeNotifierProvider
after that you can use the class anywhere in your code by either using the Consumer widget or by using:
final counter = Provider.of<Counter>(context)
Here is a good post/tutorial about how to use Provider
I am using the Flutter provider package to manage all the states and separate the business logic from the UI part, and have all the API call present in the provider class that I need to call every time the user moves to that page.
But the issue is I don't want to hold the data even when the user moves to another screen that is the case when I declare provider in main.dart.
class _HomeScreenAppState extends State<HomeScreenApp> {
bool _isLoading;
int counter = 0;
String seller, user;
#override
void initState() {
_isLoading = true;
super.initState();
}
#override
void didChangeDependencies() {
super.didChangeDependencies();
_fetchHomedetails();
}
Future<void> _fetchHomedetails() async {
await Provider.of<HomeDetailProvider>(context, listen: false)
.getEarnignStatus(context);}
}
I have used ChangeNotifierProvider(create:(context) =>HomeProvider(),
builder:(context) => HomeScreen()
But if there is any dialog (bottomsheet, alertdialog) which is using HomeProvider, the dialog cannot access the HomeProvider data present on its parent widget.
I'm currently trying Provider as a state management solution, and I understand that it can't be used inside the initState function.
All examples that I've seen call a method inside a derived ChangeNotifier class upon user action (user clicks a button, for example), but what if I need to call a method when initialising my state?
Motivation:
Creating a screen which loads assets (async) and shows progress
An example for the ChangeNotifier class (can't call add from initState):
import 'package:flutter/foundation.dart';
class ProgressData extends ChangeNotifier {
double _progress = 0;
double get progress => _progress;
void add(double dProgress) {
_progress += dProgress;
notifyListeners();
}
}
You can call such methods from the constructor of your ChangeNotifier:
class MyNotifier with ChangeNotifier {
MyNotifier() {
someMethod();
}
void someMethod() {
// TODO: do something
}
}
Change your code to this
class ProgressData extends ChangeNotifier {
double _progress = 0;
double get progress => _progress;
void add(double dProgress) async {
// Loading Assets maybe async process with its network call, etc.
_progress += dProgress;
notifyListeners();
}
ProgressData() {
add();
}
}
In initState all the of(context) things don't work correctly, because the widget is not fully wired up with every thing in initState.
You can use this code:
Provider.of<ProgressData>(context, listen: false).add(progress)
Or this code:
Future.delayed(Duration.zero).then(_){
Provider.of<ProgressData>(context).add(progress)
}):
So an AssetLoader class which reports on its progress will look something like this, I guess:
import 'package:flutter/foundation.dart';
class ProgressData extends ChangeNotifier {
double _progress = 0;
ProgressData() {
_loadFake();
}
Future<void> _loadFake() async {
await _delayed(true, Duration(seconds: 1));
_add(1.0);
await _delayed(true, Duration(seconds: 2));
_add(2.0);
await _delayed(true, Duration(seconds: 3));
_add(3.0);
}
// progress
double get progress => _progress;
// add
void _add(double dProgress) {
_progress += dProgress;
notifyListeners();
}
// _delayed
Future<dynamic> _delayed(dynamic returnVal, Duration duration) {
return Future.delayed(duration, () => returnVal);
}
}
As Fateme said:
the widget is not fully wired up with everything in initState
Also, you can use something like this in your initState
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
Provider.of<ProgressData>(context, listen: false).add(5);
});
I think it's more standard!
Be aware that you should use the correct context! I mean the context of the Builder!
The problem here lies with the fact that context does not exist yet in initState as extensively explained by the other answers. It doesn't exist because it hasn't yet been made a part of the widget tree.
Calling a method
If you're not assigning any state and only calling a method then initState would be the best place to get this done.
// The key here is the listen: false
Provider.of<MyProvider>(context, listen: false).mymethod();
The code above is allowed by Flutter because it doesn't have to listen for anything. In short, it's a one off. Use it where you only want to do something instead of read/listen to something.
Listening to changes
Alternatively, if you need to listen to changes from Provider then the use of didChangeDependencies would be the best place to do so as context would exist here as in the docs.
This method is also called immediately after initState.
int? myState;
#override
void didChangeDependencies() {
// No listen: false
myState = Provider.of<MyProvider>(context).data;
super.didChangeDependencies();
}
If you've never used didChangeDependencies before, what it does is get called whenever updateShouldNotify() returns true. This in turn lets any widgets that requested an inherited widget in build() respond as needed.
I'd usually use this method in a FutureBuilder to prevent reloading data when data already exists in Provider after switching screens. This way I can just check Provider for myState and skip the preloader (if any) entirely.
Hope this helps.