🚄

【Flutter】色んな状態管理で作ってみよう ⑥MobX

2022/02/11に公開

※こちらの記事は【Flutter】色んな状態管理手法でカウンターアプリを作ってみるの一部として作成された記事です

Reactからの刺客 MobX

今回はRedux同様、Flutterが参考にしたReactで発展した設計思想MobXを元にした状態管理手法MobXパッケージでカウンターアプリを作っていきます

使用するPackage:

概要

  • 状態管理クラスを自動生成して使うユニークな状態管理手法
  • MobX自体は元々Javascript(主にReact)で使う為に開発された設計思想
  • Pavan Podira氏が開発
  • 初版リリースは2019年1月

全体像

MobXとは?

前述の通り、Reactと共に使う為に開発された設計思想で以下3つの構成要素から成ります。

  1. Observables:監視される状態値
  2. Actions:状態値の変更を発火する指示
  3. Reactions:状態値の変更に反応して走る再描画や処理
    mobx

アノテーション

MobXでは状態管理クラスの事をMobX Storeと呼び、flutter_mobxパッケージではStore内の変数や処理に以下アノテーションを付記する事でそれぞれの役割を指定します。

  • @observable:指定された変数を監視対象にし変更を検知する事が出来る様になります。
  • @action:状態値の変更を行うメソッドに付記します。状態値の変更は全てこのActionメソッド内で行わなければなりません。
  • @computed:observable値自体に変更を加えないが、値を使って別の値を算出するような場合に使われます。主にobservable値を使ったgetterメソッドに対して付記します。

その後、build_runnermobx_codegenによってアノテーションの指定に沿って状態管理に使うクラスやメソッドを自動生成してくれます。

キーとなるクラスやメソッド

  • Observerクラス:ラップしたWidgetをstoreのobservable変数の変更に反応して再描画させるクラス

今回のサンプルでは使用していませんが、observableの変更に応じて再描画するだけでなく、以下メソッドで処理を走らせる事が出来ます。

  • reactionメソッド:observable変数が違う値になった場合の実行される処理を定義できます
reaction((_) => store.count, (count) => print(count));
  • autorunメソッド:メソッド内で使われてるobservable変数に変更があった場合、実行される処理を定義できます
autorun((_) => print(store.count));
  • whenメソッド:observable変数が指定した条件に合致する場合のみ実行される処理を定義できます
when((_) => store.count == 5, () => print('Count reach to 5'));

準備

カウンターアプリを例に実際描いてみましょう。
サンプルコードはこちら⬇︎

1. パッケージのインポート

コード生成は開発環境で行う為、build_runnermobx_codegenpubspec.yamldev_dependenciesに追記します。

dependencies:
  flutter_mobx: ^2.0.2

dev_dependencies:
  build_runner: ^2.1.2
  mobx_codegen: ^2.0.2

2. Stateクラス

  • 今回はCountフィールドを持つCounterObjクラスを定義。
class CounterObj {
  CounterObj(this.count);
  int count;
}

3. 状態管理クラス(MobX Store)

  • 前述の通り、flutterのMobXでは状態管理用のコードを自動生成する為、MobXの書き方に従って、状態管理クラスの素を作ります。
  • 以下の3つを記述していきます
    1. 生成されるコードのファイル名
    2. 自動生成されたクラスをミックスインする状態管理クラス
    3. Storeクラスをミックスインした抽象クラス
// ①生成されるコードのファイル名
part 'counter_store.g.dart'; 

// ②状態管理クラス
class CounterStore = CounterStoreBase with _$CounterStore; 

// ③Storeをミックスインした抽象クラス
abstract class CounterStoreBase with Store { 
  
  CounterObj countObj = CounterObj(0);

  
  void increment() {
    countObj = CounterObj(countObj.count + 1);
  }

  
  void decrement() {
    countObj = CounterObj(countObj.count - 1);
  }

  
  void reset() {
    countObj = CounterObj(0);
  }
}

observablesactionsは③の抽象クラスに記述していきます。

上記クラスを定義した状態だとエラーが出ますが、無視して大丈夫です。
記述後、以下コマンドをコマンドラインから実行します。

$ flutter pub run build_runner build

するとコードが自動生成され、エラーも解消します。

4.UIへ注入

  • flutter_mobxパッケージでは依存関係の注入の為のクラスは用意されていないので、providerパッケージを使用します。
  • createフィールドで状態管理クラスCounterStoreをインスタンス化
  • childフィールドに定義した_MobxCounterPage widgetにインスタンスを注入
  
  Widget build(BuildContext context) {
    return Provider(
      create: (_) => CounterStore(),
      child: _MobxCounterPage(),
    );
  }

さあ、これで_MobxCounterPage widgetより下に位置する全てのWidgetでCounterStoreクラスにアクセスできる様になりました。

状態へのアクセス

MobX Storeへのアクセス

状態管理クラスへのアクセス自体はProviderで依存関係を注入しているので、Providerを通してアクセスします。

  final CounterStore store = Provider.of(context);

observableの変更に伴った再描画

  • observableとなった状態変数の変更に応じてWidgetを再描画させる場合はObserverクラスを使います。
   Observer(
   builder: (context) => Text(
     '${store.countObj.count}',
     style: Theme.of(context).textTheme.headline4,
   ),
 ),

actionsへのアクセス

こちらもProvider経由でアクセスする事が出来ます。

  FloatingActionButton(
    onPressed: () => store.increment(),
    tooltip: 'Increment',
    heroTag: 'Increment',
    child: Icon(Icons.add),
  ),

全体

状態管理クラス

import 'package:mobx/mobx.dart';

part 'counter_store.g.dart';

// 状態クラス
class CounterObj {
  CounterObj(this.count);
  int count;
}

// 状態管理クラス(MobX store)
class CounterStore = CounterStoreBase with _$CounterStore;

abstract class CounterStoreBase with Store {
  
  CounterObj countObj = CounterObj(0);

  
  void increment() {
    countObj = CounterObj(countObj.count + 1);
  }

  
  void decrement() {
    countObj = CounterObj(countObj.count - 1);
  }

  
  void reset() {
    countObj = CounterObj(0);
  }
}

カウンター本体

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:state_management_examples/state_managements/mobx/counter_store.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:state_management_examples/widgets/main_appbar.dart';

// 依存関係を注入
class MobxCounterPage extends StatelessWidget {
  const MobxCounterPage({Key key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return Provider(
      create: (_) => CounterStore(),
      child: _MobxCounterPage(),
    );
  }
}

// カウンター本体
class _MobxCounterPage extends StatelessWidget {
  const _MobxCounterPage({Key key}) : super(key: key);

  
  Widget build(BuildContext context) {
    final CounterStore store = Provider.of(context);

    print('rebuild!');

    return Scaffold(
      appBar: MainAppBar(
        title: 'MobX x Provider',
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Observer(
              builder: (context) => Text(
                '${store.countObj.count}',
                style: Theme.of(context).textTheme.headline4,
              ),
            ),
          ],
        ),
      ),
      floatingActionButton: FittedBox(
        child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: <Widget>[
            FloatingActionButton(
              onPressed: () => store.increment(),
              tooltip: 'Increment',
              heroTag: 'Increment',
              child: Icon(Icons.add),
            ),
            const SizedBox(width: 16),
            FloatingActionButton(
              onPressed: () => store.decrement(),
              tooltip: 'Decrement',
              heroTag: 'Decrement',
              child: Icon(Icons.remove),
            ),
            const SizedBox(width: 16),
            FloatingActionButton.extended(
              onPressed: () => store.reset(),
              tooltip: 'Reset',
              heroTag: 'Reset',
              label: Text('RESET'),
            ),
          ],
        ),
      ),
    );
  }
}

以上でした

今回は例がシンプル過ぎて使いませんでしたがreactionautorunwhenを使う様な状況が発生した場合、本領を発揮する状態管理手法だと思います。

参考

Discussion