How to combine multiple providers in Flutter - flutter

I am new to Flutter development.
I am building application where once users login they are shown list of posts.
If the user is not login still they are shown some random post.
I got parts of the application various posts in the internet.
This is what I did
class TestApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
Provider<AuthBase>(
create: (context) => Auth(),
),
StreamProvider<User>.value(
value: Auth().onAuthStateChanged,
),
ProxyProvider<User, Database>(
update: (BuildContext context, User user, Database db) {
return user == null
? FirestoreDatabase(uid: null)
: FirestoreDatabase(uid: user.uid);
}),
ChangeNotifierProxyProvider<Database, PostProvider>(
create: (context) => PostProvider(),
update: (ctx, database, previousProvider) {
print("at ChangeNotifierProxyProvider $previousProvider");
return previousProvider.update(database);
},
),
],
child: MaterialApp(
title: 'Test Project',
home: LandingPage(),
),
);
}
My idea is;
if the user is logged in StreamProvider<User>.value will give that user to
ProxyProvider<User, Database> and it will create database with that user.
And that database in turn will be used by ChangeNotifierProxyProvider<Database, PostProvider> to create the provider that actually get posts. It uses the database to get the posts.
I noticed even if the user is logged in at the start I get a null value for the user then immediately i get the actual user.
In the landing page, I only have the following line and it generate an error
final provider = Provider.of<PostProvider>(context);
I noticed the cause for the error was this line;
previousProvider.update(database);
First time time Postprovider constructor and update methods get called without any problem. (here we get null user)
When the FirestoreDatabase get created with the actual user "previousProvider" is null.
This is the reason for the error.
Not sure why this is happening or how to fix it.
Is there a better way of doing this?
Initially getting a null value even when there is a logged in user may be the reason. How to prevent it?

I will try to explain your code a bit so we are on the same page
Provider<AuthBase>(
create: (context) => Auth(), //this subscribe an Auth Class to the provider
),
StreamProvider<User>.value(
value: Auth().onAuthStateChanged,
/// This subscribe a stream from another Auth Class instance
/// Unless Auth its a singleton class, this is not related to the previous Provider
/// and changes to Provider<AuthBase> won't affect this Stream
),
I noticed even if the user is logged in at the start I get a null
value for the user then immediately i get the actual user.
That happens when using Stream, while the first value is captured (sometimes is delayed a tick) its null, you can use initialData parameter if you know the first value while the stream subscription is done
Finally
ChangeNotifierProxyProvider<Database, PostProvider>(
create: (context) => PostProvider(),
update: (ctx, database, previousProvider) {
print("at ChangeNotifierProxyProvider $previousProvider");
return previousProvider.update(database);
},
),
update expects to return a value of type PostProvider, but you're returning whatever the method previousProvider.update(database) returns (which I think it just updates some inner variable or something, I believe it's some void method maybe?)
Change the update like this
update: (ctx, database, previousProvider) {
print("at ChangeNotifierProxyProvider $previousProvider");
return previousProvider..update(database);
/// It's the same as writing this
/// previousProvider..update(database);
/// return previousProvider;
},

Related

Flutter: accessing providers from other providers

For my flutter project, I am using the following multiple providers below:
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider<FirstProvider>(
create: (context) => FirstProvider(),
),
ChangeNotifierProvider<SecondProvider>(
create: (context) => SecondProvider(),
),
ChangeNotifierProvider<ThirdProvider>(
create: (context) => ThirdProvider(),
),
ChangeNotifierProvider<FourthProvider>(
create: (context) => FourthProvider(),
),
],
child: const MainApp(),
);
}
Because sometimes I need to either get data or call functions from different providers from another provider, I am using it like this:
//First Provider
class FirstProvider with ChangeNotifier {
void callFunctionFromSecondProvider({
required BuildContext context,
}) {
//Access the SecondProvider
final secondProvider= Provider.of<SecondProvider>(
context,
listen: false,
);
secondProvider.myFunction();
}
}
//Second Provider
class SecondProvider with ChangeNotifier {
bool _currentValue = true;
void myFunction(){
//Do something
}
}
The callFunctionFromSecondProvider()of the FirstProvider is called from a widget and it will call myFunction() successfully, most of times.
Depending on the complexity of the function, I am sometimes experiencing that I can't access the SecondProvider, presumably due to context being null, when the widget state changes.
I am reading some documents online regarding provider, and they are suggesting changenotifierproxyprovider for what I understood as 1 to 1 provider relationship.
However, in my case, one provider needs to be accessed by multiple providers and vice versa.
Question:
Is there a more appropriate way that I can approach my case where one provider can be accessed by multiple providers?
EDIT:
Accessing provider should also be able to access different variable values without creating a new instance.
Instead of passing context to the callFunctionFromSecondProvider function add the second provider as the parameter. So the function looks like the below.
Not sure this is the correct way of doing that but my context null issue was fixed this way.
void callFunctionFromSecondProvider({
required SecondProvider secondProvider,
}) {
secondProvider.myFunction();
}
}
Alright.
So it looks like Riverpod by the same author is the way to go as it addresses alot of flaws such as Provider being dependent on the widget tree, in my case, where the underlying issue came from.
—--------
For the time being, I still need to use the provider and for a quick and dirty solution, I am providing the context of not only the current widget that I am trying to access the provider, but also passing the parent context of the widget directly, so that in case a modal (for example) is closed, then any subsequent provider call can still be executed using the parent context.
Hope this helps.

StreamProvider returns no data when used with Navigator

The issue is that I don't get any values out of my StreamProviders (which are defined on a global level) within my Authenticated route:
runApp(MultiProvider(
providers: [
Provider.value(value: userService),
StreamProvider.value(value: authService.userStream, initialData: null),
StreamProvider.value(value: userService.userDataStream),
StreamProvider.value(value: userService.characterStream),
],
child: MyApp(),
));
}
I noticed that it's to do with the logic that I have for my Navigator (if I remove it the provider values are passed down the widget tree as expected). The Navigator I'm using is based around the idea that the app has 3 states: Not Authenticated, Authenticated and Authenticated-First-Time. I get the value whether I'm authenticated from the loginStream (so far everything works):
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: loginStream,
builder: (ctx, snapshot) {
if (!snapshot.hasData) return Loading();
LoginState state = snapshot.data;
if (state == LoginState.LOGGED_OUT) return LoginScreen();
if (state == LoginState.FIRST_TIME) return CharacterCreationScreen();
return Navigator(
key: navigatorKey,
initialRoute: "/home",
onGenerateRoute: (settings) => PageRouteBuilder(
pageBuilder: (ctx, _, __) => routes(settings)(ctx),
transitionsBuilder: pageTransition,
),
);
},
);
The thing is that if I'm Authenticated and say in the HomeScreen, then both userDataStream and characterStream return null even if there's actual data available. If I remove the StreamBuilder + LoginLogic itself and just have the Navigator widget returned above, then HomeScreen gets the correct values.
UPDATE:
I noticed that it's not even the StreamBuilder. If I remove the 3 if's within the builder, then the stream values are propagated correctly. Not sure why that happens.
I´m not quite sure if this helps since I´m lacking details but here is what I noticed so far:
If you create the objects in the multiprovider for the first time you should not use .value - check if this applies.
Try cleaning up the if statements in the function body of your StreamBuilder (use if, else if and else keywords.
Also, following your description, it sounds like whenever an if statement is true, returns and thus cancels the build´s function body, the stream somehow resets and defaults to null. Maybe look into that & update your question.
Change this
Provider.value(value: userService),
StreamProvider.value(value: authService.userStream, initialData: null),
To this
Provider(create: (context) => userService)
StreamProvider(create:(context) => authService.userStream, initialData: null),
Do the same for all the providers that u are registering
To expose a newly created object, use the default constructor of a provider. Do not use the .value constructor if you want to create an object, or you may otherwise have undesired side effects.
https://pub.dev/packages/provider

How reinitialize flutter MultiProvider's all providers data?

In my application, I have many providers, for instance notifications, auth, profile, conversations, trips, etc... And now I need to reinitialize all providers, when user logout. Otherwise old user data will still stay in provider, and new user will get them.
After spending my day, I solved the problem in this way. It is the most elegant way I could do. So after logout, you have to remove all screens and navigate to the root widget, within which your Provider or MultiProvider is created, and so your provider or all your providers inside MultiProvider will be recreated, and all data will be reinitialized
Navigator.of(context).pushAndRemoveUntil<T>(
MaterialPageRoute(
builder: (_) => MyApp(),
),
(_) => false,
);
Where MyApp is the root widget, which is passed as parameter in your main function in main.dart.
runApp(
MyApp(token: token),
);
You can call the providers and clear all the user's data. For example:
You can call your authProvider.logOut(), all still depends on your project structure.
RaisedButton(
child: const Text("Logout"),
onPressed: () {
final Auth auth = Provider.of<Auth>(context, listen: false);
auth.isAuthentificated = false;
},
),

ChangeNotifierProxyProvider not initiated on build

I'm trying to understand multiproviders in Flutter. In my App, one Provider need to change based on a value from an other Provider.
AuthProvider gets initiated higher up in the widget tree on build. Works like a charm with automatic sign in if possible...
In a lower placed widget, I try to initiate two other Providers. One, WhatEver, is not depended on other data and gets initiated on build like it is supposed to using ChangeNotifierProvider.
ProductList however is depended on AuthProvider. If log in status is changed, the ProducList should update accordingly.
In my attempts, I've found out, ie found on SO, that ChangeNotifierProxyProvider is the right way to go. But when I run the App, it seems like the 'create'-part of ChangeNotifierProxyProvider is not initiated when the widget gets build. It seems like the ProductList provider is not initiated until it's read or written to.
What have I misunderstood using MultiProviders and ChangeNotifierProxyProvider?
return MultiProvider(
providers: [
ChangeNotifierProvider<WhatEver>(create: (context) => WhatEver()),
ChangeNotifierProxyProvider<AuthProvider, ProductList>(
create: (_) => ProductList(Provider.of<AuthProvider>(context, listen: false)),
update: (_, auth, productList) => productList..reloadList(auth)
),
],
The ProductList looks like this:
final AuthProvider _authProvider;
static const String _TAG = "Shop - product_list.dart : ";
ProductList(this._authProvider) {
print(_TAG + "ProductList Provider initiated");
reloadList(this._authProvider);
}
void reloadList(AuthProvider authProvider) {
print(_TAG + "ProductList reload started");
if (authProvider.user==null) {
print(_TAG + "ProductList: _authProvider == null");
_loadBuiltInList();
} else {
print(_TAG + "ProductList: user = " + authProvider.user.displayName);
_loadFirestoreList();
}
}
I have code that does this:
ChangeNotifierProxyProvider<AuthService, ProfileService>(
create: (ctx) => ProfileService(),
update: (ctx, authService, profileService) =>
profileService..updateAuth(authService),
),
My ProfileService() does not rely on AuthService being available when it is constructed. The code works fine :)
The ChangeNotifierProxyProvider documentation explicitly describes this approach:
Notice how MyChangeNotifier doesn't receive MyModel in its constructor
anymore. It is now passed through a custom setter/method instead.

Using two BLoCs in same page and passing first BLoC's state in second BLoC

I have been learning about Bloc Pattern in Flutter for a few days.
I have a page where I need to generate OTP and validate it.
There are two APIs(generateOtp, validateOtp) two implement this functionality.
In the generateOtp API response, I need to save one key i.e uniqueIdentifier.
Then I need to pass the above uniqueIdentifier and Otp value(User entered) to the validateOtp API.
I have created two separate BLoCs... generateOtpBloc, validateOtpBloc.
Using MultiBLoC Provider I am using these two BLoCs.
Navigator.of(context).push(
MaterialPageRoute<LandingPage>(
builder: (_) => MultiBlocProvider(
providers: [
BlocProvider<GenerateOtpBloc>(
create: (context) => GenerateOtpBloc(GenerateOtpInitial())
),
BlocProvider<ValidateOtpBloc>(
create: (context) => ValidateOtpBloc(ValidateOtpInitial())
)
],
child: OtpPage(),
),
),
);
I am able to invoke APIs and get the API responses in my UI page.
But how to save the uniqueIdentifier value which I get in the generateOtp and how to pass this uniqueIdentifier in the second API?
I thought of using setState() to set the state of uniqueIdentifier. But I'm receiving an error.
child: BlocBuilder<GenerateOtpBloc, GenerateOtpState>(
builder: (context, state) {
if (state is GenerateOtpLoading) {
print("**********GenerateOtpLoading*************");
return buildLoading();
} else if (state is GenerateOtpLoaded) {
print("**********GenerateOtpLoaded*************");
***//But Im getting error here.***
***setState(() {
uniqueIdentifier: state.uniqueIdentifier
});***
return buildGenerateOtpWidget(context, state.generateOtpRes);
} else {
print("**********Else*************");
print(state);
}
},
),
),
Both generateOtp and validateOtp requests and responses are completely different... that is why I used two different BLoCs.
Suggest to me the best way to handle this?
Why you try to use two blocs for handle it? you can use two events in one bloc. This is my code in the OTP login project similar to your project:
class LoginBloc extends Bloc<LoginEvent, LoginState> {
FirstApiClass _firstApi;
SecondApiClass _secondApi;
LoginBloc() : super(Loading()) {
_firstApi = FirstApiClass();
_secondApi = SecondApiClass();
}
#override
Stream<LoginState> mapEventToState(
LoginEvent event,
) async* {
if (event is GenerateOtp) {
// Use FirstApiClass
} else if (event is ValidateOtpBloc) {
// Use SecondApiClass
}
}
}
However, you can also use one Api class for this situation!
I hope it's useful for you.