preface

Flutter, Google’s mobile UI framework, has gained a lot of popularity this year. In my actual project, I found that THE experience of FLUTTER was good. Of course, a good experience of FLUTTER requires a deep understanding of the state management of flutter. To understand the state management of FLUTTER, it is a good way to learn about the official recommended state management tool of Flutter — Provider. That’s why I’m writing this article. One can summarize the knowledge they have learned, deepen the impression. Second, I hope to help more people use providers well. Due to my limited level, the article inevitably has mistakes and omissions, I hope we can criticize and correct.

1.1 Start from the state management of FLUTTER

Reactive programming languages almost always involve the concept of state management. What is a state? As far as the user can see the interface, we can see the appearance, such as the color of the button, which can be called a state. The user’s data, such as the name of a song in a music playlist, can also be called a state. For the part that users can’t see, such as the current state of the phone’s network, Bluetooth and GPS, it can also be called a state. Of course, for those invisible states, there are also ways to visualize them in the form of an interface, so let’s not worry about the details here.

How do you manage these states? According to the methods recommended in Flutter Real, there are three ways to manage the status of widgets that have a parent or relative relationship:

  • Widgets manage their own state.
  • Widgets manage child Widget state.
  • Mixed management (both parent and child widgets manage state).

Interested friends can go to see the contents of the book. I will not repeat it here.

The above method only manages the state of widgets that have a parent or relative relationship. But what about states that don’t have relatives, such as across routes? There are two ways.

One is to learn from the way the state is managed between the parent and child widgets. Raise the state that needs to be shared in the Widget high enough, and then update it through the subscriber mode notification interface when the state changes.

The alternative is to use the InheritedWidget in flutter. A Provider is essentially an encapsulation of the InheritedWidget in flutter. We’ll talk more about this later. While the first method is not recommended, learning about this method can help us understand the inherent itedWidget design in Flutter. So I’ll give you a detailed example to help you understand this kind of state management. Here’s an example:

1.2 No State management instance under InheritedWidget

Now suppose we have a requirement to write a progress bar control that returns a progress value indicating the current progress each time we drag the progress bar. For convenience, we use an integer value to represent the progress of the bar, leaving out the interface and easily writing code like the following (which is actually a counter) :

class ProgressBar extends StatefulWidget {
   ProgressBar({Key? key,required this.onProgressChange}) : super(key: key);
   final void Function(int value)? onProgressChange;
 
  @override
  _ProgressBarState createState() => _ProgressBarState();
}
 
class _ProgressBarState extends State<ProgressBar> {
  int progressValue = 0;
 
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Container(
          color: Colors.blue[100],
          child: Text("value:$progressValue"), ), ElevatedButton(onPressed: () { progressValue++; setState(() { widget.onProgressChange! .call(progressValue); }); }, child: Text("increase")),,); }}Copy the code

We use the ProgressBar like this:

@override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text(widget.title),
        ),
        body: Center(
          child: ProgressBar(onProgressChange: (value){
           print("current progress value:$value"); },),)); }Copy the code

This is an example of a child widget managing its own state and then returning the value of the progress bar to the parent widget via a callback when the button is clicked.

So far, there is nothing wrong with the code above, and the parent Widget gets the state of the child Widget perfectly. However, if there is a need to control the progress of the progress bar outside of the progress bar (for example, sliding the screen instead of dragging the progress bar), the above notation will not apply. Because the parent Widget has no control over the state of the child widgets.

How to solve this problem? The obvious state that needs to be shared here is the value of progressValue, and we want to promote this state, for example by referring to the parent Widget, so that we can control the progress of the progress bar through the parent Widget. We add a ProgressBarController member to each ProgressBar, and the parent Widget holds the ProgressBarController and passes the ProgressBarControlle to the child Widget. We expect the ProgressBarController to control updates to the state of the child widgets. The code is as follows:

class ProgressBarController {
  int progressValue = 0;
}
 
class ProgressBar extends StatefulWidget {
  ProgressBar(
      {Key? key,
      required this.onProgressChange,
      required this.progressBarController})
      : super(key: key);
  final void Function(int value)? onProgressChange;
  final ProgressBarController progressBarController;
 
  @override
  _ProgressBarState createState() => _ProgressBarState();
}
 
class _ProgressBarState extends State<ProgressBar> {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Container(
          color: Colors.blue[100],
          child: Text("value:${widget.progressBarController.progressValue}"),
        ),
        ElevatedButton(
            onPressed: () {
              setState(() {
                widget.progressBarController.progressValue++;
              });
            },
            child: Text("increase")),,); }}Copy the code

Use the progress bar component above:

  late ProgressBarController progressBarController;
 
  @override
  void initState() {
    super.initState();
    progressBarController = ProgressBarController();
  }
 
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text(widget.title),
        ),
        body: Column(
          children: [
            Center(
              child: ProgressBar(
                progressBarController: progressBarController,
                onProgressChange: (value) {
                  print("current progress value:$value");
                },
              ),
            ),
            ElevatedButton(
              onPressed: () {
                progressBarController.progressValue++;
              },
              child: Text("increase child"),),])); }Copy the code

The running results are as follows:

You can see that there are two buttons, increase is the Widget managed by the progress bar itself, and Increas Child is the button in the parent Widget that we expect to click. The value of value is updated. However, when I click the button below, the value of value on the interface is not updated. What’s the problem? A closer look at the code shows that the parent Widget does not call setState() when the button is clicked; instead, setState() is called. The code is as follows:

// before			
ElevatedButton(
    onPressed: () {
        progressBarController.progressValue++;
    },
    child: Text("increase child"),),// after
ElevatedButton(
    onPressed: () {
        setState(() {
            progressBarController.progressValue++;
        });
    },
    child: Text("increase child"),),Copy the code

The requirement is solved, but what’s the problem with writing this way?

A serious problem is that we call the setState () function in the parent Widget when we are updating the state of the child Widget. In this way, we update the parent Widget, and the parent Widget updates the child Widget. Obviously the parent widget should not be updated unnecessarily.

Is there a way to update the state of the child Widget without changing the parent Widget? The answer is the subscriber model. You just need to notify the child widgets of updates when the parent Widget’s button is clicked. This mode can be easily implemented using the changeNotifier in Flutter as follows:

class ProgressBarController extends ChangeNotifier{
  int progressValue = 0;
 
  increaseProgressValue(){
    progressValue++;
    notifyListeners(); // The notification interface is updated}}Copy the code

Add a subscription event in ProgressBar using ProgressBarController:

class ProgressBar extends StatefulWidget {
  ProgressBar(
      {Key? key,
      required this.onProgressChange,
      required this.progressBarController})
      : super(key: key);
  final void Function(int value)? onProgressChange;
  final ProgressBarController progressBarController;
 
  @override
  _ProgressBarState createState() => _ProgressBarState();
}
 
class _ProgressBarState extends State<ProgressBar> {
  @override
  void initState() {
    super.initState();
    widget.progressBarController.addListener(_update);
  }
 
  @override
  void dispose(){
    widget.progressBarController.removeListener(_update);
    super.dispose();
  }
 
  _update(){
    setState(() {
    });
  }
 
  @override
  Widget build(BuildContext context) {
    print("build.");
    return Column(
      children: [
        Container(
          color: Colors.blue[100],
          child: Text("value:${widget.progressBarController.progressValue}"),
        ),
        ElevatedButton(
            onPressed: () {
              widget.progressBarController.increaseProgressValue();
            },
            child: Text("increase")),,); }}Copy the code

Use:

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text(widget.title),
        ),
        body: Column(
          children: [
            Center(
              child: ProgressBar(
                progressBarController: progressBarController,
                onProgressChange: (value) {
                  print("current progress value:$value");
                },
              ),
            ),
            ElevatedButton(
              onPressed: () {
                progressBarController.increaseProgressValue();
              },
              child: Text("increase child"),),])); }Copy the code

Clicking the Increa Child button now eliminates the need to call setState () in the parent Widget, and the parent Widget does not need to be updated. If ProgressBarController is defined as a global variable, state management across routes can be achieved.

The above code can achieve global state management, but subscribing to events each time and then removing them is too cumbersome when the project is complex. Therefore, it is not suitable for the control of business logic, but more suitable for the state management of custom widgets. For example, the progress bar control in this example (although there is no interface, it can be improved later).

The ideal approach is to use an InheritedWidget, or borrow a Provider plug-in that encapsulates the InheritedWidget component.

1.3 InheritedWidget

What is an InheritedWidget? According to the description of Flutter In Action:

InheritedWidget is an important functional component of Flutter. It provides a way to share data from top to bottom in the widget tree. For example, we share data with the InheritedWidget in the root widget of Flutter. Then we can retrieve the shared data in any child widget! This feature comes in handy in scenarios where you need to share data across the widget tree! For example, the Flutter SDK uses inheritedWidgets to share app Theme and Locale information.

Because using a Provider doesn’t require direct manipulation of the InheritedWidget, I won’t cover it here. Let’s look directly at how to use a Provider to implement state management for a progress bar like the one above.

1.4 Using The Provider to Manage the status

Let’s take a look at using a Provider to achieve the same progress bar.

First, the ProgressBarController code can be left unchanged:

class ProgressBarController extends ChangeNotifier {
  int progressValue = 0;
 
  increaseProgressValue() {
    progressValue++;
    notifyListeners(); // The notification interface is updated}}Copy the code

Then, when the program starts, start the MultiProvider directly and create the “state” that needs to be shared, in this case the ProgressBarController. As the name suggests, MultiProvider can create multiple different “states.” Of course, these states can’t be the same. The code is as follows:

void main() {
  runApp(MultiProvider(
    providers: [
      ChangeNotifierProvider(create: (_) => ProgressBarController()),
    ],
    child: MyApp(),
  ));
}
Copy the code

With a Provider, the implementation of the ProgressBar becomes concise. Call the context.watch() method. This method looks for the ProgressBarController ancestor closest to the ProgrsesBar. Find what can be read from ProgressBarController later. So here’s progressValue. The code is as follows:

class ProgressBar extends StatefulWidget {
  ProgressBar({Key? key}) : super(key: key);
 
  @override
  _ProgressBarState createState() => _ProgressBarState();
}
 
class _ProgressBarState extends State<ProgressBar> {
  @override
  Widget build(BuildContext context) {
    return Container(
        child: Text("value:"+ context.watch<ProgressBarController>().progressValue.toString())); }}Copy the code

How do I use the ProgressBar after using a Provider? You can also get the ProgressBarController instance by calling context.read(), and then call the increaseProgressValue() method. Since the notifyListeners() are called in the increaseProgressValue() method, the value is updated.

class _MyAppState extends State<MyApp> {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text("learn provider")),
        body: Column(
          children: [
            Center(
              child: Container(
                child: ProgressBar(),
              ),
            ),
            ElevatedButton(
                onPressed: () {
                  context.read<ProgressBarController>().increaseProgressValue();
                },
                child: Text("increase"() [() [() [() [() }}Copy the code

What is the difference between context.read() and context.watch()?

We change watch to read in the code above and notice that the value of value is not updated when the button is clicked. The answer is obvious by now. Use the watch method to get an instance of the ProgressBarController. The widget that calls the watch method listens for state changes on the ProgressBarController. Read simply gets an instance of ProgressBarController and does not listen for state changes.

In this case, can we use the Watch? The answer is that it is not recommended because it incurs an unnecessary performance drain.

Read and Watch encapsulate the provider.of () method.

Context. read is equivalent to Provider. Of (context,listen:false);

Context.watch () equals Provider. Of (context,listen:true);

1.5 Consumer method

As mentioned in the previous section, you can update the interface by listening to the Model layer data through context.watch(). This is sometimes too cumbersome and not intuitive. The Provider encapsulates a series of Consumer methods to get the state. Specific usage:

Consumer<ProgressBarController>{
    builder:(){
        // ...}}Copy the code

The builder method returns the control used to show the progress, which is intuitive and concise. Let’s keep it up until someone sees it.