状態管理 - 1m-llc/Flutter-KtoK GitHub Wiki

考察

Provider+ChangeNotifierパターンが良さそう。
概念がわかりやすく、ProviderからConsumer以下のウィジェットだけを再構築する仕組みがわかりやすい。
実装してみてBlocより簡易に状態管理を行えると感じた。

Blocパターンは、コード量や複雑さが課題としてあり、学習コストが高い。
BLoCパターンには(ロジックをUIから分離できるので)同一のコードを複数プラットフォーム間(AngularDartとFlutterなど)で使い回せる という考えがもともとあって、そのためならStream縛りのルールは良いけれど、Flutterでアプリ開発するだけならちょっとやり過ぎ感がある。

Provider+ChangeNotifier vs Bloc

Flutter推奨の新旧2つのパターン比較とその他のパターンの比較表を整理しました。
Provider+ChangeNotifierとBlocのカウンターアプリのソースコードを後述していますので比較してみてください。

Flutter推奨はProviderパッケージ

Flutterを初めて使用する場合で、別のアプローチ(Redux、Rx、フックなど)を選択する強い理由がない場合は、Providerパッケージがおそらく最初に使用するアプローチです。プロバイダーパッケージは理解しやすく、コードをあまり使用しません。また、他のすべてのアプローチに適用できる概念を使用しています。

Provider+ChangeNotifier
状態変数を複数Widget間で共有する際に使うパターン

以下、概念図です。
Providerパッケージの一部でもあるConsumerウィジェットは値の変更を検知して、変更が発生するとウィジェットをその下に再構築します。

main.dart

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

void main() {
  runApp(
    // アプリ全体をProviderでラップする
    // childにWidgetを設定
    ChangeNotifierProvider(
        create: (_) => CounterNotifier(),
        child: TopWidget(),
    ),
  );
}

// Providerに値の変更を通知するChangeNotifier継承クラス
class CounterNotifier with ChangeNotifier {
  int _count = 0;
  int get count => _count;

  void increment() {
    _count++;
    // このクラスを使っているWidgetに対して状態の変化を通知
    notifyListeners();
  }
}

class TopWidget extends StatelessWidget {
  const TopWidget({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final counterNotifier = Provider.of<CounterNotifier>(context, listen: false);

    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Example'),
        ),
        body: Center(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Text('Push + Button:'),
              // Consumerでラップされた数値表示部分のみのWidgetを更新する(それ以外は更新しない)
              Consumer<CounterNotifier>(
                  builder: (BuildContext context, CounterNotifier value, Widget _) {
                    return Text(
                        '${value.count}',
                        style: Theme.of(context).textTheme.headline4,
                    );
                  },
              ),

            ],
          ),
        ),
        floatingActionButton: FloatingActionButton(
          // +ボタンが押されたらCounterNotifierクラスのincrementメソッドを呼び出す
          onPressed: counterNotifier.increment,
          child: const Icon(Icons.add),
        ),
      ),
    );
  }
}

Blocパターン(Business Logic of Component)
状態を管理し、プロジェクトの中心的な場所からデータにアクセスするのに役立ちます。
Flutterで言うビジネスロジックは、View(Widget)とModelをつなぐ部分にあります。Viewからデータを受け取り、Modelとやりとりをしてステートの更新をし、Viewにステートの変更を通知する役割を持っています。

Google I/O 2018では以下のような画像で紹介されました。



pub.dev blocでは以下のような画像で説明されました。



import 'package:flutter/material.dart';
import 'package:bloc/bloc.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

class CounterObserver extends BlocObserver {
  @override
  void onChange(Cubit cubit, Change change) {
    print('${cubit.runtimeType} $change');
    super.onChange(cubit, change);
  }
}

// Streamを継承したCubitを継承する
class CounterCubit extends Cubit<int> {
  CounterCubit() : super(0);

  void increment() => emit(state + 1);
}

void main() {
  Bloc.observer = CounterObserver();
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: TopPage(),
    );
  }
}

class TopPage extends StatelessWidget {
  const TopPage({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => CounterCubit(),
      child: CounterWidget(),
    );
  }
}

class CounterWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final textTheme = Theme.of(context).textTheme;
    return Scaffold(
      appBar: AppBar(title: const Text('Counter')),
      body: Center(
        child: BlocBuilder<CounterCubit, int>(
          builder: (context, state) {
            return Text('$state', style: textTheme.headline2);
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        // ボタンを押したらCounterCubitクラスのincrementメソッドが呼ばれる
        onPressed: () => context.bloc<CounterCubit>().increment(),
        child: Icon(Icons.add),
      ),
    );
  }
}

その他

他のフレームワークに強いバックグラウンド処理がある場合は以下の比較表や「オプションページ」を参照してください。

比較表

名称 説明
setState 標準の機能でsetState()を呼び出すことで状態の更新を行います。 LINK
InheritedWidget & InheritedModel 標準で用意されているwidgetを駆使する手法です。
・子孫が親の変更を感知できる
・buildメソッドを再度呼ぶことで再構築し直すことができる
・先祖をO(1)で取得できる
LINK
Provider+Scoped Model 基本的にはModelで囲むってだけでStatefullWidgetとStateを意識する必要がなくなるのでシンプルに感じます。小さめのアプリだったらこれで行けそう気がします。 LINK
provider+ChangeNotifier Google I/O でも 2018 年は BLoC を推奨していましたが (Build reactive mobile apps with Flutter (Google I/O '18))、 2019 年では意見を変えて provider パッケージの使用を推奨しています。 LINK1
LINK2
Redux Javascriptのために考えられた設計。アプリ内の全状態のStore(json的なもの)があって、前の状態とアクションを組み合わせて新しい状態を生み出すという状態管理手法です。テストコードも書きやすい。 LINK
BLoC Business Logic Componentの略でビジネスロジックとUIを明確に分けることを目的としています。
BLoCの原則は以下の3つです。
・インプットとアウトプットは、単純なStreamとSinkに限定する。
・依存性は、必ず注入可能でプラットフォームに依存しないものとする。
・プラットフォームごとの条件分岐は、許可しない。
Blocに保持しているデータをStreamとSinkを使って伝搬させることによって非同期でwidgetを更新させることができます。

・Stream
listen()メソッドなどで、値が流れてきた時に自動で行う処理を設定できる。
・Sink
add()メソッドを使って、Streamに新しい値を流す。
・Provider
一つのBLoCを複数のWidgetで使えるようにする
・StreamBuilder
これでWidgetをラップすることで、BLoCのstreamを使ってピンポイントにUIを更新できる
LINK
MobX MobXはReduxと同様にJavascriptのために考えられた設計で、Observables・Actions・Reactionsの3つで成り立っています。アノテーションをつけることで3つの状態を関連付けています。javaっぽい書き方です LINK
Flutter Hooks 「React Hooks」のFlutter版みたいなイメージ。将来的にはFlutterの標準機能として取り込まれそうとも公式には否定的との意見もある。Hooks は、クラス(オブジェクト)のメンバ変数やモジュールのローカル変数を、あたかも関数のローカル変数のように使う手法のひとつ LINK
Riverpod Providerの作者による、Providerの進化版です。RiverpodとFlutter Hooksはセットで使うのが、作者の推しみたいです。

・良いところ
Flutterに依存しなくなった(Pure Dart)
コンパイル時にエラーを検知できる

・悪いところ
まだまだ開発段階。
これから破壊的な変更が入りそう
プロダクトに組み込むにはまだ早い感じ
LINK1
LINK2

Flutter Architecture Samples

Flutterアーキテクチャのサンプルがまとまっています。
https://fluttersamples.com/

⚠️ **GitHub.com Fallback** ⚠️