すらぼうの開発ノート

モバイルアプリエンジニアのメモ

【Flutter】BLoCパターンの実装

BLoCパターンとは

BLoCパターンはアプリケーション開発に用いられるアーキテクチャパターンの一つ。

ざっくり以下のような特徴がある。

次の記事に非常にわかりやすく説明されている。詳細が知りたい方はこちらを参照してほしい。

www.flutteris.com

この記事では私が実装する際の備忘録として記載する。


実装方法

まずポイントから列挙する。以下の手順で実装するとBLoCパターンでの実装が可能。

  • 状態管理用BLoCクラスの作成
  • イベントクラスの作成
  • Observerクラスの作成
  • Observerクラスの配置
  • BlocProviderを配置
  • BlocBuilderを配置
  • イベントのadd

今回の説明ではflutter createで最初に生成されるCounterアプリに付け足すことを想定する。

状態管理用BLoCクラスの作成

class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0) {
    on<CounterIncrementPressed>((event, emit) {
      emit(state + 1);
    });
  }
}

状態管理用Blocクラスは、flutter_blocパッケージのBlocクラスを継承することで作成できる。 ジェネリクスにはsinkされるイベントと、管理したい状態を指定する。

上記の例だと、CounterEventというイベントがBLoCにsinkされ、int型の値を状態として保持して管理する。

状態の初期値は、BLoCクラスのコンストラクタにてsuper()に渡す。

Blocクラスの定義を見るとわかるが、BlocクラスのコンストラクタではinitialStateとして引数を渡すので、Blocクラスを継承したCounterBlocでもsuper()を用いてinitialStateに初期値を渡している。

またコンストラクタには発生しうるイベントとハンドラを登録する。onメソッドで登録する。

on<CounterIncrementPressed>((event, emit) {
      emit(state + 1);
    });

ハンドラは2つの引数をとり、eventとemitがある。 eventはトリガーとなるイベント。emitにはイベント発生後の状態を渡す。 stateでemit前の状態、今回だとint値を取得できる。 上記コードではカウンターアプリなのでstateをインクリメントしている。

イベントクラスの作成

abstract class CounterEvent {}

class CounterIncrementPressed extends CounterEvent {}

公式では抽象クラスを作成し、それを継承する形でイベントを作成しているので同じように作成した。 イベントを作成する際は抽象クラスを継承するようにする。

Observerクラスの作成

class SimpleBlocObserver extends BlocObserver {
  @override
  void onTransition(Bloc bloc, Transition transition) {
    super.onTransition(bloc, transition);
    print("${bloc.runtimeType} $transition");
  }
}

イベントが発生した際に実行される処理をまとめておける。 onTransitionではイベント発生時に次のようなログを取得できる。

flutter: CounterBloc Transition { currentState: 0, event: Instance of 'CounterIncrementPressed', nextState: 1 }

Observerクラスの配置

Observerクラスは次のように登録することで有効化する。

Bloc.observer = SimpleBlocObserver();

observerはstaticな変数なので、Bloc.observerと記述する。

BlocProviderを配置


class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
        create: (context) => CounterBloc(),
        child: MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
            primarySwatch: Colors.blue,
          ),
          home: const MyHomePage(title: 'Flutter Demo Home Page'),
        ));
  }
}

考え方はproviderと同じだと思われる。 BlocProviderが配置されているツリーの下でcontext.readなどが使用できる。

BlocBuilderを配置

BlocBuilder<CounterBloc, int>(builder: (_, counter) {
              return Text(
                "$counter",
                style: Theme.of(context).textTheme.headlineMedium,
              );
            }),

ジェネリクスには取得したいBlocクラスを指定する。 builderでstateを取得してwidgetを作成できる。

ちなみに次のようにBlocBuilderを使わずに書くこともできる。

Text(
              context.watch<CounterBloc>().state.toString(),
              style: Theme.of(context).textTheme.headlineMedium,
            )

ただしwatchを使うと、状態が更新された際の追加処理が記述しにくいため、BlocBuilderの方が拡張性が高いと思われる。

イベントのadd

floatingActionButton: FloatingActionButton(
        onPressed: () {
          context.read<CounterBloc>().add(CounterIncrementPressed());
        },
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),

イベントをBLoCにsinkするにはaddメソッドを使う。引数には渡したいイベントを指定するだけ。

参考資料

bloclibrary.dev