In this article, I'd like to talk about how I ended up using Riverpod as a state management system for my flutter app.
- Provider with MVVM
get_it
for global dependencies- Riverpod
Provider with MVVM
When I started the flutter project as a newbie two years ago, I wanted to build something similar to the MVVM pattern I used to use for Android application development. After some research, I foundProvider the most straightforward option. (I wasn't even thinking of state management at that time)
So, I was doing something like this at the entry point of the app:
finalserviceA=ServiceA();finalserviceB=ServiceB();MultiProvider(providers:[Provider<RepoA>(create:(_)=>RepoA(service:serviceA)),Provider<RepoB>(create:(_)=>RepoB(service:serviceA)),Provider<RepoC>(create:(_)=>RepoC(service:serviceB)),],child:MaterialApp(),)
I've been initializing theservices
andrepositories
at the application launch time and adding them into the Provider using theMultiProvider
. It allowed theViewModels
of each page to access the repositories. (Yes, oneview
, oneViewModel
)
classViewModelAwithChangeNotifier{ViewModelA(this.repo){init();}finalRepoArepo;Stringtitle;Future<void>init()async{title="test";notifiyListeners();}}// Wrap the page with ChangeNotifierProviderclassPageAextendsStatelessWidget{@overrideWidgetbuild(BuildContextcontext){returnChangeNotifierProvider(create:(_)=>newViewModelA(Provider.of<RepoA>(context,listen:false)),child:PageView(),);}
I could access the' ViewModel' in any widgets used in thePageView
using theSelector
.
Selector<ViewModelA,String>(selector:(_,vm)=>vm.title,builder:(_,data,__){returnText('title:${data}');})
It was annoying to write the boilerplate codes of theSelector
every time I needed to use variables in the ViewModel, but I helped to reduce unnecessary rendering. So far, It's not bad.
After growing the code bases with many repositories and services, I realized that every time I add more repositories in the global provider scope with theMultiProvider
, it creates the cascading tree in the widget hierarchy. It made debugging super tricky with the Widget Inspector tool.
One day, I decided to removeMultiProvider
and convert all global dependencies as singletons. Technically they were already singletons but just living in the Provider scope anyway.
get_it for global dependencies
After some research, I found theget_it looked promising.
I replacedMultiProvider
with getIt singletons at the application start:
finalserviceA=ServiceA();finalserviceB=ServiceB();getIt.registerSingleton<RepoA>(RepoA(service:serviceA));getIt.registerSingleton<RepoB>(RepoB(service:serviceA));getIt.registerSingleton<RepoC>(RepoC(service:serviceB));
And the View and ViewModel like this:
classViewModelAwithChangeNotifier{// This allows passing mocked repository to the ViewModel for testing purposesViewModelA(RepoA?repo):_repo=repo??GetIt.get<RepoA>(){init();}finalRepoA_repo;Stringtitle;Future<void>init()async{title="test";notifiyListeners();}}classPageAextendsStatelessWidget{@overrideWidgetbuild(BuildContextcontext){returnChangeNotifierProvider(create:(_)=>newViewModelA(),child:PageView(),);}
Now, I don't see the cascading tree in the widget inspector, and much more comfortable debugging the widgets. I also liked it because the widget doesn't need to know what dependencies the ViewModel needs. Thanks to the GetIt can give it to me withoutcontext
. But I still have a slow application start problem.
A few months later, I wanted to tackle the start-up time issue and realized I might have a few options.
- Use lazy singleton with GetIt.
- Go for another approach.
I could have converted the global dependencies to lazy singleton with GetIt so that it doesn't need to initialize all dependencies at the application start. However, It would still keep in the memory until the app completely shut down. I wanted something that could automatically initiate and de-initiate based on the need.
So another round of research started!
Riverpod
In the meantime, I also was working on a react project that uses theReact Query, and even though it's not a client-side state management library, I found it a simple and nice way of managing states. I wanted to have something similar but for flutter. Finally, theRiverpod caught my eye. The developer of the Provider developed Riverpod as a successor of the Provider. (Later, I foundRecoil is the one I've been looking for in the React app for client state management, and it has a lot of similarities with Riverpod)
As a first step, I converted all singleton services and repositories into Riverpod providers. I also definedabstract classes
for better testability.
abstractclassServiceA{}classServiceAImplimplementsServiceA{}abstractclassServiceB{}classServiceBImplimplementsServiceB{}abstractclassRepoA{Future<List<Entity>>getMany();}classRepoAImplimplementsRepoA{RepoAImpl(this.service)finalServiceAservice;@overrideFuture<List<Entity>>getMany(){returnservice.getMany();};}abstractclassRepoB{}classRepoBImplimplementsRepoB{RepoBImpl(this.service)finalServiceAservice;}abstractclassRepoC{}classRepoCImplimplementsRepoC{RepoCImpl(this.service)finalServiceBservice;}finalserviceAProvider=Provider<ServiceA>((_)=>ServiceAImpl());finalserviceBProvider=Provider<ServiceB>((_)=>ServiceBImpl());finalrepoAProvider=Provider<RepoA>((ref)=>RepoAImpl(ref.watch(serviceAProvider)));finalrepoBProvider=Provider<RepoB>((ref)=>RepoBImpl(ref.watch(serviceAProvider)));finalrepoCProvider=Provider<RepoC>((ref)=>RepoCImpl(ref.watch(serviceBProvider)));
I also started getting rid of the ViewModels. Since I can now define a state as a provider that is accessible from any widgets, the ViewModel was too heavy as a state. This way, I was able to write more reusable codes.
Create state providers
// Simple future state providerfinalstateAProvider=FutureProvider<List<EntityA>>((ref){returnref.watch(repoAProvider).getMany();});// State provider with notifier that contains methods.finalstateBProvider=StateNotifierProvider<EntityB>((ref){returnStateBNotifier(EntityB(),ref.watch(repoAProvider));});classStateBNotifierextendsStateNotifier<ThemeMode>{StateBNotifier(super.state,this.repo);finalRepoArepo;voidfetch(){state=repo.getMany();}}
I can access the providers from widgets.
classWidgetAextendsConsumerWidget{@overrideWidgetbuild(BuildContextcontext,WidgetRefref){finalstateB=ref.watch(stateBProvider);returnInkWell(onTap:()=>ref.read(stateBProvider.notifier).fetch(),child:Text('${stateB.name}'),);}
With the Riverpod, I could make the state lighter and more reusable. I often create a container widget that holds the state that wraps the pure widgets. This way, the design widget is reusable as a design widget without business context. Providers and widgets are all unit-testable by simply mocking the repository or other dependency of the Provider.
Top comments(2)
For further actions, you may consider blocking this person and/orreporting abuse