[Flutter] State Management 1: StatefulWidget + setState to provider. Using a counter app as an example

This article can be read in about 19 minutes.

Evolution of State Management in Flutter

In Flutter, you can use StatefulWidget to have internal state.We also use libraries such as Provider, Riverpod and Bloc to manage the state of the entire application.

In this article, we will look at statefulWidget + setState to the next step, Provider. We will touch on Riverpod in another article.

StatefulWidget + setState

With StatefulWidget, unlike StatelessWidget, a widget can have an internal state that can be changed while the application is running. The setState method is then used to rebuild (redraw) the widget when this internal state is changed.

This is the first combination to study in the beginner stage of starting Flutter when you want to create a screen that changes state in response to user actions, such as a counter app.

Example: Counter app

When the user presses a button, the counter increases and the count on the screen updates.

Dart
import 'package:flutter/material.dart';

class Counter extends StatefulWidget {
  // This class is a state setting.
	// It is provided by the parent and holds the values used by the build method of State.
	// Widget subclass fields are always marked as "final".

  const Counter({super.key});

  @override
  State<Counter> createState() => _CounterState();
}

class _CounterState extends State<Counter> {
  int _counter = 0;

  void _increment() {
  // setState call: When setState is called, the Flutter is held by the corresponding State object
  // schedule the build method to re-execute.
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    // This method is reexecuted each time setState is called.
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        ElevatedButton(
          onPressed: _increment,
          child: const Text('Increment'),
        ),
        const SizedBox(width: 16),
        Text('Count: $_counter'),
      ],
    );
  }
}

void main() {
  runApp(
    const MaterialApp(
      home: Scaffold(
        body: Center(
          child: Counter(),
        ),
      ),
    ),
  );
}

When _increment is called, the value of _counter is increased, the build method is re-executed by setState, and the count value displayed in the Text widget is updated.

Dart
        MaterialApp
             |
          Scaffold
             |
           Center
             |
          Counter (StatefulWidget)
             |
        _CounterState (State)
             |
           Row
       /          
ElevatedButton   Text
(Increment)    (Count: x)

Flutter’s build method is re-run each time setState is called, resulting in a rebuild of the entire widget; although a new widget tree is created within the build method, in practice the Flutter framework uses a diffing algorithm to compare it to the previous build and apply only the changes that are actually necessary to the UI.

In this example, when the _counter variable is updated, only the Text widget containing this variable is redrawn. However, the ElevatedButton and other widgets are not modified, as they do not need to be redrawn.

This process is similar to React’s virtual DOM and is designed to minimize unnecessary UI redraws.

Two challenges with setState

Difficulty in sharing state between remote widgets

setState is basically suitable for local state management. If you want to share state among multiple widgets or access state from different parts of the application, setState alone makes state propagation and access difficult.

Difficulty in separating business logic and UI -> Difficult to maintain

When setState is used, business logic and UI logic tend to be closely tied together. Separating state management outside of the widget improves code reusability and ease of testing.

Therefore, the concept of Provider was born to solve the above setState issues.

Introducing the provider package and implementing a counter application

Introducing the provider package

provider install | Flutter package
A wrapper around InheritedWidget to make them easier to use and more reusable.

Create a CounterModel class to hold the count state.

A class that holds the state and mixin ChangeNotifier. changeNotifier provides the ability to notify listeners that the state has changed.

notifyListeners() is a method defined in the ChangeNotifier mixin that, when notifyListeners() is called, points to the object (an instance created from the CountModel class, in this case instance of CountModel generated through the create function in ChangeNotifierProvider), all widgets listening to it will be rebuilt, so that changes in state will be reflected in the UI.

Dart
// lib/count_model.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';  //Providerをimport

class CountModel with ChangeNotifier {
	// Take _ (make private) and describe it so that it can be called from another file.
  int counter = 0;

  void incrementCounter() {
    counter++;
    notifyListeners();
  }
}

Enclose the model with a ChangeNotifierProvider and initialize the model.

The ChangeNotifierProvider is responsible for providing an instance of CountModel and notifying listeners (such as Consumer widgets) when there are changes to that instance.

Without the ChangeNotifierProvider, the Consumer widget would not be able to monitor the CountModel instance and update the UI based on changes when notifyListeners() is called.

Enclose the parts of the widget that you want to redraw in response to changes in the value of model in a Consumer Widget.

When the builder function is called within a Consumer widget, the function is passed three arguments: context, model, and child.

context: represents the current BuildContext, indicating the widget’s position in Flutter’s widget tree
model: points to the object being listened to by the Consumer, in this case an instance of CountModel. Through this object, state is maintained and changes are notified to the widget. model can be used to access properties and methods of CountModel and update the UI appropriately.
child: child provides a widget tree that is not rebuilt each time the Consumer widget is rebuilt. This is used for performance optimization; widgets that should not be rebuilt each time the builder is rerun, i.e., that do not depend on state changes, are provided through the child parameter. This is not used in this case.

Dart
class MyHomePage extends StatelessWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
	    // Instantiate CountModel class with create function
      create: (_) => CountModel(),
      child: Scaffold(
        appBar: AppBar(
          title: Text(title),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Text(
                'You have pushed the button this many times:',
              ),
              Consumer<CountModel>(
                builder: (context, model, child) {
                  return Text(
                    '${model.counter}',
                    style: Theme.of(context).textTheme.headline4,
                  );
                },
              ),
            ],
          ),
        ),
        floatingActionButton: Consumer<CountModel>(
          builder: (context, model, child) {
            return FloatingActionButton(
              onPressed: () => model.incrementCounter(),
              tooltip: 'Increment',
              child: Icon(Icons.add),
            );
          },
        ),
      ),
    );
  }
}

Flow of state management using the provider package

  1. Button press: User taps the FloatingActionButton.
  2. State update: The FloatingActionButton’s onPressed callback executes model.incrementCounter(). This calls the incrementCounter method in the CountModel class and increments the counter.
  3. Notification to listeners: When notifyListeners() is called within the incrementCounter method, all listeners (in this case, Consumer widgets and ChangeNotifierProvider) are notified.
  4. Rebuild widget: When the ChangeNotifierProvider detects a change in the provided CountModel instance, it rebuilds the Consumer widget. The Text widget is updated by referencing the latest value of the model.counter.

Rebuild widget: When the ChangeNotifierProvider detects a change in the provided CountModel instance, it rebuilds the Consumer widget. The Text widget is updated by referencing the latest value of the model.counter.
ChangeNotifierProvider provides the instance itself (CountModel()) and is not involved in the notification itself. However, without providing an instance, it is not possible to detect instance state changes (notifyListeners()) in the first place, so it is not possible to redraw the UI (redraw the Consumer widget).

For example, let us consider an example where a family shares a single TV set. The TV is the ChangeNotifierProvider and the TV program corresponds to the CountModel. Members of the family (child widgets) can watch the TV at any time by going to the living room (the appropriate location in the widget tree).

  1. Providing a TV: A TV is placed in the living room (ChangeNotifierProvider provides the CountModel).
  2. Program Selection: A family member uses the remote control (action) to change the channel (ChangeNotifierProvider changes the state of the CountModel).
  3. Program notification: the TV (CountModel) displays the new channel (change of state) and everyone in the living room can see it (notifyListeners() notifies listeners).

In this example, ChangeNotifierProvider ensures that the TV is in the living room and accessible to everyone in the family. Then, when the channel changes (CountModel is changed), the TV shows the new program (new state), which can then be viewed by those in the living room (Consumers and other listeners).

Advantages of Using Provider

Encapsulation of state:

Using Provider, state can be encapsulated in a specific class. This clearly separates the state logic from the UI logic, improving code readability and maintainability.

Improved reusability:

Classes that manage state can be reused in other widgets, making it easier to share state throughout the application.

Efficient UI updating:

When notifyListeners() is called, only the contents of Consumer are redrawn.

In other words, with Provider, the developer can explicitly limit the scope of redrawing by using Consumer, avoiding unnecessary widget rebuilding.

How to call the various Provider models

Provider.of: called when changed by notifiListeners().

Dart
final model = Provider.of<CountModel>(context);

context.watch: called when changed by notifiListeners().

Dart
final model = context.watch<CountModel>();

context.read: called only once, not called if changed by notifiListeners();

Dart
final model = context.read<CountModel>();

context.select: called only when a specific property is updated

Dart
final count = context.select<CountModel, int>(
      (CountModel model) => model.count,.
    );

コメント

Copied title and URL