The concept of state management remains one of the most critical topics in Flutter. This is because everything we do in Flutter, from operations related to receiving information from a user to displaying a piece of data, deals with the state. Therefore, managing this data in the best way possible ensures the application is clean-coded, properly abstracted, operates smoothly, and delivers the best results possible.
Many state management solutions have been developed over the years, each based on the same concept of manipulating or modifying the state in the cleanest and most easily accessible way possible. In this article, we will be building a sample app with one of the best state management packages for Flutter: Provider.
Before we begin, note that this article assumes you have an operational Flutter development environment on your machine, along with working knowledge of Flutter.
Let’s talk about what it means to manage the state in a Flutter application.
The “state” in Flutter refers to the data stored inside a widget that can be modified depending on the current operation. The state of an app can be updated or completely changed at the start of an application, or when a page reloads.
That means everything widgets do requires handling the data retrieved from the user and passing it among themselves to perform one or more operations. Flutter can also use the state to display pieces of information to the user.
The Provider package, created by Remi Rousselet, aims to handle the state as cleanly as possible. In Provider, widgets listen to changes in the state and update as soon as they are notified.
Therefore, instead of the entire widget tree rebuilding when there is a state change, only the affected widget is changed, thus reducing the amount of work and making the app run faster and more smoothly.
Recall what we discussed about Provider earlier: that widgets listen to changes and notify each other if there is a rebuild. As soon as the state changes, that particular widget rebuilds without affecting other widgets in the tree.
Three major components make all of this possible: the ChangeNotifier
class in Flutter, the ChangeNotifierProvider
(primarily used in our sample app), and the Consumer
widgets.
Whatever change in the state observed from the ChangeNotifier
class causes the listening widget to rebuild. The Provider package offers different types of providers – listed below are some of them:
Provider
class takes a value and exposes it, regardless of the value typeListenableProvider
is the specific provider used for listenable objects. It will listen, then ask widgets depending on it and affected by the state change to rebuild any time the listener is calledChangeNotifierProvider
is similar to ListenableProvider
but for ChangeNotifier
objects, and calls ChangeNotifier.dispose
automatically when neededValueListenableProvider
listens to a ValueListenable
and exposes the valueStreamProvider
listens to a stream, exposes the latest value emitted, and asks widgets dependent on the stream to rebuildFutureProvider
takes a Future
class and updates the widgets depending on it when the future is completedStart by creating a new project and add this line to the dependencies block in your pubspec.yaml
file:
dependencies: provider: ^5.0.0
Run the pub get
command to get a local copy of the package:
flutter pub get
Next, we need to create a new Material app in the main.dart
file:
import 'package:flutter/material.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Material App', home: Scaffold( appBar: AppBar( title: Text('Material App Bar'), ), body: Center( child: Container( child: Text('Hello World'), ), ), ), ); } }
Now, create a new class that contains the state data required for the application. Let’s name it UserDetailsProvider
. The UserDetailsProvider
class will declare all the methods dealing with handling the state here.
This class extends the ChangeNotifier
class; ChangeNotifier
provides us access to the notifyListeners
method, which we will use to notify listening widgets to rebuild when the state changes.
We declare two controllers for our TextFormField
: name
and age
. The method for updating the name and age of the user based on user input is also declared in this class.
Everything dealing with the state of the app is declared here:
class UserDetailsProvider extends ChangeNotifier { TextEditingController nameController = TextEditingController(); TextEditingController ageController = TextEditingController(); int _age = 0; String _userName = ''; int get userAge => _age; String get userName => _userName; void updateAge(int age) { _age = age; notifyListeners(); } void updateName(String name) { _userName = name; notifyListeners(); } }
After the name is updated, we call the notifyListeners
method, which informs the listening widgets about a change in the state and, therefore, triggers a rebuild of all relevant widgets.
Now that we have the UserDetailsProvider
class (which handles the state), we need to link the class to the screen by using ChangeNotifierProvider
. Now, wrap the entire app with a ChangeNotifierProvider
in the runApp
method of the main block.
The ChangeNotifierProvider
exposes two important properties: create
and child
. The class we declared, which extends ChangeNotifier
, is passed into the create
property, linking the class to the screen:
import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; void main() => runApp( ChangeNotifierProvider<UserDetailsProvider>( create: (_) => UserDetailsProvider(), child: MyApp(), ), ); // ignore: use_key_in_widget_constructors class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return const MaterialApp( title: 'Material App', home: HomeScreen(), ); } }
Now the app is linked to the class providing the state; whenever there is a change in state, it causes a rebuild of the screens in-app.
Currently, the HomeScreen
widget contains a form with two TextFormField
s to receive the name and age of the user. Also, a RawMaterialButton
is included to save changes after the user has passed in the required details.
After this set of widgets, we have two Text
widgets that display the values given by the user. These two widgets are the only widgets that need to be updated whenever there is a change in the application state.
That means we do not need every screen to rebuild every time there is a change in the state. Therefore, we need a way to selectively rebuild only the Text
widgets concerned with the state change. For that, we have the Consumer
widget.
The Consumer
widget allows only the child widgets to rebuild without affecting other widgets in the widget tree. As stated previously, we want only the text
widgets displaying the details given by the user to update.
We achieve this by wrapping the two Text
widgets with a Column
and returning it at the builder
function exposed by the Consumer
widget:
Consumer<UserDetailsProvider>( builder: (context, provider, child) { return Column( children: [ Text( 'Hi ' + provider.userName, style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), Text( 'You are ' + provider.userAge.toString() + ' years old', style: const TextStyle( fontSize: 18, fontWeight: FontWeight.w400, ), ), ], ); }, ),
Now, only the Text
widgets will update whenever the state changes in the app.
Be sure to use the providers at the lowest level possible; you can use the providers only with the widgets affected. Using it at a high level will cause widgets not concerned with the state change to rebuild. Same thing with the Consumer
widget; make sure you consume at the specific level to avoid rebuilding the entire widget tree.
Our sample app is finally ready!
Emphasis on the importance of state management in Flutter cannot be overstated. Today, we have dissected the Provider package and used it to manage the state of a sample Flutter application. Hopefully, with the hands-on knowledge you have gained by building an app alongside this article, you can now correctly manage the state of your app in a clean and more accessible manner.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.