/flutter-provider-example

State management in Flutter using provider package

Primary LanguageCMake

flutter_provider_example

A simple bare-bones provider example to illustrate clearly the use of the provider package for state management.

Code analysis

This description assumes that you have a basic uderstanding of Flutter stateful and stateless widgets.

The provider package is the simplest and the most elegent package in Flutter world for state management.

What is state?

If you look at the basic project generated by:

  flutter create --project-name ...

It has a MyHomePage as a stateful widget. Here the "state" is a simple integer value, but in a real world application, this would be the data needed by your application. For example, if you were writing a Todo app, your state would be the list of Todo's.

This project takes the counter example and uses a provider class to do the state managent.

In order to understand the provider package, you need to understand three key classes, viz:

  1. ChangeNotifier
  2. ChangeNotifierProvider
  3. Consumer

The use of the first two classes is very important.

ChangeNotifier

The class which manages the state and encapsulates the business logic. It must either inherit from ChangeNotifier class or use the with clause. In the present example, this is CounterProvider class.

ChangeNotifierProvider

This class MUST be the parent of the tree to which the state data needs to be made available.

In the present example, this is done in main.dart:

Widget _wrapHomePageWithProvider() {
    return ChangeNotifierProvider<CounterProvider>(
      create: (context) => CounterProvider(),
      builder: (context, child) => const HomePage(),
    );
  }

The ChangeNotifierProvider requires us to provide a create function and a builder function. The create function should instantiate the provider class and the builder function should build the widget.

You may even wrap the entire app with a ChangeNotifierProvider like this:

void main() {
  runApp(ChangeNotifierProvider<CounterProvider>(
    create: (_) => CounterProvider(),
    builder: (context, child) => const MyApp(),
  ));
}

In this case, the provider lifetime will be that of the entire app. However, this will be fine for a simple app only.

The Consumer widget

Once the Provider has been injected into the Widget tree, there are several ways to access the provider and it's data. In the present example, we have used a Consumer widget (also defined in the Provider package) to access the provider data and methods.

The Consumer widgets constructor is defined in the Provider packge as:

class Consumer<T> extends SingleChildStatelessWidget {
  Consumer({
    Key? key,
    required this.builder,
    Widget? child,
  }) 

As you can see from the above snipet extracted from the Provider library, the Consumer widget's constructor requires a builder method. This builder method gets the provider instance as the second parameter. Using the consumer widget also sets up a dependency between the provider and the widget in that whenever the provider calls "notofyListeners", the widget wrapping the consumer gets rebuilt. Hence, the widget displaying the state data can be a stateless widget. Now the widget is ONLY responsible in displaying the data and is very clean.

Using the Consumer widget wraps the whole widget and the whole widget gets rebuilt. This can be very expensive if the state is complex and the tree consists of several widgets. If this were so, we should not use the consumer widget and instead use the other methods outlined below.

If some data items of the state are needed only for display and do not change. In this case, we can use:

  String title = Provider.of<CounterProvider>(context, listen:false ).title;

If we use this construct, then the widget will not be rebuilt. We can use this in some part of the subtree which does not need to be rebuilt with every state change. If however we pass the listen param as true, then this establishes a dependency.

In this example, the AppBar will be rebuilt even though we are using the Listen = false in the above call Don't do this in production code!

There is another set of calls that can be used:

  String title = context.read<CounterProvider>().title;  // readonly connectioon
  context.watch<CounterProvider>();   // establishes a dependency between the provider and the widget

The above two methods are the preferred way because they explicitly state the dependency.

All these are defined in Provider.dart and are relatively easy to understand once you understand the basics.

I usually have a provider for each functional area of the application. For example, I typically use a LoginStatePovider to encapsulate the login functioonality, a ShoppingCartProvider to provide the logic needed for a the cart etc.

If the business functionality needs a state machine, I use a state variable in the provider and update the state. The widgets display different parts of the UI depending upon the state. Another provider which can be very useful in this scenarion is the BloC component. Happy Coding!