How should I manage the state of a large model in Flutter?
Asked Answered
D

1

7

I'm writing my first Flutter app and struggling with the variety of state management solutions. I decided to start with Provider, but I'm thinking about switching over to BLoC. So far most of the examples I've found are limited to relatively simple things, like showing a list of items or responding to some button presses. In my case, almost all of the app is focused on setting up a rather large hunk of data. (It's basically a whole bunch of forms all working on different bits of a large data structure.)

At the moment, all of the state management is put together into one provider class because most of it is very closely related. For instance, the biggest part of it is a list of items, and then a bunch of subsets of that list. The majority of the data manipulation in the app is on those subsets.

I wasn't really intending to do this in the beginning, but I've found myself putting the code that actually uses the provider pretty close to the top level, and then passing data down the tree. It goes against the whole point of Provider, but it's resulted in less duplicated code. For example, one of the screens in my app has a bunch of cards that all contain very similar lists. The main difference between them is that their contents comes from different lists. So in order to reduce duplicated code, I generalized the cards and lists as much as possible, and passed the necessary data down the tree. I'm also passing down callback functions to handle things like editing an item or removing it from the list.

Does this sound like an okay way to go about it? The fact that I'm passing state and callbacks down the tree feels like a code smell to me, but as I mentioned, it's resulted in less duplicated code. What about the fact that I have a single provider that exposes a lot of stuff? Part of me feels like I should have the data model separated from the provider, and then have multiple smaller providers. If I were to go that route, would BLoC be a better fit than Provider? Would there potentially be a performance boost if I'm able to break up the single provider? Having multiple providers or blocs seems like it could complicate saving changes, but maybe not. Have you ever found that to be an issue?

EDIT: I just found this question, which talks about reusing providers for different data: Flutter Provider. How to have multiple instances of the same provider type?

I hadn't really thought of it like that before, but that's a big part of what I'm struggling with. I have different lists that are modified independently. It's the same widgets that are used in multiple places (sometimes on the same screen), and the functionality in the provider would also be the same if I broke what I have up into smaller pieces. Since Provider is based on type rather than instance, maybe it just isn't going to work in this case.

Dowden answered 22/9, 2021 at 3:42 Comment(0)
C
3

My first Flutter app used a single Provider class and it worked but was a giant messy class and triggered tons of UI updates.

Currently I use a "models" folder for data models and "services" folder for Provider singletons.

Models has all my data classes with utility and mutator methods. For example a MyCard class to hold values plus MyCard.fromJson() and .toJson() for serialization, and maybe some routines like clear() and validate().

Services is where I create Providers as singletons. For example, a class MyCardService with ChangeNotifier singleton that has a List<MyCard> to hold all the MyCards. If I want a UI widget to update based on changes, like when MyCardService().loadCardsFromNetowrk() completes, I access the service in a build() method using Provider watch or select. If I want to access data outside the build() chain, I access it with the singleton pattern: MyCardService().myVariable. This removes the need for context lookups outside the build() chain.

This provides a lot of control for async access. For example you can have a mutex on get and set methods, a Future<bool> isInitialized that resolves as true or false after your service tries to initialize. You can await this future in any async call that needs to access the service or a FutureBuilder()

I also have some non-Provider service singletons that other services use for stuff like network API access and filesystem caching.

The provider services are mostly organized by UI data consumption. This way a notifyListeners() doesn't trigger UI rebuilds for unmodified widgets (a major problem with my first single-file provider project that resulted is a bunch of individual variables using select instead of one watch).

In the above example, the Provider singleton holds a List and all the methods for working on the List<>. The MyCard data model holds all the methods for working on a MyCard. When I have a widget that edits a single MyCard, I use a stateful widget that does a MyCardService().save(myCard) when editing is finished. This also helps isolate UI tree rebuilds.

Conjunctivitis answered 22/9, 2021 at 6:37 Comment(3)
Thanks @Pat9RB. Do you have any separate services that work on the same model? Like, if you had a Foo model alongside MyCard, and a Bar model that contained lists of both MyCard and Foo, would you potentially have separate services for modifying each of those lists? Regarding the singleton - I've been using Provider.of<T>(context, listen: false) to accomplish the same thing. Is there any advantage to using a singleton instead?Dowden
I don't have any services that hold the same data, but I do have them cascade. I do have services that provide multiple models as one unit - like a PersonService might contain models: complexAddress, socialAccounts, txHistory, etc. if I want those all on one view. I'd have a "DisplayPerson" widget looking at the PersonService. If I had a separate view that only had txHistory I'd have it instead in its own TxHistoryService with a ShowTxHistory widget, and when I wanted both displayed on the same view put both DisplayPerson and ShowTxHIstory wigets in a column or listview, etc.Conjunctivitis
The singleton model lets you use the service where you don't have a Flutter context to look it up, for example from another service. But I end up using it pretty much everywhere I don't need the listener connection because it's cleaner, i.e. MyService().doSomething() or MyService().value vs context.read<MyService>().doSomething() or Provider.of<MyService>(context, listen: false).valueConjunctivitis

© 2022 - 2024 — McMap. All rights reserved.