How can I implement a provider that is depended on another provider? - flutter

I am using the provider package with flutter and dart I have a authentication provider and a stream provider that provides a user model. The stream provider that provides the user model is depended on the authentication provider. How can I get my user id to my stream provider?
This is my multiprovider
MultiProvider(
providers: [
Provider<AuthenticationProvider>(builder: (context) => AuthenticationProvider(),),
StreamProvider<UserModel>.value(value: userStream(//here we call the async method in the Authentication provider to get the user id),
),
]
This is the method inside the Authentication Provider returns a authentication model which contains the user id
Future<UserAuthenticationCertificate> userAuthenticationCertificate() async
{
FirebaseUser authenticatedUser = await _authentication.currentUser();
if(authenticatedUser != null)
return UserAuthenticationCertificate.fromFirebaseAuthentication(authenticatedUser);
return null;
}
This is the authentication certificate
import 'package:firebase_auth/firebase_auth.dart';
class UserAuthenticationCertificate
{
String _userID;
String get userID{
return _userID;
}
UserAuthenticationCertificate(this._userID);
UserAuthenticationCertificate._internal(this._userID);
factory UserAuthenticationCertificate.fromFirebase(FirebaseUser firebaseUser)
{
return UserAuthenticationCertificate._internal(
firebaseUser.uid
);
}
}
Edit
This is what I currently have
Provider<AuthenticationProvider>(create: (_) => AuthenticationProvider(),),
Provider<UserProvider>(
create: (_) => UserProvider(),
),
StreamProvider(create: (context) {
return Provider.of<UserProvider>(context).userStream(Provider.of<AuthenticationProvider>(context).userAuthenticationCertificate());
}),
So now I provide the user provider, in a streamprovider? I want to be providing the userModel as the stream how do I do that?

You can use the context parameter passed to create of your providers to read other providers:
Provider(create: (_) => Auth()),
StreamProvider(create: (context) {
return Provider.of<Auth>(context).something;
});

Related

Error in provider flutter context listen:false

So, I'm making an app of OTP login with flutter and dart and when I provide the pohone number it's shows an error
The code is this:
void sendPhoneNumber() {
final ap = Provider.of<AuthProvider>(context, listen: false);
String phoneNumber = phoneController.text.trim();
ap.signInWithPhone(context, "+{selectedCountry.phoneCode}$phoneNumber");
}
The error it's when I'm in final ap, there shows this: "Tried to listen to a value exposed with provider, from outside of the widget tree."
Any idea what can i do to solve it?
Seem like you're forgetting to create an object
Single
Provider(
create: (_) => MyModel(),
child: ...
)
Multi
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => myViewModel()),
]
)

Flutter - ProxyProvider how to fix error "Null check operator used on a null value"

Use case of the app:
at App start, it will fetch a random User from a RestAPI,
then it will use the result of the first call to make another RestAPI call, this is why I need ProxyProvider.
main.dart:
return MultiProvider(
providers: [
ChangeNotifierProvider(
create: (_) => RandomUser(),
),
ChangeNotifierProxyProvider<RandomUser, FavoriteList>(
create: (BuildContext ctx) => FavoriteList(ctx.read<RandomUser>()),
update: (_, RandomUser user, __) => FavoriteList(user),
),
RandomUser provider:
class RandomUser extends ChangeNotifier {
final apiUsers = UsersApi();
UserModel? _profile;
String? _userId;
RandomUser() {
fetchUser();
}
Future<void> fetchUser() async {
await apiUsers
.apiGetUser()
.then((user) => {
_profile = user,
_userId = chosenUserId,
})
.catchError((e) {
print("error: $e");
});
notifyListeners();
}
UserModel get profile => _profile;
String get chosenUserId => _userId;
}
FavoriteList provider:
class FavoriteList extends ChangeNotifier {
final RandomUser _user;
final _apiFavoriteList = FavoriteListApi();
List<FavoriteListModel> _favoriteList = <FavoriteListModel>[];
FavoriteList(this._user) {
fetchFavoriteList(_user.chosenUserId);
}
Future<void> fetchFavoriteList(String userId) async {
await _apiFavoriteList
.apiGetFavoriteList(userId)
.then((favoriteList) => {
_favoriteList = favoriteList,
})
.catchError((e) {
print("error: $e");
});
notifyListeners();
}
List<FavoriteListModel> get favoriteList => this._favoriteList;
}
So as you can see, the FavoriteList provider needs the RandomUser provider, to retrieve the getter value chosenUserId
When I launch the App, I get right away the error "Null check operator used on a null value"
on the getter chosenUserId and in the main.dart where I call "create" of the ProxyProvider
What am I doing wrong?
Shouldn't the ProxyProvider first initialized the first Provider, so all the values I need are available?
The issue is that RandomUser.fetchUser() has not completed before FavoriteList is created. You should code allowing for this situation, e.g. in RandomUser:
String? get chosenUserId => _userId;
and in FavoriteList:
final? RandomUser _user;
FavoriteList(this._user) {
if (_user != null && _user?.chosenUserId != null) {
fetchFavoriteList(_user.chosenUserId);
}
}
String? get chosenUserId => _user?.chosenUserId;
When fetchUser() completes then FavoriteList will be updated.
Your UI will have to cope with the (temporary) missing data, of course.
BTW the documentation for ChangeNotifierProxyProvider suggests that you should structure your code like this:
ChangeNotifierProxyProvider<MyModel, MyChangeNotifier>(
create: (_) => MyChangeNotifier(),
update: (_, myModel, myNotifier) => myNotifier
..update(myModel),
child: ...
);
In that situation, if MyModel were to update, then MyChangeNotifier will be able to update accordingly. Notice how MyChangeNotifier doesn't receive MyModel in its constructor anymore. It is now passed through a custom setter/method instead.
try this solution
my code was like this
providers: [
ChangeNotifierProvider(create: (ctx) => Auth()),
ChangeNotifierProxyProvider<Auth, Products>(
create: (ctx) => Products(null, null, []),
update: (ctx, auth, previousProducts) => Products(
auth.token! ,
auth.userId! ,
previousProducts == null ? [] : previousProducts.items),
),
ChangeNotifierProvider(create: (ctx) => Cart()),
ChangeNotifierProxyProvider<Auth, Orders>(
create: (ctx) => Orders(null, null, []),
update: (ctx, auth, previousOrders) {
print(auth);
return Orders(auth.token!, auth.userId!,
previousOrders == null ? [] : previousOrders.orders);
},
),
],
and fix that by change ! to ?? like this code
ChangeNotifierProxyProvider<Auth, Products>(
create: (ctx) => Products(null, null, []),
update: (ctx, auth, previousProducts) => Products(
auth.token ?? null,
auth.userId ?? null,
previousProducts == null ? [] : previousProducts.items),
),

Flutter: Access SharedPreferences Provider / ChangeNotifier in a Stream Class

I've looked around in StackoverFlow and was not able to find myself a solution to this.
Scenario:
I have a Flutter SharedPreferences Provider with ChangeNotifier Class, that will get updated with the current Logged In User info.
Simplified content:
class SharedPreferences {
final String userId;
final String userName;
SharedPreferences({
#required this.userId,
#required this.userName,
});
}
class SharedPreferencesData with ChangeNotifier {
var _sharedPreferencesData = SharedPreferences(
userId: 'testUserId',
userName: 'testUserName',
);}
And a database.dart file with Class containing DataBaseServices to get FireStore Streams from Snapshots:
class DatabaseService {
final CollectionReference companiesProfilesCollection =
Firestore.instance.collection('companiesProfiles');
List<CompanyProfile> _companiesProfilesFromSnapshot(QuerySnapshot snapshot) {
return snapshot.documents.map((doc) {
return CompanyProfile(
docId: doc.documentID,
companyName: doc.data['companyName'] ?? '',
maxLocationsNumber: doc.data['maxLocationsNumber'] ?? 0,
maxUsersNumber: doc.data['maxUsersNumber'] ?? 0,
);
}).toList();
}
Stream<List<CompanyProfile>> get getCompaniesProfiles {
return companiesProfilesCollection
.where('userId', isEqualTo: _userIdFromProvider)
// My problem is above -----
.snapshots()
.map(_companiesProfilesFromSnapshot);
}
}
I Don't want to fetch the entire Stream data as it could be massive for other Streams, I just want to pass the userID under .where('userId', isEqualTo:_userIdFromProvider).
I couldn't access the context in this class to get the data from the Provider
Couldn't send the userId to getCompaniesProfiles getter, as getter don't take parameters
And if I convert this getter to a regular method, I wont be able to send the userID to it, as this has to run under void main() {runApp(MyApp());} / return MultiProvider(providers: [ and By then I cannot call fetch the sharedPreferences with a context that does not contain the provider info ...
Couldn't figure out how to receive the context as a constructor in this class, when I did, I got the following Only static members can accessed in initializers in class DatabaseService.
I'm still a beginner, so I would appreciate if you could share with me the best approach to handle this.
Thank you!
*********** Re-Edited by adding the below: **************
I'm trying to implement the same scenario, here is my code:
Main file:
return MultiProvider(
providers: [
ChangeNotifierProvider<SpData>(
create: (context) => SpData(),
),
ProxyProvider<SpData, DS>(
create: (context) => DS(),
update: (ctx, spData, previousDS) {
print('ChangeNotifierProxyProvider RAN');
previousDS.dbData = spData;
return previousDS;
},
),
],
SP File:
class SP {
final String companyId;
SP({
#required this.companyId,
});
}
class SpData with ChangeNotifier {
var _sPData = SP(
companyId: '',
);
void setCompanyId(String cpID) {
final newSharedPreferences = SP(
companyId: cpID,
);
_sPData = newSharedPreferences;
print('_spData companyId:${_sPData.companyId}');
notifyListeners();
}
String get getCompanyId {
return _sPData.companyId;
}
}
DS file:
class DS with ChangeNotifier {
SpData dbData;
void printCompanyId() {
var companyId = dbData.getCompanyId;
print('companyId from DataBase: $companyId');
}
}
The SpData dbData; inside Class DS does not update. I've added the prints to figure out what is running and what is not. When I run my code, the print function in main.dart file print('ChangeNotifierProxyProvider RAN'); does not run.
What am I missing? Why ChangeNotifierProxyProvider is not being triggered, to update dbData inside DS file? Thanks!
You can use ProxyProvider for this purpose.
ProxyProvider is a provider that builds a value based on other providers.
You said you have a MultiProvider, so I guess you have SharedPreferencesData provider in this MultiProvider and then DatabaseService provider. What you need to do is use ProxyProvider for DatabaseService instead of a regular provider and base it on the SharedPreferencesData provider.
Here is an example:
MultiProvider(
providers: [
ChangeNotifierProvider<SharedPreferencesData>(
create: (context) => SharedPreferencesData(),
),
ProxyProvider<SharedPreferencesData, DatabaseService>(
create: (context) => DatabaseService(),
update: (context, sharedPreferencesData, databaseService) {
databaseService.sharedPreferencesData = sharedPreferencesData;
return databaseService;
},
dispose: (context, databaseService) => databaseService.dispose(),
),
],
child: ...
Here is what happens in the code snippet above:
ProxyProvider calls update everytime SharedPreferencesData changes.
DatabaseService gets its sharedPreferencesData variable set inside update.
Now that you have access to sharedPreferencesData inside the DatabaseService instance, you can do what you want easily.

Flutter await Provider to be ready inside multiprovider

I have a problem with synchronize Providers creation.
I'm new to flutter, I'll try to explain better as i can.
In my main.dart i have two providers one for the user auth and one for another widget into the code, it simply have a list to display.
I use ChangeNotifierProxyProvider because all other provider needs to access to user auth tokens and details. All methods for user to store,read tokens are in the userProvider.
When UserProvder.init() is called the object is created but is still not ready beacuse of the http request, the code in the main continue to execute and pass the UserProvider to the Conto Provider that thinks UserProvider is ready but it is not. When the ContoProvider start to retrive the List with the token inside UserProvider access through userService failed the call because is still null.
So, How can i synchronize the creation of provider in order to wait the UserProvider to be full ready and then initialize all other providers?
main.dart
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider<UserProvider>(
create: (_) => UserProvider.init(),
),
ChangeNotifierProxyProvider<UserProvider, ContoProvider>(
create: (_) {
return ContoProvider.init(
Provider.of<UserProvider>(_, listen: false));
},
update: (_, userProvider, contoProvider) =>
contoProvider..update(userProvider),
),
],
child:...
userProvider.dart
User activeUser = User(null, null, null);
UserStatus status = UserStatus.Checking;
UserProvider.init() {
checkUserPresence();
}
void checkUserPresence() async {
/*here i check if in secure storage there is a refreshtoken if there is i make an http
request for a new token and a second request to fill the User into UserProvider so i need await async*/
}
ContoProvider.dart
UserProvider userService;
ContoProvider.init(UserProvider user) {
userService = user;
lookUpConti();
}
void lookUpConti() async {
/*here i make an http call to retrive some data, i access to
userService for get token and refresh token if needed*/
}
You can use WidgetsFlutterBinding.ensureInitialized()
void main() {
/** WidgetsFlutterBinding.ensureInitialized() is required in Flutter v1.9.4+ before using any plugins if the code is executed before runApp. */
WidgetsFlutterBinding.ensureInitialized();
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider<UserProvider>(
create: (_) => UserProvider.init()),
ChangeNotifierProxyProvider<UserProvider, ContoProvider>(
create: (_) {
return ContoProvider.init(
Provider.of<UserProvider>(_, listen: false));
},
update: (_, userProvider, contoProvider) =>
contoProvider..update(userProvider),
),
],
child: MyApp(),
),
}
PS: I will recommend that you separate your repositories from your provider. That is, not API call to external/web resources should be found in your provider. You can pass such class into your provider as arguments.

How to use a provider inside of another provider in Flutter

I want to create an app that has an authentication service with different permissions and functions (e.g. messages) depending on the user role.
So I created one Provider for the user and login management and another one for the messages the user can see.
Now, I want to fetch the messages (once) when the user logs in. In Widgets, I can access the Provider via Provider.of<T>(context) and I guess that's a kind of Singleton. But how can I access it from another class (in this case another Provider)?
From version >=4.0.0, we need to do this a little differently from what #updatestage has answered.
return MultiProvider(
providers: [
ChangeNotifierProvider(builder: (_) => Auth()),
ChangeNotifierProxyProvider<Auth, Messages>(
update: (context, auth, previousMessages) => Messages(auth),
create: (BuildContext context) => Messages(null),
),
],
child: MaterialApp(
...
),
);
Thanks for your answer. In the meanwhile, I solved it with another solution:
In the main.dart file I now use ChangeNotifierProxyProvider instead of ChangeNotifierProvider for the depending provider:
// main.dart
return MultiProvider(
providers: [
ChangeNotifierProvider(builder: (_) => Auth()),
ChangeNotifierProxyProvider<Auth, Messages>(
builder: (context, auth, previousMessages) => Messages(auth),
initialBuilder: (BuildContext context) => Messages(null),
),
],
child: MaterialApp(
...
),
);
Now the Messages provider will be rebuilt when the login state changes and gets passed the Auth Provider:
class Messages extends ChangeNotifier {
final Auth _authProvider;
List<Message> _messages = [];
List<Message> get messages => _messages;
Messages(this._authProvider) {
if (this._authProvider != null) {
if (_authProvider.loggedIn) fetchMessages();
}
}
...
}
Passing another provider in the constructor of the ChangeNotifierProxyProvider may cause you losing the state, in that case you should try the following.
ChangeNotifierProxyProvider<MyModel, MyChangeNotifier>(
create: (_) => MyChangeNotifier(),
update: (_, myModel, myNotifier) => myNotifier
..update(myModel),
);
class MyChangeNotifier with ChangeNotifier {
MyModel _myModel;
void update(MyModel myModel) {
_myModel = myModel;
}
}
It's simple: the first Provider provides an instance of a class, for example: LoginManager. The other Provides MessageFetcher. In MessageFetcher, whatever method you have, just add the Context parameter to it and call it by providing a fresh context.
Perhaps your code could look something like this:
MessageFetcher messageFetcher = Provider.of<ValueNotifier<MessageFetcher>>(context).value;
String message = await messageFetcher.fetchMessage(context);
And in MessageFetcher you can have:
class MessageFetcher {
Future<String> fetchMessage(BuildContext context) {
LoginManager loginManager = Provider.of<ValueNotifier<LoginManager>>(context).value;
loginManager.ensureLoggedIn();
///...
}
}
Seems like this would be a lot easier with Riverpod, especially the idea of passing a parameter into a .family builder to use the provider class as a cookie cutter for many different versions.