Okay I have a FlutterFire app and using Riverpod to manage the state of my app. I have a Listenable Provider named userProvider where I get the user data from Cloud Fierestore. Note that I have a Stream Provider which updates the userProvider. Here's what I mean:
Stream<UserModel> getUserData(String uuid) {
return _users.doc(uuid).snapshots().map(
(event) => UserModel.fromMap(event.data() as Map<String, dynamic>));
}
That above is my Stream Provider and here's what I mean when saying it updates the state of my userProvider:
void getData(WidgetRef ref, User data) async {
userModel = await ref
.watch(authControllerProvider.notifier)
.getUserData(data.uid)
.first;
ref.read(userProvider.notifier).update((state) => userModel);
}
the above function is in my main.dart file.
My userProvider is like this:
final userProvider = StateProvider<UserModel?>((ref) => null);
okay so I made it nullable because before sign in there'll be no user signed in meaning no data to hold.
When I try to use the userProvider I'm getting an error saying null check operator used on a null value but I use a null check operator because I'm certain the user is signed in now and there should be data store to that Provider.
Plaese help me know what is wrong with my code.
Thank you
You need to check first for null before accessing the data in the Provider.
to check null heres the code:
final user = userProvider.state;
if (user != null) {
// use user
} else {
// handle the case where user is null
}
Probably the provider state is reset, although I don't see you have an .autoDispose modifier. The use this way, you can track at what point in time the state of the provider turns into null:
final userProvider = StateProvider<UserModel?>((ref) {
ref.listenSelf((previous, next) {
print('$next => $previous');
});
ref.onDispose(() {
print('userProvider has been disposed');
});
});
I think it's a great way to stop provider nulling :) Debugging is everything.
Related
I'm stuck with a situation where I am trying to use a RiverPod provider in my Flutter app to represent user preference data. In this case, the user preference data is stored in FireStore.
I'm stuck with understanding how to load provider state from Firestore. Currently, I'm trying to use the "userPreferencesFutureProvider" to load the 'GdUserPreferences" data from a service that calls Firestore, which then pushes the data into "userPreferencesProvider" using the 'overrideWith' method. However, when I access the user preference data via the 'userPreferencesProvider' provider the data loaded from Firestore is not present
final userPreferencesFutureProvider = FutureProvider<bool>( (ref) async {
final p = ref.watch(userPrefsServiceProvider);
GdUserPreferences? aPrefs = await p.load();
if (aPrefs == null) {
aPrefs = GdUserPreferencesUtil.createDefault();
}
userPreferencesProvider.overrideWith((ref) => UserPreferencesNotifier(p,aPrefs!));
return true;
});
final userPreferencesProvider = StateNotifierProvider<UserPreferencesNotifier,GdUserPreferences>((ref) {
return UserPreferencesNotifier(ref.watch(userPrefsServiceProvider),GdUserPreferences());
});
Any suggestions?
Update
Further to the feedback received I have updated my code as shown below, but this still doesn't work...
final userPreferencesFutureProvider = FutureProvider<bool>( (ref) async {
// get service that wraps calls to Firestore
final p = ref.watch(userPrefsServiceProvider);
// load data from Firestore
GdUserPreferences? aPrefs = await p.load();
// if none found then create default values
if (aPrefs == null) {
aPrefs = GdUserPreferencesUtil.createDefault();
}
// push state into a provider that will be used
ref.read(userPreferencesProvider.notifier).update(aPrefs);
// this future returns a boolean as a way of indicating that the data load succeeded.
// elsewhere in the app access to the user preference data is via the userPreferencesProvider
return true;
});
final userPreferencesProvider = StateNotifierProvider<UserPreferencesNotifier,GdUserPreferences>((ref) {
print('default provider');
return UserPreferencesNotifier(ref.watch(userPrefsServiceProvider),GdUserPreferences());
});
class UserPreferencesNotifier extends StateNotifier<GdUserPreferences> {
// service is a wrapper around FireStore collection call
final GdUserPreferencesService service;
UserPreferencesNotifier(this.service, super.state);
void update(GdUserPreferences aNewPrefs) {
print('update state');
state = aNewPrefs;
}
}
The purpose of having a separate FutureProvider and StateNotifierProvider, is that I can insert the FutureProvider when the app first loads. Then when I want to access the user preference data I can use the straight forward StateNotifierProvider, which avoids the complications of having Future Providers all over the app.
Note: using the print methods I can show that the 'update' method is called after the userPreferencesProvider is first created, so I can't understand why the data is not updated correctly
Apologies to all responders...
The problem was caused by a coding error on my side. I had two versions of 'userPreferencesProvider' defined in two different locations. Taking out the rogue duplicate definition and it now works fine.
I have StreamProvider whiсh get data from FireStore where i passing on id of document that
i want to retrieve.
This is StreamProvider in root of widgets:
StreamProvider<List<TheExercise>?>(create: (_) => DatabaseService(programId: context.watch<DataChangeNotifier>().getData).exercises, initialData: null, ),
In "context.watch().getData" i pass id that will change depends on user activity, but i can't figure out how to rebuild streamProvider when it's get a new id.
Now stream is building one time when i listen to it.
final _exercises = Provider.of<List<TheExercise>?>(context)?? [];
Here is code in DatabaseService class:
CollectionReference exercisesCollection(){
final CollectionReference _exercisesCollection = FirebaseFirestore.instance.collection('userData').doc(_auth.currentUser!.uid.toString()).collection('programs')
.doc(programId).collection('exercises');
return _exercisesCollection;
}
Stream<List<TheExercise>> get exercises {
return exercisesCollection().snapshots()
.map((listOfExercise));
DataChangeNotifier class code:
class DataChangeNotifier with ChangeNotifier {String _dataIdOfExercise = '';String get getData => _dataIdOfExercise;void changeIdOfExercise(String dataIdOfExercise) {
_dataIdOfExercise = dataIdOfExercise;
notifyListeners();} }
I solved it replase StreamProvider on StreamProvider.value.
As i understood it takes a value every time depens on link in parameter "value".
StreamProvider<List<TheExercise>?>.value(value: DatabaseService(programId: context.watch<DataChangeNotifier>().getData).exercises, initialData: null),
But will appreciate any answers for additional knowledge.
I was following the Resocoder tutorial on how to manage states with RiverPod and StateNotifier.
What kind of trips me is as to how to call the .getWeather on an initial load with some let say default value. The example only illustrates using context.read(..) in the onPressed(..) function which is what is recommended in the riverpod docs.
But then how do you actually make a call on load, since that would mean calling context.read in the build method which is highly discouraged. (mentioned in the last part of this section)
Because .getWeather is a Future function, you can actually add the future initialization inside the constructor of WeatherNotifier and let it update the state itself.
final weatherNotifierProvider = StateNotifierProvider(
(ref) => WeatherNotifier(ref.watch(weatherRepositoryProvider)),
);
class WeatherNotifier extends StateNotifier<WeatherState> {
final WeatherRepository _weatherRepository;
WeatherNotifier(this._weatherRepository) : super(WeatherInitial()){
getWeather('some city name'); // add here
}
Future<void> getWeather(String cityName) async {
try {
state = WeatherLoading();
final weather = await _weatherRepository.fetchWeather(cityName);
state = WeatherLoaded(weather);
} on NetworkException {
state = WeatherError("Couldn't fetch weather. Is the device online?");
}
}
}
Look into .family modifier, you can pass data to a provider returned data/state.
https://riverpod.dev/docs/concepts/modifiers/family
Listening to state with a parameter:
final state = ref.watch(myProvider('my arg'));
Creating a provider:
myProvider = StateNotifierProvider.family<String>((ref, arg) => "Hello $arg");
If you print state you'll get
print(state); //prints "Hello my arg"
I am trying to get documents with their own subcollections, from Stream, but I am stuck.
This is where I set up my StreamSubscription:
Future<void> _toggleOrdersHistorySubscription({FirebaseUser user}) async {
_ordersHistorySubscription?.cancel();
if (user != null) {
_ordersHistorySubscription = ordersRepository
.ordersHistoryStream(userId: user.uid)
.listen((ordersSnapshot) {
final List<OrderModel> tmpList = ordersSnapshot.documents.map((e) {
// e.reference.collection("cart").getDocuments().;
return OrderModel.orderFromSnapshot(e);
}).toList();
add(OrdersHistoryUpdated(ordersHistory: tmpList));
});
}
}
The issue is that I can't see a way to get subcollection along with the parent document because getDocuments returns a Future.
Anyone can clear this issue for me?
So, I update the code method a separate method for retrieving data when listener is triggered but it doesn't work fully and I do not understand what's happening and why part of the code is working and part is not.
List<OrderModel> _getOrdersHistory({
#required QuerySnapshot snapshot,
}) {
return snapshot.documents.map((document) {
List<OrderedProductModel> cart = [];
List<AddressModel> addresses = [];
document.reference.collection("cart").getDocuments().then((snapshot) {
snapshot?.documents?.forEach((doc) {
cart.add(OrderedProductModel.fromSnapshot(doc));
});
});
document.reference
.collection("addresses")
.getDocuments()
.then((snapshot) {
snapshot?.documents?.forEach((doc) {
addresses.add(AddressModel.addressFromJson(doc.data));
});
});
final order = OrderModel.orderFromSnapshot(
document,
restaurantCart: cart,
);
return order.copyWith(
orderAddress:
(addresses?.isNotEmpty ?? false) ? addresses.first : null,
sentFromAddress:
(addresses?.isNotEmpty ?? false) ? addresses.last : null,
);
})
.toList() ??
[];
}
As an alternate solution to my original issue is that I made a map entry in Firestore instead of a collection for 2 address documents (which are set above as orderAddress and sentFromAddress) and for the cart I decided to get the data when needed for every cart item.
So the method which I put in the update is not the final one, but I want to understand what is happening up there as I do not understand why:
Why the cart is shown as empty if I do a print(order); right before the return and in the bloc it has data;
Why the orderAddress and sentFromAddress are both empty no matter what I try;
To be short: You'll never be able to get a List synchronously if you get the data async from firebase.
Both questions have the same answer:
Your timeline:
For each document
Create an empty list
Initiate the firebase query - getDocuments()
Subscribe to the returned future with - .then((snapshot){cart.add(...)}).
This lambda will be invoked when the documents arrived.
Another subscribe
Save your empty cart and first/last of empty addresses to an OrderModel
Your List contains the references to your empty lists indirectly
Use your bloc, some time elapsed
Firebase done, your callbacks starts to fill up your lists.
Regarding your comment like stream.listen doesn't like async callbacks:
That's not true, you just have to know how async functions work. They're run synchronously until the first await, then return with an incomplete future. If you do real async things you have to deal with the consequences of the time delay like changed environment or parallel running listeners.
You can deal with parallel running with await for (T v in stream) or you can use subscription.pause() and resume.
If anything returns a future, just do this:
...getDocuments().then((value) => {
value is the item return here. Do something with it....
})
Also, you might want to split your method up a bit to share the responsibility.
If getDocuments is a Future function and you need to wait for it, I think you should add await before it. I also don't see the snapshot status checking in your code pasted here. Maybe you have already checked the snapshot status in other function? Make sure the snapshot is ready when using it.
I have been attempting to create streams to Firestore documents using the uid obtained from my auth Provider:
class AuthService {
...
static final provider = StreamProvider.autoDispose((ref) => FirebaseAuth.instance.onAuthStateChanged);
...
}
However, I am struggling to actually create a StreamProvider dependent on the value from the auth Provider.
class User {
...
static final provider = StreamProvider((ref) {
final stream = ref.read(AuthService.provider);
// Returns AsyncValue<Stream<User>> instead of desired AsyncValue<User>
return stream.map((auth) => Service.user.stream(auth.uid));
});
...
}
I also tried using Computed to return the uid or the stream itself but you cannot read a Computed from a Provider (which makes sense in retrospect).
This question is the most relevant on this topic but it is dealing with Provider, not Riverpod.
P.S. Can a Riverpod tag be created?
Edit:
The answer isn't working quite right. The await for loop is only ever triggering once, whereas a listener catches all events.
static final provider = StreamProvider((ref) async* {
final stream = ref.read(AuthService.provider);
print('userProvider init');
stream.listen((auth) {
print('LISTENED: ${auth?.uid}');
});
await for (final auth in stream) {
print('uid: ${auth?.uid}');
yield* Service.user.stream(auth?.uid);
}
});
This code yields the following on login:
userProvider init
LISTENED: <redacted UID>
uid: <redacted UID>
And then on logout:
LISTENED: null
Where I would expect to see uid: null as well, which would update the stream, but upon any more auth events, only the listener is triggered and no events are caught by the await for loop.
Interestingly, using the flutter inspector, the value emitted by the auth provider never changes, either:
AutoDisposeStreamProvider<FirebaseUser>#95f11: AsyncValue<FirebaseUser>.data(value: FirebaseUser(Instance of 'PlatformUser'))
persists through login/logout events, which could explain this behavior, but I am not sure what to do to fix it.
Any ideas? I have been stuck on this for a while and can't correct the issue.
The problem is, your provider doesn't create a Stream<User> but a Stream<Stream<User>>
As part of 0.6.0-dev, you can use ref.watch to easily combine streams:
class User {
...
static final provider = StreamProvider((ref) {
final auth = ref.watch(AuthService.provider);
return Service.user.stream(auth.uid);
});
...
}
I want to preface this by saying I've only been working with Dart/Flutter for a few months, so feel free to correct anything I say and I will update the answer.
I solved this issue after much trial and error and re-reviewing documentation many times. I guess I made a poor assumption that Providers would update when a Provider they depend on changes.
I found that once a StreamProvider returns (or yields), until it is disposed it will always return the same value it originally did, regardless of dependency changes or events coming from a Stream. This is where Computed is useful but doesn't really work well when you desire to return AsyncValue from your provider (how StreamProvider behaves).
Also, additional confusion was caused by the Flutter Inspector not updating the ProviderScope widget correctly. I have to click around and refresh a few times to see updates to Providers or their state. Anyways...
class AuthService {
...
static final provider = StreamProvider.autoDispose((ref) {
final sub = FirebaseAuth.instance.onAuthStateChanged.listen((auth) => ref.read(uidProvider).state = auth?.uid);
ref.onDispose(() => sub.cancel());
return FirebaseAuth.instance.onAuthStateChanged;
});
static final uidProvider = StateProvider<String>((_) => null);
...
}
class User {
...
static final provider = StreamProvider.autoDispose((ref) {
final uid = ref.read(AuthService.uidProvider)?.state;
return Service.user.stream(uid);
});
...
This solution works given that your UI no longer depends on providers (allowing them to be properly disposed) when a user signs out of your app.