🎃

【Flutter】色んな状態管理で作ってみよう ②BLoC編

2022/02/11に公開

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

Providerの前は主流?BLoC

さて今回はStreamをベースとし、Providerと肩を並べるほど認知されている状態管理パッケージのBlocを使ってカウンターアプリを作っていきます

使用するPackage:

概要

  • Providerの次に人気な状態管理手法(pub.devのLIKE数は2番目に多い)
  • Streamを活用した状態管理手法
  • リアクティブプログラミングの考え方をベースに考案されたBLoCパターンを実現するパッケージ
  • EquatableパッケージなどをリリースしているFelix Angelov氏が開発
  • 初版リリースは2018年10月

全体像

BLoCパターンとは

BLoCとは「Business Logic Component」の略語。その名を冠したBLoCパターンはビジネスロジックを集約したコンポーネントを活用し状態管理を行う手法です。

この手法はGoogleの開発者により開発され、2018年のGoogle I/Oで紹介されました。

最大の特徴はStreamを活用している事。Streamを活用する事で常に流し込まれる状態値を監視し、反応的(リアクティブ)にUI側を更新していきます。

BLoCパターンは以下によって構成されます

  1. ビジネスロジックを集約した状態管理クラス
  2. 状態管理クラスにEventオブジェクトを流し込むStream
  3. 受け取ったEventオブジェクトに応じて状態オブジェクトをUIに流し込むStream

また常に新しいStateオブジェクトをStreamに流す為、状態値を変更していくわけではなくImmutableな状態値を扱う状態管理手法となります。

BlocクラスとCubitクラス

BLoCパターンに沿った状態管理を単体で実現する為に作られたのがblocパッケージです。

状態管理クラスはblocパッケージが用意してくれているBlocクラスを継承するのですが、より簡易的なCubitクラスと言うものも用意されています。

Blocクラスを使う場合、状態値をStreamに流し込むメソッドを発火するのにEventオブジェクトをStream経由で渡す必要があります。

Cubitクラスを使う場合は状態管理クラスに定義したメソッドに直接アクセスする事が出来、状態値をStreamに流し込む為のemitメソッドと言うのが用意されています。

図で表すと下記のようなイメージ

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

Blocを使う場合、

  • Blocクラス:状態管理クラスにBloc機能を継承するクラス
  • Eventオブジェクト:状態管理クラス内のメソッドを発火させる指示を与えるオブジェクト
  • mapEventToStateメソッド:UIから流されてくるEventオブジェクトを一手に引き受け、対応する処理を実行するメソッド。EventとStateの繋ぎ役。

Cubitを使う場合、

  • Cubitクラス:状態管理クラスにCubit機能を継承するクラス
  • emitメソッド:StateオブジェクトをUIに繋がるStreamに流すメソッド

Bloc、Cubit共通、

  • Stateオブジェクト:UIに繋がるStreamに流される状態値のオブジェクト
  • BlocProviderクラス:Widgetツリーに沿って状態管理クラスの依存関係を注入するクラス
  • BlocBuilderクラス:Stateオブジェクトが流れてくるStreamを監視し、Stateオブジェクトに応じてラップした子widgetを再描画するクラス

準備

具体的にカウンターアプリを例に使い方を見ていきましょう。まずはBlocクラスを使った場合から。

ソースコードはこちら⬇︎
blocを使ったサンプル
cubitを使ったサンプル

1. Stateクラス

  • 今回はCountフィールドを持つCounterStateクラスを定義。
  • このCounterStateオブジェクトがEventに応じて、UI側にStreamを通じて流し込まれていきます。

Equatableは「同じ状態値を持つインスタンスを同一として扱うクラス」です。 blocパッケージと同じ開発者が開発している為、相性が良く一緒に使われている例が多いです。

class CounterState extends Equatable {
  final int count;
  const CounterState({ this.count});

  
  List<Object> get props => [count];
}

2. Eventクラス

次にUI側から状態管理クラスにStreamを通じて流し込まれるEventクラスです。
今回はincrement、decrement、resetの3つのイベントを用意します。

abstract class CounterEvent extends Equatable {
  const CounterEvent();
  
  List<Object> get props => [];
}

class IncrementEvent extends CounterEvent {}

class DecrementEvent extends CounterEvent {}

class ResetEvent extends CounterEvent {}

3. 状態管理クラス

  • Blocクラスを継承する事でmapEventToStateメソッドを使える様になります。
  • UI側から流れてきたEventオブジェクトを検知し、それに応じた処理の分岐を記述します。
  • また最新のStateオブジェクトを格納したstate変数にアクセスすることもできます。
class CounterBloc extends Bloc<CounterEvent, CounterState> {
  CounterBloc() : super(CounterState(count: 0));

  
  Stream<CounterState> mapEventToState(CounterEvent event) async* {
    if (event is IncrementEvent) {
      yield CounterState(count: state.count + 1);
    } else if (event is DecrementEvent) {
      yield CounterState(count: state.count - 1);
    } else if (event is ResetEvent) {
      yield CounterState(count: 0);
    } else {
      yield CounterState(count: state.count);
    }
  }
}

4.UIへ注入

  • blocパッケージでも依存関係の注入にはProviderを使用します。
  • providerパッケージの時とほぼ同じです。違いはflutter_blocパッケージのBlocProviderを使う事くらい。
  • createフィールドで状態管理クラスCounterBlocをインスタンス化
  • childフィールドに定義した_BlocCounterPage widgetにインスタンスを注入
  
  Widget build(BuildContext context) {
    return BlocProvider<CounterBloc>(
      create: (context) => CounterBloc(),
      child: const _BlocCounterPage(),
    );
  }

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

状態へのアクセス

UI➡︎状態管理クラスのStream

状態管理クラスへ続くStream、言うなれば上りのStreamにアクセスするには、BlocProvider.of<T>(context)もしくはcontext.read<T>()を使います。

addメソッドを使って、実行したい処理のEventオブジェクトを流し込みます。

final CounterBloc counterBloc = BlocProvider.of<CounterBloc>(context);

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

状態管理クラス➡︎UIのStream

状態管理クラスからUIへの下りのStreamへのアクセスにはBlocBuilderクラスを使います。

ラップしたWidgetにStreamで流れてきたStateオブジェクトを通知し、UIを再描画します。

    BlocBuilder<CounterBloc, CounterState>(
        builder: (context, state) => Text(
        '${state.count}',
        style: Theme.of(context).textTheme.headline4,
        ),
    ),

Cubitの場合

Cubitクラスを使った場合は以下のような違いがあります

  1. 処理の発火には状態管理クラスのメソッドに直接アクセスする
    • 状態管理クラスへの上りのStreamがなくなる
    • Eventクラスが不要
    • mapEventToStateメソッドが不要
  2. 状態値の流し込みにはemitメソッドを使う

状態管理クラス

emitメソッドの引数に渡されたstateオブジェクトがUI側に向かうStreamに流し込まれていきます。

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

  void increment() => emit(CounterState(state.count + 1));
  void decrement() => emit(CounterState(state.count - 1));
  void reset() => emit(CounterState(0));
}

メソッドへのアクセス

providerパッケージを使う場合と全く同じです

    FloatingActionButton(
        onPressed: () => context.read<CounterCubit>().increment(),
        tooltip: 'Increment',
        heroTag: 'Increment',
        child: Icon(Icons.add),
    ),

全体

コード全体についてはblocクラスを使ったサンプルとcubitクラスを使ったサンプルとで長くなってしまうので、Githubのリンクで失礼しますm(_ _)m

サンプルコード

以上でした

いかがでしたでしょうか?

自分はProviderから入ったので、Blocで実際運用をした事はありませんが、Providerとは全く違ったパラダイムの手法で非常に興味深かったです。

現在でも海外の記事を多く見るので、海外で開発するような事があればblocパターンで開発するという事もあるのかなと思います。

参考

Discussion