😎

【Flutter】状態管理①:StatefulWidget + setStateからproviderへ。カウンターアプリを例に

2024/04/10に公開

Flutterにおける状態管理の進化

Flutterでは、**StatefulWidget**を使用して内部状態を持つことができる。また、ProviderやRiverpod、Blocなどのライブラリを使用してアプリケーション全体の状態管理を行う。

本記事では、statefulWidget + setStateから、次のステップであるProviderまでをみていく。別記事でRiverpodについて触れる予定。

StatefulWidget + setState

StatefulWidget を使用すると、StatelessWidgetと異なり、ウィジェットがアプリケーションの実行中に変更することができる内部状態を持つことができる。そして、setState メソッドは、この内部状態が変更された際にウィジェットを再構築(再描画)するために使用される。

カウンターアプリなど、ユーザーのアクションに応じて状態が変化する画面を作成したい場合にFlutterを始めた初心者の段階で、まず勉強する組み合わせである。

例)カウンターアプリ

ユーザーがボタンを押すと、カウンターが増加し、画面上のカウントが更新される。

import 'package:flutter/material.dart';

class Counter extends StatefulWidget {
  // このクラスは状態の設定です。
	// 親によって提供され、Stateのbuildメソッドで使われる値を保持する。
	// Widgetサブクラスのフィールドは常に"final"としてマークされる。

  const Counter({super.key});

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

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

  void _increment() {
  // setStateの呼び出し: setStateが呼び出されると、
  // Flutterは該当のStateオブジェクトが保持する
  // buildメソッドを再実行するようスケジュールする。
    setState(() {
      _counter++;
    });
  }

  
  Widget build(BuildContext context) {
    // このメソッドはsetStateが呼ばれるたびに再実行される。
    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(),
        ),
      ),
    ),
  );
}

**_incrementが呼ばれると、_counterの値が増加し、setStateによってbuildメソッドが再実行され、Text**ウィジェットに表示されるカウントの値が更新される。

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

Flutterの**buildメソッドは、setStateが呼ばれるたびに再実行され、その結果としてWidget全体が再構築される。build**メソッド内で新しいウィジェットツリーが生成されるが、実際には、Flutterフレームワークは差分(diffing)アルゴリズムを使用して、前回のビルドと比較し、実際に必要な変更のみをUIに適用する。

この例では、**_counter変数が更新されると、この変数を含むTextウィジェットのみが再描画される。しかし、ElevatedButton**やその他のウィジェットは再構築の必要がないため、変更されない。

このプロセスは、Reactの仮想DOMと類似しており、不要なUIの再描画を最小限に抑えるように設計されている。

setStateの2つの課題

離れたWidget間で状態を共有するのが困難

**setStateは基本的にローカル状態の管理に適している。複数のウィジェット間で状態を共有したい場合や、アプリケーションの異なる部分から状態にアクセスしたい場合、setState**だけでは状態の伝播やアクセスが難しくなる。

ビジネスロジックとUIの分離が困難→メンテナンスしにくい

**setState**を使うと、ビジネスロジックとUIロジックが密接に結びついてしまいがち。状態管理をWidgetの外部に分離することで、コードの再利用性やテストの容易さが向上する。

というわけで、上記setStateの課題を解決すべく、Providerという概念が誕生した。

Providerを導入して、カウンターアプリを実装

providerパッケージを導入

https://pub.dev/packages/provider/install

カウント状態を保持するCounterModelクラスを作成

状態を保持するクラスで、**ChangeNotifierをMixinしている。ChangeNotifier**は、状態が変更されたことをリスナーに通知する機能を提供する。

notifyListeners()は、ChangeNotifier mixinに定義されているメソッド。**notifyListeners()が呼び出されると、そのオブジェクト(CountModelクラスから生成されるインスタンスを指しており、この場合はChangeNotifierProvidercreate関数を通じて生成されるCountModel**のインスタンス)をリッスンしているすべてのウィジェットが再構築され、これにより、状態の変更がUIに反映されるようになる。

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

class CountModel with ChangeNotifier {
	// 別ファイルから呼び出せるように_(プライベート化)を取って記載
  int counter = 0;

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

ChangeNotifierProviderで囲んでmodelをinitialize

**ChangeNotifierProviderCountModel**のインスタンスを提供し、そのインスタンスに変更があった場合にリ、、、

続きはこちらで記載しています。
https://kazulog.fun/dev/flutter-statefulwidget-setstate-provider/

Discussion