未熟学生エンジニアのブログ

ヨシオ個人開発ブログ

FlutterやWeb周り全般、チーム開発について語るブログ

Flutterアプリの主流な状態管理パターンと導入事例まとめ(2020年版)

はじめに

Flutterアプリを作る際、状態管理の方法/設計/アーキテクチャは必ず気になるポイントだと思います。この記事では2020年6月現在で主流とされているパターンと実際の導入事例を説明します。

この記事では、詳しい話は省略して概要のみを説明します。代わりにオススメの解説記事のリンクを載せてありますので、詳しい話はそちらから学ぶことができるようになっております。

状態管理パターンとは?

状態管理パターンとはなんなのか、という話ですが、単純にいうと以下の2つのことをうまくやるためのパターンです。

  • アプリの状態を持つ変数を1つまたは2つ以上の箇所(クラス)で使えるようにする
  • 変数の変更に応じて、UIを更新(リビルド)する

状態管理とは、基本的にはこの2つのことだと思えば良いと思います。この2つをうまくやるスキルを身につけることで、どんなアプリに対しても開発効率を大きく改善できると思っています。

Flutterの主流な状態管理パターン

この記事で紹介する状態管理パターンは以下です。

  • StatefulWidget
  • InheritedWidget / InheritedModel
  • ChangeNotifier/ValueNotifier + Provider
  • BLoC + Provider
  • state_notifier + freezed + Provider
  • ? + Riverpod
  • Redux
  • mobX

どれを選ぶべき?

結論: ChangeNotifier + Provider が一番無難

どの状態管理手法を選ぶべきか、迷いますよね・・。そこで、最近指針となるようなツイートを発見したのでご紹介します。

繰り返しますが、こちらで説明されている通り、 ChangeNotifier と Providerの組み合わせが一番無難なパターンと言えそうです。 ただし、今回この記事で紹介するパターンは、組み合わせが可能なものもありますし、混在が悪というわけではありませんので、ご注意ください。

ChangeNotifer + Providerに不安な点があるとすれば、package:providerが2019年5月のGoogle I/O以降にという比較的新しいパッケージであるためか、企業がリリースしているアプリでChangeNotifierをメインで使っている例をネット上で見つけることはできませんでした(パターン3にて後述)。しかし、これも後にご紹介しますが、時期的な問題で採用したBLoCをChangeNotifierに置き換えようという企業も実際にあるようですので、それほど大きな不安を抱く必要はないと思います。

2020/07/17 追記: 同じ方による、新しいライブラリRiverpod(まだ安定版ではないので注意)も含めた以下の投稿がありました。

2020/07/17 追記2: Flutter公式からもpackage:providerが推奨となりました。

https://flutter.dev/docs/development/data-and-backend/state-mgmt/options#provider

では、前置きはこのあたりにして、各パターンについて説明していきます。

パターン1. StatefulWidget

StatefulWidgetは、基本的にはある状態変数fooを1つのWidgetだけが変更し、その変更を子Widgetに伝えたい場合に使います。

例として、Adding interactivity to your Flutter app - Flutter よりコードと画像を引用します。

f:id:swiftfe:20200624113849p:plain

class TapboxA extends StatefulWidget {
  TapboxA({Key key}) : super(key: key);

  @override
  _TapboxAState createState() => _TapboxAState();
}

class _TapboxAState extends State<TapboxA> {
  bool _active = false;

  void _handleTap() {
    setState(() {
      _active = !_active;
    });
  }

  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: _handleTap,
      child: Container(
        child: Center(
          child: Text(
            _active ? 'Active' : 'Inactive',
            style: TextStyle(fontSize: 32.0, color: Colors.white),
          ),
        ),
        width: 200.0,
        height: 200.0,
        decoration: BoxDecoration(
          color: _active ? Colors.lightGreen[700] : Colors.grey[600],
        ),
      ),
    );
  }
}

この例の場合、状態変数 _active を子WidgetであるTextBoxDecorationで使い、_activeが変化するタイミングでsetState()を実行し、リビルドして状態を反映させます。

逆に、ある状態変数を2つ以上のWIdgetが変更したり、子Widgetで状態変数が変更されて親Widgetでその状態変数を利用するような場合は、package:provider + ChangeNotifier、あるいはpackage:provider + BLoCなどを使った方が管理しやすいです。

パターン2. InheritedWidget / InheritedModel

InheritedWidgetはわかりにくくコード量も無駄に多くなるということで、package:providerが登場しました。現状、package:providerでできるような用途の場合、package:providerを使いましょう。 その方がコードは劇的に短くかけてわかりやすくなります。そのため、 どうしてもpackage:providerでできないようなことをしたいときのみInheritedWidgetを使うべき だと思います。

そのため、ここでの説明も最低限にします。

InheritedWidgetは、状態変数をWidget間で共有する際に使うWidgetです。InheritedWidgetは状態変数と子Widgetを持ちます。

https://aimana-it.com/wp-content/uploads/2019/06/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88-2019-06-09-12.46.00.pnghttps://aimana-it.com/wp-content/uploads/2019/06/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88-2019-06-09-12.46.00.png:image=https://aimana-it.com/wp-content/uploads/2019/06/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88-2019-06-09-12.46.00.pngより引用)

InheritedWidgetを使うと、以下のことができます。

  • 数の子Widgetから状態変数を取得、変更できる
  • その変更を検知して子Widget部分的に(StatefulWidgetではできなかった) リビルドして変更を反映させることができる
  • setState()のバケツリレーをせずに親Widgetをリビルドできる

もっと詳しく

以下の記事が詳しいです。 qiita.com

medium.com

パターン3. ChangeNotifier/ValueNotifier + Provider

これもInheritedWidgetを使ったパターンと同じく、状態変数を複数Widget間で共有する際に使うパターン です。また、冒頭で述べた通り、Flutter開発をする大多数の方にお勧めできるパターンです。

pub.dev

providerパッケージに含まれるProvider.of()やConsumerなどを使うことで、Provider系モジュールにより注入した値の変更を検知し、Widgetをリビルドすることができます。このリビルドの際には状態変数に関わる部分だけを対象にすることができ、パフォーマンス面でも最適になりやすいです。

現在はFlutter公式ガイドでもリストの一番上で紹介されており、シンプルな場合にはこれを使うのが良いとされているのだと思います。とりあえず迷ったらこれ、と考えて良いと思います。

ChangeNotifierの作成

class Counter with ChangeNotifier {
  int _count = 0;
  int get count => _count;

  void increment() {
    _count++;
    notifyListeners();
  }
}

Provider(ChangeNotifierProvider)のchildにWidgetを設定

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

void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => Counter()),
      ],
      child: MyHomePage(),
    ),
  );
}

Widget

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Example'),
        ),
        body: Center(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              const Text('You have pushed the button this many times:'),
              const Count(),
            ],
          ),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () => context.read<Counter>().increment(),
          tooltip: 'Increment',
          child: const Icon(Icons.add),
        ),
      ),
    );
  }
}

Widget

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

  @override
  Widget build(BuildContext context) {
    return Text(
        '${context.watch<Counter>().count}',
        style: Theme.of(context).textTheme.headline4);
  }
}

導入事例

日本企業での導入事例はネット上では見つけられませんでしたが、既にリリース済みのアプリを後述のBLoCパターンからprovider+ChangeNotifierの実装に置き換えようとしているという記事がありました。

https://qiita.com/hukusuke1007/items/318875bb11c23572d46f

また、個人開発アプリでは導入事例がありました。この記事では局所的にRx(≒Streamの拡張ライブラリ)を使っているとも書かれています。 https://qiita.com/takashi-i/items/d364b4547db14d5c31de

以下の動画の12:10付近から、2019年5月当時のFlutterにおける状態管理の歴史について少し説明があります。

ここでは、それまで主流だったScopedModelとBLoCについて触れられており(BLoCは現在も主流の一つですが)、新しい方法として、providerパッケージが紹介されています。

youtu.be

もっと詳しく

providerは種類が多いのですが、この記事が網羅的でかつ情報も随時更新されていて新しいです。 qiita.com

Flutter公式サンプルとしてもproviderが採用されています。 flutter.dev

パターン4. BLoC + Provider

BLoCパターンは、簡単に言うとStream/Observableを入出力役として状態管理やロジックの実行を行うパターンです。ChangeNotifierの場合は普通にメンバを直接取得/変更していましたが、それをStream経由に限定するのが大きな特徴 です。

参考図(リンク元より引用) BLoCの参考図

BLoCのメリット、デメリット

Streamを使った状態管理がしたい場合、現在でもBLoCパターンを使うのが一般的でしょう。StreamBuilderとの組み合わせにより、局所的なリビルドが簡単に行えます。インクリメンタルサーチのようなRxDartを使った方が書きやすい場合や、Firestoreのsnapshots系のメソッドなど、Stream系の値を返すライブラリを使う場合に相性が良いです。

一方で、BLoCの実装に必要なStream/RxDartの学習コストが高いことや、ChangeNotiferなどに比べてボイラープレートコードが多くなることがデメリットと言われています。

コード例

Stream/RxDart初心者のためのBLoC入門(Flutter) - Qiita より引用

counter_bloc.dart

import 'dart:async';

class CounterBloc {
  final _actionController = StreamController<void>();
  Sink<void> get increment => _actionController.sink;

  final _countController = StreamController<int>();
  Stream<int> get count => _countController.stream;

  int _count = 0;

  CounterBloc() {
    _actionController.stream.listen((_) {
      _count++;
      _countController.sink.add(_count);
    });
  }

  void dispose() {
    _actionController.close();
    _countController.close();
  }
}

main.dart

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

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: Provider<CounterBloc>(
          builder: (context) => CounterBloc(),
          dispose: (context, bloc) => bloc.dispose(),
          child: MyHomePage(title: 'Flutter Demo Home Page'),
        ));
  }
}



class MyHomePage extends StatelessWidget {
  MyHomePage({this.title});

  final String title;

  @override
  Widget build(BuildContext context) {
    final counterBloc = Provider.of<CounterBloc>(context);
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            StreamBuilder(
              initialData: 0,
              stream: counterBloc.count,
              builder: (context, snapshot) {
                return Text(
                  '${snapshot.data}',
                  style: Theme.of(context).textTheme.display1,
                );
              },
            )
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          counterBloc.increment.add(null);
        },
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

導入事例

https://qiita.com/navitime_tech/items/21a6111b36b98cac3100

https://note.com/yamarkz/n/nd9716541d8ad

https://speakerdeck.com/yamarkz/division-plan-and-practical-example-in-bloc-pattern

flutter_blocBLoC

package:flutter_blocは、event、stateなどの制約も同梱しており、blocの本来の制約よりもきつい制約が設定されたライブラリです。

pub.dev

flutter_blocBLoCパターンの実装に必須なパッケージではありません。 それどころか、flutter_blocの制約が強いことを嫌うエンジニアをネット上で見かけることもあります。

しかし一方でより強い型付け/制約付けによりチーム開発でのコードの可読性や統一性を高める効果があるのだと思います。

BLoCの課題

Provider のススメ | Unselfish Meme

(一時、「まだBLoCで消耗してるの?」というタイトルでした。いつの間にか変わっていたのですね。[B! flutter] まだ BLoC で消耗してるの? | Unselfish Meme

BLoCはスタンダードなパターンとして考えられていましたが、BLoCに含まれるStream/Rxという概念に馴染みがないエンジニアにとっては敷居が少し高く、シンプルな状態管理の場合にはBLoCパターンはオーバーなのではないかという意見も出始め、ちょうどそのころ package:provider が登場したことで、次で説明する「Provider + ChangeNotifier/ValueNotifier」パターンに流れる人も多く現れました。

(2019年のGoogle I/Oでも同様の内容が語られています)

Stream=BLoCではない

状態変数の管理にStreamを使うからといって、BLoCパターンを無理に使う必要はありません。Streamを使った状態管理パターンの一つとしてBLoCパターンがあるというだけです。

おまけ:BLoCの歴史

BLoCは、Google I/O 2018 で紹介されました。この際には、ScopedModelのminimal rebuildができないという欠点を補うという点でScopedModelに優っているとされていました。また、 このタイミングではInheritedWidgetを使った自前実装のCartProviderというWidgetを使って複数WidgetでのBLoCの共有を実現していました (動画では 23:40 付近で言及)。

youtu.be 23:11 付近

cubit

flutter_bloc | Flutter Package の軽量版パッケージとして、cubitというパッケージも公開され、海外のエンジニアに好評のようです。ただし、おそらくflutter_blocを使っていた人が軽量板を求めて移行している場合が多いと考えられますが、そうでない方でも使いやすいものになっているようです。

pub.dev

注:現在は packages:flutter_bloc に含まれるようになりました。

実際のコードは以下のようになっており、とてもシンプルです。

https://github.com/felangel/cubit より引用。

class CounterCubit extends Cubit<int> {
  CounterCubit() : super(0);

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

利用側はこのようになります。以下のクラスやメソッドを用いてCubitを使います。

  • CubitProvider
  • context.cubit()
  • CubitBuilder<CounterCubit, int>

https://pub.dev/documentation/flutter_cubit/latest/ より引用

class CubitCounter extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: CubitProvider(
        create: (_) => CounterCubit(),
        child: CounterPage(),
      ),
    );
  }
}

class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Cubit Counter')),
      body: CubitBuilder<CounterCubit, int>(
        builder: (_, count) {
          return Center(
            child: Text('$count'),
          );
        },
      ),
      floatingActionButton: Column(
        crossAxisAlignment: CrossAxisAlignment.end,
        mainAxisAlignment: MainAxisAlignment.end,
        children: <Widget>[
          Padding(
            padding: const EdgeInsets.symmetric(vertical: 4.0),
            child: FloatingActionButton(
              child: const Icon(Icons.add),
              onPressed: context.cubit<CounterCubit>().increment,
            ),
          ),
・・・

CubitはStreamを継承しており、Streamのように使えつつ、emit()で簡単に状態更新ができるようになっています。

Cubit class - bloc library - Dart API

cubitは、FlutterのValueNotifierや後に紹介するpackage:state_notifierのStream版ともいえるかと思いますので、ValueNotifierやStateNotifier、ChangeNotifierを使っていて同じようにStreamを使いたいという場合は、選択肢に入ってくるかと思います。

(7/29日追記)一方で、package:riverpodの作者のremiさんも以下のような発言をしており、StateNotifierでもstreamを扱いやすくなるようです。こちらも注目ですね。

もっと詳しく

より詳しく知りたい方は、こちらの記事がオススメです。

note.com

パターン5. state_notifier + freezed + Provider

state_notifierは、ValueNotifier/ChangeNotifierに相当するパッケージです。ほとんどValueNotifierと同じです。

pub.dev

state_notifierを使うと、状態変数をimmutableに扱うことが強制されます。 StateNotiferは、メンバ T stateを持ち、このstateを変更する際には、新しい値で置き換えることになります(state = state.copyWith(count: count+1))。 対して、ChangeNotifierの場合では、すでにある変数の一部(例えば、state.count +=1のような感じ)を変更するという形を取っています。

class Counter extends StateNotifier<int> {
  Counter(): super(0)

  void increment() {
    state++;
  }
}

state_notifierを使うと、現在の状態と新しい状態が変更後も両方存在することになる ため、履歴の管理やがしやすいです。そのため、「元に戻す」といった操作の実装が簡単になります。

一方、ChangeNotifierを使う場合、状態変数がクラスオブジェクトだった場合などに、参照をコピーして保存した履歴は最終的に全て同じものになってしまいます。state_notifierの場合、このようなことは起こりません。

また、 state_notifierを使うと、以下の例のようにStateとNotifierを自然と別々に取得できるようになるので、ChangeNotifierよりも明確に取得役(State)と更新役(Notifier)が分かれているのもポイントです。

      Text(
              context.select((MyState value) => value.count).toString(), // State
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: context.watch<MyStateNotifier>().increment, // Notifier
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),

state_notifierでは、状態変数を一つの値として持ちます。ChangeNotifierでは状態変数を複数もつことができますが、state_notifierではそれはできません。その不便さを解消するため、freezedライブラリをセットで使うことが多いです。

また、LocatorMixinによるDI(以下のread()メソッド)が可能となっています。これは、state_notfierクラス内でproviderから上位widgetで設定されたインスタンスを取得することができるというものです。

class Counter extends StateNotifier<int> with LocatorMixin {
  Counter(): super(0)

  void increment() {
    state++;
    read<LocalStorage>().writeInt('count', state);
  }
}

また、notifieyListeners()を呼ぶ必要がなくなることや、ValueNotifierよりもパフォーマンス上の改善が行われています。(これはValueNotifierも同じですが)

flutter_state_notifier | Flutter Package より引用

import 'package:example/my_state_notifier.dart';
import 'package:flutter/material.dart';
import 'package:flutter_state_notifier/flutter_state_notifier.dart';
import 'package:provider/provider.dart';

// This is the default counter example reimplemented using StateNotifier + provider
// It will print in the console the counter whenever it changes
// The state change is also animated.

// The "print to console" feature is abstracted through a "Logger" class like
// we would do in production.

// This showcase how our custom MyStateNotifier does not depend on Flutter,
// but is still able to read providers and be used as usual in a Flutter app.

void main() {
  runApp(
    MultiProvider(
      providers: [
        Provider<Logger>(create: (_) => ConsoleLogger()),
        StateNotifierProvider<MyStateNotifier, MyState>(
          create: (_) => MyStateNotifier(),
          // Override MyState to make it animated
          builder: (context, child) {
            return TweenAnimationBuilder<MyState>(
              duration: const Duration(milliseconds: 500),
              tween: MyStateTween(end: context.watch<MyState>()),
              builder: (context, state, _) {
                return Provider.value(value: state, child: child);
              },
            );
          },
        ),
      ],
      child: MyApp(),
    ),
  );
}

/// A [MyStateTween].
///
/// This will apply an [IntTween] on [MyState.count].
class MyStateTween extends Tween<MyState> {
  MyStateTween({MyState begin, MyState end}) : super(begin: begin, end: end);

  @override
  MyState lerp(double t) {
    final countTween = IntTween(begin: begin.count, end: end.count);
    // Tween the count
    return MyState(
      countTween.lerp(t),
    );
  }
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Flutter Demo',
      home: MyHomePage(),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Counter example'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              context.select((MyState value) => value.count).toString(),
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: context.watch<MyStateNotifier>().increment,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

もっと詳しく

itome.team

新パターン. Riverpod + ?

また、package:providerの作者のRemiさんが、Riverpodという実験的プロジェクト(ライブラリ)を進行中(2020/6/24現在)のようです。

まだstable(安定版)ではないため、破壊的変更に注意してください。stableリリースまでは、package:providerを使う方が無難です。

github.com

package:providerとの比較

基本的にはpackage:providerと同じようなライブラリで、package:providerで使えていたProviderやChangeNotifierProvider、Consumerなどは今まで通り使えます。

それに加えたメリットとしては、以下が挙げられます。

  • package:riverpodはFlutter非依存
  • 実行時ではなくコンパイル時にエラーを出してくれる
  • Computed、Family、AutoDisposeProviderなどの新機能
  • hooks的な使い方も可能

まだ試行段階ということなので一般的に使われるのは先になりそうですが、個人的には従来のProvider + オプションでhooksにも対応している(package:hooks_riverpod)ことから、将来的にはproviderよりもRiverpodの方がより多くのユーザを取り込めそうかなと思っています。

ただし、Flutter公式はhooksを肯定的には捉えておらず、まずはpackage:flutter_riverpodの方を使う方が無難かと思います。

もっと詳しく

以下の公式ドキュメントがわかりやすいので、気になった方はぜひ読んでみてください。

riverpod.dev

アーキテクチャパターン

ここまでで挙げたパターンは、基本的にViewModelを活用したMVVMパターンの一部として使われることが多いかと思います。具体的には、Value/ChangeNotifier、BLoC、StateNotifier(とState)がViewModelとして用いられます(ViewはWidget)。

以下ではMVVM以外のアーキテクチャパターンを説明します。

Redux

Reduxは、「Single source of truth」、「State is read only」、「Changes are made with pure function」の3つの原則によってよりわかりやすく安全に状態変数を扱うためのアーキテクチャ です。以下の図のような要素・フローによってこれらの原則が実現されます(正確には、原則を守りやすくなるということだと思います)。また、Fluxに見られる「単一方向の状態変更フロー」も満たすことができます。

引用: Redux. From twitter hype to production

Reduxの利点(ChangeNotifierやBLoCと比較)

  • 厳し目のルールのため、意図しない状態変数の変更が起こりにくい
  • 書き方が統一されやすく、複数人開発でもコードがわかりやすい
  • 「Single source of truth」により、状態変数がいろんな箇所に散らばるということがない

Reduxの欠点(ChangeNotifierやBLoCと比較)

  • 学習コストが高い
  • boiler plate(定型文)の量が多くなってしまう

導入事例

https://note.com/kitoko552/n/nd25e46daf2d4

もっと詳しく

Reduxのより詳しい解説は以下の記事をお読みください。

itome.team

FlutterにおけるReduxは、以下のpackage:flutter_reduxを使うのが定番のようです。

pub.dev

List of state management approaches - Flutter

サンプル

以下にflutter_reduxに載っているサンプルコードを引用します。

import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'package:redux/redux.dart';

// One simple action: Increment
enum Actions { Increment }

// The reducer, which takes the previous count and increments it in response
// to an Increment action.
int counterReducer(int state, dynamic action) {
  if (action == Actions.Increment) {
    return state + 1;
  }

  return state;
}

void main() {
  // Create your store as a final variable in a base Widget. This works better
  // with Hot Reload than creating it directly in the `build` function.
  final store = new Store<int>(counterReducer, initialState: 0);

  runApp(new FlutterReduxApp(
    title: 'Flutter Redux Demo',
    store: store,
  ));
}

class FlutterReduxApp extends StatelessWidget {
  final Store<int> store;
  final String title;

  FlutterReduxApp({Key key, this.store, this.title}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // The StoreProvider should wrap your MaterialApp or WidgetsApp. This will
    // ensure all routes have access to the store.
    return new StoreProvider<int>(
      // Pass the store to the StoreProvider. Any ancestor `StoreConnector`
      // Widgets will find and use this value as the `Store`.
      store: store,
      child: new MaterialApp(
        theme: new ThemeData.dark(),
        title: title,
        home: new Scaffold(
          appBar: new AppBar(
            title: new Text(title),
          ),
          body: new Center(
            child: new Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                new Text(
                  'You have pushed the button this many times:',
                ),
                // Connect the Store to a Text Widget that renders the current
                // count.
                //
                // We'll wrap the Text Widget in a `StoreConnector` Widget. The
                // `StoreConnector` will find the `Store` from the nearest
                // `StoreProvider` ancestor, convert it into a String of the
                // latest count, and pass that String  to the `builder` function
                // as the `count`.
                //
                // Every time the button is tapped, an action is dispatched and
                // run through the reducer. After the reducer updates the state,
                // the Widget will be automatically rebuilt with the latest
                // count. No need to manually manage subscriptions or Streams!
                new StoreConnector<int, String>(
                  converter: (store) => store.state.toString(),
                  builder: (context, count) {
                    return new Text(
                      count,
                      style: Theme.of(context).textTheme.display1,
                    );
                  },
                )
              ],
            ),
          ),
          // Connect the Store to a FloatingActionButton. In this case, we'll
          // use the Store to build a callback that with dispatch an Increment
          // Action.
          //
          // Then, we'll pass this callback to the button's `onPressed` handler.
          floatingActionButton: new StoreConnector<int, VoidCallback>(
            converter: (store) {
              // Return a `VoidCallback`, which is a fancy name for a function
              // with no parameters. It only dispatches an Increment action.
              return () => store.dispatch(Actions.Increment);
            },
            builder: (context, callback) {
              return new FloatingActionButton(
                // Attach the `callback` to the `onPressed` attribute
                onPressed: callback,
                tooltip: 'Increment',
                child: new Icon(Icons.add),
              );
            },
          ),
        ),
      ),
    );
  }
}

ちなみに、パッケージの最終更新日が2019/12 となっており、versionも0.6.0となっていることからあまり開発が活発ではないかのようにも思いましたが、本家のjs版Reduxも最終更新は2019/12だったことから、特にそういうわけではないようです。

Releases · reduxjs/redux · GitHub

mobX

Flutter公式の選択肢として、mobXというパターンも紹介されていますが、個人的に主流には感じられないため、今回は割愛します。以下の記事でわかりやすい説明がされておりますので、興味がある方は読んでみてください。Reduxに近いアーキテクチャのようです。

itome.team

過去に使われたパターン

ScopedModel

ScopedModelは、InheritedWIdget + ChangeNotifierのようなパッケージです。

過去によく使われていたパッケージですが、package:providerの登場後はおそらくほぼ使われなくなりました。

package:provider は ScopedModelでしたかったことである「InheritedWidgetを使いやすくすること」を達成しつつ、より汎用性が高い(ScopedModelはInheritedWIdget + ChangeNotifierのような使い方しかできない)ことから、ScopedModelを使う理由がなくなったのだと思われます。

また、過去にはprovideパッケージというものも存在していましたが、providerパッケージの方が優れているということでproviderパッケージが本流となったようです。 https://ntaoo.hatenablog.com/entry/2019/05/12/141018

https://youtu.be/d_m5csmrf7I 19:34-

導入事例

developer.diverse-inc.com

実装比較用

こちらのGitHubリポジトリにいろんなパターンのサンプルプロジェクトがあるので、同じような構成のアプリでパターンを比較したいという方におすすめです。

github.com