Flutter Provider — The easiest state management approach one could ask for

When it comes to building apps using Flutter, creating responsive and beautiful User Interfaces is not all there is. Animations, data flow and state management are core parts that cannot be ignored.

In this article, we’ll take a look at the state management approach recommended by Flutter itself — Provider.

State management — What exactly is it?

Everything is a widget in Flutter. So what is a widget? It could be said that a widget is a component. It could be as simple as a Text widget that shows text, or it could be a button that triggers a function when tapped on it. Or it could be an entire screen that holds multiple children widgets under it.

For the sake of answering the question at hand, we’ll consider the last example. Let’s say we have a screen like this:

Banking App UI — https://dribbble.com/shots/21343217-Mobile-Banking-App

In the above image, the entire screen can be a parent widget that contains multiple child widgets like the Text widget to display the total balance, the Switch widget to toggle between Send and Receive modes, etc.

The “Total Balance” value might change in the Future, so if it does change its value, how will it reflect and update in the screen? The parent widget cannot magically know that the Text value has changed unless it is informed that such a change has happened. This is precisely where “State” comes in. If we can provide a state to this widget, it can then maintain the current value and track any changes made to it.

Flutter provides a way to do exactly this — using a Stateful Widget. When we use a Stateful Widget, all the variables we declare inside its “State” can be updated and reflected on the UI by calling the setState() method. When the setState() method is called, the widget is rebuilt with the values of the state variables.

Perfect! Sounds like a charm! So what’s the issue with this? Why do we need something like provider at all?

The thing is, when setState() is called, the entire widget and all the corresponding children widgets are rebuilt. Apart from the very fact that this is a heavy operation, it’s also unnecessary. If only the value of the Text widget has changed, it makes no sense to be rebuilding the entire Widget Tree again. It would instead be much better if there was a way to just update the state of the Text widget, no?

Well, one way to tackle this is to extract the Text widget into a separate widget all by itself and then set its independent state whenever we need to. But how do we do this? Even if we extract the Text widget out into a reusable Stateful widget, how do we manipulate its state from outside the widget itself? We would have to use an InheritedWidget implementation to lift the state outside the widget and make it accessible outside. Turns out, this implementation is actually quite tedious to implement.

Why Provider?

Enter, provider. The provider approach does exactly this. As per its official documentation, it is a wrapper around an InheritedWidget implementation. Using provider, we can modify the values of a widget without having to rebuild the entire Widget Tree.

Implementing the Provider approach:

Great! So now we know that the Provider approach can solve the issue at hand. Let’s get to know more about what goes in implementing the provider.

First off, we need to decide what exactly the provider is going to provide and to what widget(s). For simplicity, we’ll keep the value for the text widget in our provider, i.e. the value for total balance.

Now that we’ve decided what value is to be stored, we need to create a class that extends ChangeNotifier. This is absolutely essential for implementing provider since this ChangeNotifier will broadcast the values wherever we need them. Here’s what an implementation of the provider class will look like:

bank_screen_provider.dart

import 'package:flutter/material.dart';

class BankScreenProvider extends ChangeNotifier {
  String _balance = '';
  String get totalBalance => _balance;

  void setBalance(String balance) {
    _balance = balance;
    notifyListeners();
  }
}

In the above code, BankScreenProvider which extends the ChangeNotifier is our provider class which will act as a central repository for storing data and performing operations on it. The method setBalance() is used to assign value to the variable we’ve created to map the total balance.

You must have noticed that the function is not returning a value. How then, you might ask, will the value be read outside the class? For exactly that purpose, we’ve created the getter totalBalance. Using this getter, we can keep our actual variable (_balance) private and safe and still expose its value wherever needed. This is mostly done to prevent modification of these values from outside the class.

Finally, the last major thing to notice is that the function is calling the notifyListeners() method. Calling this method broadcasts a notification that the values inside the BankScreenProvider has changed.

Now, how do we consume this value from the BankScreenProvider? For that, we need to wrap the relevant widget(s) with a Consumer widget. The Consumer comes along with the provider package and it listens for notifications from the type of Provider we pass to it.

bank_screen.dart

Consumer<BankScreenProvider>(
  builder: (context, provider, child) {
    String balance = provider.totalBalance;
    return Text(balance);
  },
);

As can be seen in above code, we need to give our Consumer a type of our provider (BankScreenProvider in this case). If we do not do this, we won’t be able to access the data inside our provider. Then, the builder parameter of the Consumer widget requires three parameters:

context: An instance of BuildContext which is accessible in every build method

provider: An instance of the provider we’re using (BankScreenProvider in this case)

child: A widget instance that we don’t really need for our use case

Now that we have our Consumer in place, you might ask how do we update the widget inside it whenever we want to? Remember earlier in our provider, we called a function notifyListeners() whenever we make changes to our data? This is precisely what it’s for. Whenever notifyListeners() is called, it sends a notification to all Consumers listening to that provider, telling them to rebuild the widget(s) inside them.

The Final Step

Great! So now we got a Consumer to consume our provider values and a Provider class to supply these values. All that’s left is one last step — Setting the provider scope. Whenever we write a provider, we also need to tell flutter the scope of this provider for it to understand how far up the widget tree the states need to be lifted. This is a crucial step and if missed, will result in an Exception. Go to your main.dart file and wrap your root app widget with a ChangeNotifierProvider widget like this:

main.dart

import 'package:provider/provider.dart';

void main(){
  runApp(
    ChangeNotifierProvider(
      create: (context) => BankScreenProvider(),
      child: MyApp(),
    ),
  );
}

In the future, you might need to add multiple provider scopes for different screens. In that case, you’ll just need to convert this ChangeNotifierProvider widget to a MultiProvider widget and pass it a list of all providers you want to be scoped, like this:

import 'package:provider/provider.dart';

void main(){
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(
          create: (context) => BankScreenProvider(),
          child: MyApp(),
        ),
        ChangeNotifierProvider(
          create: (context) => HomeScreenProvider(),
          child: MyApp(),
        ),
      ],
      child: MyApp(),
    ),
  );
}

You might ask — “By placing the ChangeNotifierProvider on top of the root widget, aren’t we polluting the scope of the provider?” Yes we are. What we are doing right now will allow the entire app to access values from BankScreenProvider and listen to its events. But, it’s not necessary that the provider data will be required in every single screen. So it is usually better to wrap a widget (with ChangeNotifierProvider) two steps above the subject widget in the widget tree.


Alright, with that, you’ve officially learnt how to use the provider state management approach in Flutter! Adios till the next time! If you feel this article was helpful, please share it. Claps are always appreciated as well :)

Adios!

Credits (UI Screen Ref): https://dribbble.com/shots/21343217-Mobile-Banking-App

Did you find this article valuable?

Support Atharva Patwardhan by becoming a sponsor. Any amount is appreciated!