In the last blog and webinar on State Management in Flutter, we learned about managing state using Stateful widgets and also saw how this can become difficult to manage as the complexity of the application increases.
In this blog, we will be looking at using the Provider package for State Management in Flutter, this being the Google recommended approach.
What is the Provider Package?
Provider is a wrapper around InheritedWidget that makes it easier to use with less boilerplate code.
Why Use the Provider Package?
- Too much boilerplate code.
- Need to lift the state up
- Need for the widgets to subscribe to the state and listen to changes.
- Instead of traversing through each intermediate level, in order to pass the data and rebuild the subtree
- Only the listening widget should get the change and rebuild. Others should not be impacted.
Important Classes/Objects
In this blog example, we will look at using the below provider classes which cater to the state management requirements to give a seamless flow.
- Provider
- ChangeNotifier
- ChangeNotifierProvider
- MultiProvider
Provider
- The most basic form of provider. It takes a value and exposes it, whatever the value is. It does not notify changes but only a simple way used to avoid making a StatefulWidget.
- Provider is the equivalent of a State.initState combined with State.dispose.
- Create is called only once in State.initState.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class Model { void dispose() {} } class Stateless extends StatelessWidget { @override Widget build(BuildContext context) { return Provider<Model>( create: (context) => Model(), dispose: (context, value) => value.dispose(), child: ..., ); } } |
ChangeNotifier
- The model Class which is to be made available in the application widget tree, extends ChangeNotifier, which is part of Flutter foundation library
- This is where we define the data and methods that represent the application state and make changes to it.
- After updating the state of our data, we call notifyListeners() to notify all the widgets who are listening to this change so that they rebuild and update themselves.
We will build further on the Movies example we used in the last blog on State Management.
The movie model class is defined for the data structure.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import 'package:flutter/foundation.dart'; class MovieModel { String movieId; String movieName; bool isFavorite; String posterUrl; MovieModel( {@required this.movieId, @required this.movieName, @required this.posterUrl, this.isFavorite = false}); void toggleFavorite() { isFavorite = !isFavorite; } } |
The movies provider class is defined by extending the ChangeNotifier as below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
class Movies extends ChangeNotifier { final List<MovieModel> _movies = [ MovieModel( movieId: 'M1', movieName: 'The Godfather', posterUrl: 'https://lunkiandsika.files.wordpress.com/2011/11/the-godfather-alternative-poster-1972-01.png', ), MovieModel( movieId: 'M2', movieName: 'The Notebook', posterUrl: 'http://www.impawards.com/2004/posters/notebook.jpg', ), ]; List<MovieModel> get movies { return _movies; } int get movieCount { return _movies.length; } void updateFavorite(MovieModel movieItem) { movieItem.toggleFavorite(); notifyListeners(); } List<MovieModel> get favoriteMovies { return movies.where((movie) => movie.isFavorite).toList(); } int get favCount { return favoriteMovies.length; } } |
ChangeNotifierProvider
- This class is defined to listen to a ChangeNotifier to expose the instance to its descendants and rebuild dependents whenever ChangeNotifier.notifyListeners is called.
- The parent widget is wrapped in ChangeNotifierProvider with the datatype of the ChangeNotifier Data class.
- This includes a builder method that returns an instance of ChangeNotifier ‘Data’ class, which is used to expose the instance to all the children.
- Use create when a new object is created else use value to refer to an existing instance of ChangeNotifier.
1 2 3 4 5 6 7 8 9 10 11 12 |
//DO create a new object inside create Widget build(BuildContext context) { return ChangeNotifierProvider<Movies>( create: (context) => Movies(), //Use ChangeNotifierProvider.value to provide an existing ChangeNotifier. MyChangeNotifier variable; ChangeNotifierProvider.value( value: variable, child: ... ) |
- In this example, the data is of type Movies, an instance of which will be provided at the top level widget in main.dart as below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return ChangeNotifierProvider<Movies>( create: (context) => Movies(), child: MaterialApp( title: 'Flutter Demo', theme: movieTheme, home: MyHomePage(), routes: { FavoriteMovies.routeName: (context) => FavoriteMovies(), }), ); } } |
Now the data is available on the widget tree ready to be consumed.
Consuming the provided instance
There are two ways how this data can be consumed, updated and used.
Using Provider.of()
- “Obtains the nearest [Provider<T>] up its widget tree and returns its value.” – provider.dart
- This can be used to consume the data and invoke the methods(producers), which have been provided on the widget tree.
- You can opt out to listen to changes in data by giving listen: false option in which case it will only receive the data once.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class MovieListView extends StatelessWidget { @override Widget build(BuildContext context) { Movies movies = Provider.of<Movies>(context, listen: false); return ListView.builder( itemCount: movies.movieCount, itemBuilder: (BuildContext context, int index) { return MovieTile( movieIndex: index, ); }, ); } } |
Using Consumer Widget
- When you call notifyListeners() in your model, all the builder methods of all the corresponding Consumer widgets are called.
- The builder is called with three arguments.
-
- The first one is context, which you also get in every build method.
-
- The second argument of the builder function is the instance of the ChangeNotifier. Using this we will use the data from the provider instance.
-
- The third argument is child, which is useful for optimization of the widget rebuild. If you have a large widget subtree under your Consumer that doesn’t change when the model changes, you can construct it once and get it through the builder.
-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
class MovieTile extends StatelessWidget { final int movieIndex; MovieTile({ @required this.movieIndex, }); @override Widget build(BuildContext context) { return Consumer<Movies>( builder: (context, movies, child) { MovieModel movie = movies.movies[movieIndex]; return ListTile( title: Text( movie.movieName, style: (movie.isFavorite) ? TextStyle(color: Colors.white) : TextStyle(color: Colors.white54), ), trailing: IconButton( icon: (movie.isFavorite) ? Icon( Icons.favorite, color: Colors.red, ) : Icon( Icons.favorite_border, color: Colors.red, ), onPressed: () { Provider.of<Movies>(context, listen: false).updateFavorite(movie); }, ), ); }, ); } } |
MultiProvider
- Another useful class from the provider package that merges multiple providers into a single linear widget tree.
- Used to improve readability and reduce boilerplate code of having to nest multiple layers of providers.
In our Movies example, if we have to include another provider to get the related book details, we will have to include another level of ChangeNotifierProvider as below:
1 2 3 4 5 6 7 8 9 10 11 |
@override Widget build(BuildContext context) { return ChangeNotifierProvider<Movies>( create: (context) => Movies(), child: ChangeNotifierProvider<Books>( create: (context) => Books(), child: MaterialApp( title: 'Flutter Demo', ... ); } |
This can lead to a lot of repetitive code as the application size grows with multiple providers.
To make it easier to understand and with less boilerplate, you can use the MultiProvider as below:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
return MultiProvider( providers: [ ChangeNotifierProvider( create: (_) => Books(), ), ChangeNotifierProvider( create: (_) => Movies(), ), ], child: MaterialApp( title: 'Flutter Demo', ... ); |
Thus by making use of the various classes of the Provider package, we are able to provide data at the top level of the widget tree and be able:
- To consume the data at any lower level without any intermediate dependencies
- To produce changes to the data by updating state through provided methods at any lower level avoiding need for passing callbacks through intermediate levels.
- To listen to changes in the data at any lower level
You can find the complete code for this example at the following git repository.
Best Practices for using Provider
When making use of the provider package for state management in your application, it is advisable to keep a note of the following best practices for building an optimized solution:
- Provide only at the needed level, instead of providing everything at the top level
- Using Provider.of() to consume data, listen to changes only if you need to, otherwise use listen:false
- Using Consumer widget, consume at the specific level in order to avoid rebuilding of the entire tree.
- When using Consumer widget, use the child option to mark part of the independent widget tree which need not rebuild.
- When using ChangeNotifierProvider, use the correct option of create or value based on if it’s an existing value or creating the provider value for the first time.
Conclusion
By making optimized use of the provider package, you can effectively manage state in your applications with less boilerplate. It also provides additional advantages like lazy-loading, easier allocation/disposal of resources and increased scalability in terms of handling complex state components as part of bigger applications.