-
-
Notifications
You must be signed in to change notification settings - Fork 956
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
Sync Providers managing the same entity #3781
Comments
This is a complex problem, and folks have raised issues asking how to deal with his before. I'll keep this open as a reminder to document this. |
Can you elaborate which features Riverpod 3 will offer that makes this simpler? Is there any ETA on Riverpod 3.0? |
I would recommend to adapt a programming pattern like the repository pattern. The repository implements your business logic including caching, pagination, filtering, etc... . Then you can build your state management using riverpod around this repository. |
@snapsl The repository pattern is mostly referred to as an wrapper around multiple data sources and is used to abstract away which data source is being used and how data is being fetched/modified. As far as I understand, a repository is stateless and has "nothing" to do with state except it's the gate to the outside world aka. the data layer. I don't quite see how this is solving the issue mentioned above, happy to hear about how you will solve it. I've read a lot of articles about the usage of riverpod but either they don't run into this issue since they are to simple or they don't don't adhere to the intended way on how to use riverpod. (E.g. Andreas Popular Post about riverpod architecture - I've seen a lot of comments by remi that this and that is not how riverpod is intended to be used). But as said, I am happy about further information or your proposal on how to tackle the above mentioned issue. |
Let's stay with the todo example and create a small sample todo repo to make this more clear. class TodoRepo {
TodoRepo(LocalSource, RemoteSource, ...);
List<Todo> getTodoList()...;
Todo getTodo(String id)...;
Todo updateTodo(String id)...;
...
} This holds all the implementation regarding todos (very simplified). A single source of truth that is encapsulated, mockable, and testable. Cool! Your state management then uses this todo repo. @riverpod
TodoRepo todoRepo(Ref ...) => TodoRepo(....); and use it for our state management. @riverpod
class Todos extends _$Todos {
List<Todo> build() => ref.watch(todoRepoProvider).getTodoList();
void updateTodo(String id) {
final todo = ref.read(todoRepoProvider).updateTodo(id);
state = ...;
ref.invalidate(todoProvider(id));
}
...
}
@riverpod
Todo todo(Ref ref, String id) => ref.watch(todoRepoProvider).getTodo(id); Fixed:
In general there is a reason for these programming patterns to exist since other devs had the same problems and came up with solutions that we should use to not make the same mistakes. I hope this helps you with the real application :) |
Making a repository is only a workaround in Riverpod's case. I won't go into detail here as things are still WIP. But I do want to improve this. |
@rrousselGit creating a riverpod pattern 👍 |
@snapsl I think there are multiple scenarios why this might not work: Reason 1: class TodoRepo {
...
// Fetches the given page of the todos, each containing e.g. 20 todos
List<Todo> getTodoList(int page)...;
...
} @riverpod
class Todos extends _$Todos {
List<Todo> build(int page) => ref.watch(todoRepoProvider).getTodoList(page);
void updateTodo(String id) {
...
}
...
} As mentioned in the original issue, lets still assume we are working on a web application where the user might directly navigate to Reason 2: @freezed
class Todo with _$Todo {
const factory Todo({
required String id,
required String title,
required String description,
/// The date and time the todo is scheduled to be
/// worked on/completed.
/// If this is null, the todo has not been scheduled yet.
DateTime? scheduledAt,
/// The date and time the todo has been completed.
/// If this is null, the todo has not been completed yet.
DateTime? completedAt,
/// The date and time the todo has been completed.
/// If this is null, the todo has not been completed yet.
required DateTime createdAt,
}) = _Todo;
factory Todo.fromJson(Map<String, dynamic> json) => _$TodoFromJson(json);
} As you can see, the Now lets assume we have some widget/page in our app that displays:
For each of these we can make an API call that returns a List:
For each of those, we have a provider of course... I don't think I have to show an example on how to create them. Naturally a todo might end up in multiple lists: Where should I place the update method? In every provider? This is what #3285 was initially about as far as I remember (the issue has been edited) I hope I gave you some understandable and "real-life" why I think your proposal does not solve the issue. |
Okay, I am not sure if it helps to stay with the todo example. But this is still the same principal.
class TodoRepo {
List<Todo> get listTodo = [];
List<Todo> getPage(int page)...;
List<Todo> getRecentTodos();
...
}
Note:
|
See above @riverpod
Todo todo(Ref ref, String id) => ref.watch(todoRepoProvider).getTodo(id); |
@snapsl
In order to keep this thread cleaner, I am also down to discuss further on discord if that's okay with you: My name there is same as in Github |
Hello! 😄 It's been a while since I've tried to tackle this problem, and since then I've recently updated the original issue with a summary of the problem:
I want to share my 2 cents and my experience about this. First, it's best to recognize that this is quite a hard problem. We're exploring boundaries that most client-side applications get wrong, even popular ones. I, too, used a "repository pattern" to solve this in the past months, and I'd definitively advise against such an approach. Assuming you're suggesting this repository should save on a local cache whenever new data is received from our network (e.g. with sqlite):
I want to share another workaround while we wait for R3' docs. Given we have some "shared state" family providers, e.g.: The idea is to save "divergent data" on a separate hash-like data structure (here I'm just saving the In my use case, whenever we hit a "like" button, we write an override in there, meaning: "this has been touched, and the source of truth has now changed". If you're curious I'm working on a repository on this topic since a while, which I've just updated. You can check out my implementation example, there. I can't wait for Remi's solution and for R3! |
@lucavenir Thanks for joining this discussion! I also tried a lot of different solutions that I've tried, e.g. caching and streaming from a repository class ("watchXY -> Stream" and every update emits a the updated value...), listen to the "single item" provider and creating it with already fetched values , by creating a class for the params e.g. "ItemReference" where there is a mandatory id property and an optional value property and overwriting the hashCode and equality operator -> This was you can pass "initial" data to a provider, since an "ItemReference" with only id is equal to an ItemReference with a value. The only way that doesn't feel like actually working AGAINST Riverpod came to my mind after reading your comment, so I still have to try how it feels: Create an "ItemSyncService" - A (family) Notifier which state represents the "cached" value.
typedef ItemSyncServiceState = ({
/// The currently cached value, this is nullable since initially we have no value OR in case the item does not exists
Item? value,
/// Used to determine if some provider set the state or if we haven't fetched it yet.
bool hasValue,
/// Used for optimistic updates. When some provider updates this state optimistically, the pending future should be
// set here so other providers can await that future and revert to their "old state" when the operation fails.
Future? pendingOperation,
}); This way if no one is interested in updates to that item or no one is modifying it, the provider will get disposed. Would appreciate your opinion on it and if you thought about it already too or even tried it. |
I also was thinking about this problem as well.
This way I have N streamproviders If I stream the first 50 and then stream 50 more, I could change the
|
Hi! I would like to second the importance of this problem. I'm relatively new with flutter and have started the first more complex app project using provider. I was thinking about following Andrea's architecture but after reading through this issue here I also don't see how this necessarily solves the problem at hand. It would be amazing if riverpod would make this easier / possible and documentation about this would be unprecedented from what I could find 👀 On the risk of diverging from the specific riverpod solution: One thing that I also found regarding this topic was the offline first approach. But that sounds too good to be true to me? Local db as state manager? If the app use case allows to store the entire (or required selected) data in a local db would that solve the problem of outdated data in different places? |
Describe what scenario you think is uncovered by the existing examples/articles
It is quite common to have a List provider as well as an Item provider, especially when dealing with
web applications and (deep) links. It is not well documented on how to share state between provider that might
contain the same Item. To be more precise on what I mean, lets take a look at an example:
Lets suppose we develop an Web-Application with the following paths:
and a simple model of the todo as the following:
As you can see, it is a pretty simple setup:
/todos
shows us an overview of the todos (e.g. only the title and the completion status) while/todos/:id/
will display an detailed version of that todo (e.g. title, status, description).Since this is an (web-) application, the user might navigate to the detailed screen without ever rendering the overview screen.
Lets start pretty simple - just have two separate providers that fetch the todos/todo:
Okay - fair! But now the first problem raises:
We might fetch the same entity twice - first when the user visits the overview and second when he visits the detailed view - pretty wasteful on ressources, no? You might argue now that this can be fixed fairly easy by awaiting the todos and returning the todo as a dependent provider.
But now we still fetch the whole list just to get that one provider, so we can even optimize it further...
Okay. Understandable to this point - TLDR; The detailed provider should check the list provider if it exists, and if yes, check its state so we don't fetch an todo twice - seems logical!
But now lets suppose we don't have a single todosProvider, this can be the due to multiple reasons:
Reason 1: The Todos Provider doesn't fetch all the todos, only those that are not completed (=> isCompleted: false). And we have another Todos Provider (uncompletedTodosProvider) that fetches the uncompleted todos. Now we need to modify our todo provider to check 2 possible lists...
In this case is a little trivial but assume we have something else with a lot more properties and a more complex data model, we could end up with 3-4 places to check. While adding all of those providers to check first before fetching the actual item seem a little annoying, I am willing to accept it BUT:
Reason 2 (prob. more common): The Todos Provider is actually a FamilyProvider since we have several thousand of todos (overall we are pretty busy, no?) so that the fetching of all todos would take a long time and a lot of ressources. So we simply add a
int page
to the params of the todosProvider - Lets make a loop to check every familyProvider(page)... but wait - how far should we check? We don't know how many pages there might be...
Let's mentally reset and assume we found a solution OR just go with the simple overfetching - we fetch our todo it in the todosProvider aswell as in the todoProvider 🤷🏼♂️
But aren't Todos supposed to be updatable? I mean we somehow want to check of our todos, right? This means we have to make our todoProvider an class based (notifier) provider.. But updating the state of this provider doesn't update it within the overviewProvider and vice versa.. How can we keep them synchronized?
There just doesn't seem to be an sufficient example that is "complex" enough to showcase how to use riverpod with
this common use-cases above.
I hope with this little example made my problem clear - if not, I am happy to discuss your solutions and will throw further constraints at you! :D
Describe why existing examples/articles do not cover this case
The docs on the riverpod website cover just simple use-cases, either
using read-only (family) provider or local/simple state notifier.
I have been struggling with the problem mentioned for a long time now
and can't find a general solution to tackle this problem. It seems like
other people having problems with this problem (or a related one) too.
E.g. #3285
The text was updated successfully, but these errors were encountered: