-
-
Notifications
You must be signed in to change notification settings - Fork 973
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Error: The argument type 'ScopedProvider<FirestoreDatabase>' can't be assigned to the parameter type 'AlwaysAliveProviderBase<Object, FirestoreDatabase>' #144
Comments
Maybe I can answer some of this. ScopedProviders can only be read within the UI tree. They cannot be obtained from the ref parameter in provider creation. The best way to deal with scoped providers providing parameters to other providers is to use a Family provider. // in the top level of the file
final jobsStreamProvider = StreamProvider.family<List<Job>, FirestoreDatabase>(
(ref, database) => database.jobsStream(),
);
// in your Jobs Page widget
final asyncValue = watch(jobsStreamProvider(watch(databaseProvider))); It's not a problem to have providers in the top level of the file. They don't use memory until they are used, and with the dispose modifier they are cleaned up. If you want to make it private to the file to avoid polluting the global scope that is also a good option. However, final databaseProvider =
StateProvider<FirestoreDatabase>((ref) =>null);
// Instead of the override:
// Use a hook or something in initState of the widget
// where you are doing the override
// and update the state of the databaseProvider
// i.e. (hooks) in the build() method
useMemoized((_) {
context.read(databaseProvider).state = FirestoreDatabase(uid: user.uid);
});
// (no hooks) (I could be wrong on this since I use hooks
initState() {
context.read(databaseProvider).state = FirestoreDatabase(uid: user.uid);
}
// Note this update logic of the databaseProvider state could also be anywhere else such as in
// an onTap or other such reaction to user input
final jobsStreamProvider = StreamProvider.family<List<Job>>(
(ref) => ref.watch(databaseProvider).jobsStream(),
);
// in your Jobs Page widget
final asyncValue = watch(jobsStreamProvider)); |
@TimWhiting is correct, especially on:
(Although ScopedProviders can read other ScopedProviders if they need to) In general, ScopedProvider is quite a niche. It's here mostly for rebuild optimizations and custom themes. He is also correct in suggesting StateProvider, which is usually the easiest solution. |
The answer should come naturally. Providers have only access to |
@rrousselGit Maybe a good lint would be for this case where a user is trying to use the watch method of the Consumer or ConsumerWidget inside a definition of a provider... |
Though I guess you do get the compile time error already. |
How is this possible? It is likely that @bizz84's confusion is caused by how he declares his providers directly in the I'll definitely add a lint against writing: Widget buid(...) {
final provider = Provider(..);
} |
@rrousselGit I think it might be helpful to have some documentation on how to think about architecture in an app using Riverpod. So in this case we have: If you can create this dependency graph for the dataflow: I think this is understood by you and me, but I don't think it is explicitly mentioned or pointed out anywhere. The focus of the documentation is currently more on
But I haven't seen as much on
I think this is why there are a lot of questions in the issues that are not actual bugs, but misunderstandings on how to use it in a good way. |
Agreed. I'll definitely improve the documentation when I can. But for now, I need to gather more data on how people naturally think with Riverpod. |
I understand. I'm an PhD Student, and currently we are onboarding a few new people in our research lab. I'll definitely take notes on what can be improved in the documentation from the things they run into. I can also help with documentation a bit. Maybe next week I can submit a PR with some documentation or a Cookbook looking at it from the dataflow perspective, and maybe sometime I'll have some time to write some documentation on making asynchronous dependency chains synchronous via StateProvider. |
That would be lovely, thanks in advance! |
Is this the recommended way you describe? The example below will become a bug in my code if i am going to use final ctrl = StateController<Foo>();
final fut = FutureProvider((ref) async {
final data = await doStuff();
ref.read(ctrl).state = data;
return data;
});
final becomeSync = Provider<Foo>((ref) {
return ref.watch(ctrl.state);
});
|
Don't do that Reverse the dependency instead: final ctrl = StateProvider<Foo>((ref) {
final foo = Foo();
ref.watch(ctrl.future).then((value) => foo.state = value);
return foo;
}); That makes the state declarative and with a uni-directional data-flow. That could be critical if, say, |
@TimWhiting @rrousselGit thanks a lot for all the answers. These clear some confusion for me. I have some follow-up questions about asynchronous dependencies at runtime. In my app I was using the data from a final authStateChangesProvider = StreamProvider<User>(
(ref) => ref.watch(firebaseAuthProvider).authStateChanges());
class AuthWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, ScopedReader watch) {
final authStateChanges = watch(authStateChangesProvider);
return authStateChanges.when<Widget>(
data: (user) => _data(context, user),
loading: () => const Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
),
error: (_, __) => const Scaffold(
body: Center(
child: Text('Error reading authStateChanges()'),
),
),
);
}
Widget _data(BuildContext context, User user) {
if (user != null) {
return ProviderScope(
overrides: [
databaseProvider
.overrideAs((watch) => FirestoreDatabase(uid: user.uid)),
],
child: HomePage(),
);
}
return SignInPage();
}
} The suggested alternatives were to:
useMemoized((_) {
context.read(databaseProvider).state = FirestoreDatabase(uid: user.uid);
});
initState() {
context.read(databaseProvider).state = FirestoreDatabase(uid: user.uid);
} I dislike 2) because I'd like to keep my setup with a I'm not sure how 1) would work in practice. Should my Maybe my goal could be rephrased as: How can I update a |
Ah I answered that in your PR before seeing your comment. TL;DR: final databaseProvider = Provider<FirestoreDatabase>((ref) {
final auth = ref.watch(authStateChangesProvider);
if (auth.data?.value?.user?.id != null) {
return FirestoreDatabase(auth.data.value.user.id);
}
return null;
}); Benefits:
|
@rrousselGit Ah yes, I think I missed that in the previous comments. Looks very clean, everything in one place. I like it! One question: what happens when there are some listeners to some of the streams in the database, and the user logs out? Would any dependant Just wondering how things look from a lifecycle point of view when the conditions inside the providers change. |
When the user logs out, It's kinda magical. Everything that needs to rebuild do so automatically 🤷 |
Suppose I have this code: final jobsStreamProvider = StreamProvider.family<List<Job>, FirestoreDatabase>(
(ref, database) => database.jobsStream(),
);
// in build method:
final jobsStream = watch(jobsStreamProvider(watch(databaseProvider))); Should I change If so, should I null check this variable before using it? final jobsStream = watch(jobsStreamProvider(watch(databaseProvider))); It does indeed seem a big magical - though I want to make sure I'm using it correctly. |
To begin with, that initial code looks weird. Why not do: final jobsStreamProvider = StreamProvider<List<Job>>((ref) {
final database = ref.watch(databaseProvider);
return database?.jobsStream() ?? Stream.empty();
}); |
Cool. I think I understand enough now. All runtime logic that handles the dependencies between providers can go inside the declaration of the providers themselves. Which is super nice because it's completely separate from the widgets themselves. Good stuff 👍 |
Depending on what your DB object is doing, you may also want to dispose it to close streams and cancel pending requests. You could do that with: final databaseProvider = Provider<FirestoreDatabase>((ref) {
final auth = ref.watch(authStateChangesProvider);
if (auth.data?.value?.user?.id != null) {
final db = FirestoreDatabase(auth.data.value.user.id);
ref.onDispose(db.dispose);
return db;
}
return null;
}); With this, when the user log-out, this will dispose of the previous DB object |
@rrousselGit An example like that one or similar would be very welcome in the docs. I happened to come across to a similar need, where I initialize some asynchronous providers before the app starts, but I want to use those providers without handling the future states (loading/error), since they have already been handled when starting the app. |
In my case my DB is stateless and only exposes streams. The |
With This matters because of Say you have: final a = StreamProvider(...);
final b = WhateverProvider((ref) {
ref.watch(a);
ref.onDispose(<do something>);
}); in this situation, when That is because we actually have a tree. |
To reinforce the fact that providers aren't global, we can't do: final a = WhateverProvider((ref) {
ref.watch(b);
});
final b = WhateverProvider((ref) {
ref.watch(a);
}); That will throw a runtime exception because the dependency graph of providers must be uni-directional. As a consequence, if |
That's a quite advanced discussion though. I'm not sure how to explain this concept to a beginner. Understanding that concept is an important step to mastering the power of |
@rrousselGit people will be familiar with the widget tree. So if providers are arranged as a tree-like structure based on their inter-dependencies, you could say so in the documentation. Maybe it would be useful to see a flow diagram for a simple example app, showing how providers are added/removed from the tree when certain events happen, alongside changes to the widget tree. Though it's just an idea and I'm not sure how it should look in practice. |
Ok, it was very hard to me to understand this since i'm so new in Flutter. I have the next use case(similar one): final userRepositoryProvider =
Provider<UserRepository>((ref) => AmplifyGraphQLUserRepository());
final userEntityProvider = Provider<User>((ref) => User.empty);
final homeScreenProvider = StateNotifierProvider<UserNotifier>((ref) {
final userEntity = ref.watch(userEntityProvider);
final userRepository = ref.watch(userRepositoryProvider);
return UserNotifier(currentUser: userEntity, userRepository: userRepository);
}); I need my userEntity to be provider after logging in, and i tried by loggedIn: (loggedInUser) {
context.read(homeScreenProvider).setCurrentUser(loggedInUser);
context.read(homeScreenProvider).deliverUserScreen();
pushAndReplaceToPage(context, HomeScreen());
},
Is it the best way to work with? |
I would make your userEntityProvider a StateProvider and set it's state to the logged in user. It makes more sense there since you won't always be on the home screen, and the homeScreenProvider watches for changes in the userEntityProvider anyways |
Describe the bug
Currently unable to get data from a
StreamProvider
that is created from a stream that comes from aScopedProvider
.Note: I may be using this wrong. If so, apologies. In any case I hope this can help to improve the documentation.
To Reproduce
Here's some minimal code from my app:
I'm currently experiencing two problems with the code above.
If I create the
jobsStreamProvider
like this (by using thewatch
argument from thebuild
method):then the code compiles and runs, but the UI is stuck in the loading state (that is, I never get
data
insidejobsStream.when
).If instead I try to use
ref
to get thedatabaseProvider
:then I get the following compile error:
Additional questions:
StreamProvider
that depends on aScopedProvider
that is only usable after it is overridden, does it make sense to create it locally inside abuild
method, rather than on the global scope?ref.watch
as opposed to justwatch
from aConsumer
orConsumerWidget
?Expected behavior
No compile error, StreamProvider produces data rather than being stuck on loading state.
The text was updated successfully, but these errors were encountered: