flutter_blocを使ったBLoC入門

Flutter歴1週間の私が [flutter_bloc - pub.dev](https://pub.dev/packages/flutter_bloc) を **なんとなく** 使えるようになるまでのまとめです。

いろいろ試して遊んでいたリポジトリも貼っておきます → [flutter-bloc-test](https://github.com/tomo-Gomibako/flutter-bloc-test)
以下ではソースコードを抜粋して説明していきますので、全コードを読みたいときはこのリポジトリを参照してください。

# BLoCパターン
BLoC = Business Logic Component

StateをViewから切り離して管理することで、

- キレイ (疎) に実装できる！
- Widget間でのデータのやり取りもできる！

StateはRxっぽく ([違うところ](https://pub.dev/packages/rxdart#rx-observables-vs-dart-streams)もあるけど) 更新できる！

って感じ。違ったらごめんなさい。

---
- もっと厳密で詳しい説明が必要な方
- BLoCがどうやってデータを保持・更新しているか知りたい方

などには、[丁寧でわかりやすい記事](https://qiita.com/tetsufe/items/521014ddc59f8d1df581#blocbloc%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3%E3%81%A8%E3%81%AF)を書いてくださっている方がいらっしゃるので、そちらへどうぞ。
(私も練習中に参考にさせていただきました。ありがとうございました。)

## Bloc型
Rxと聞いてビビっている方に朗報です。
flutter_bloc における Bloc型 は「Rxらしい」部分が
とてもいい感じにラップされているので安心してご利用いただけます。

# Cubit と Bloc
flutter_bloc には Cubitクラス と Blocクラス というお膳立てされたBLoCがあるのでStreamを直接触る必要はありません。
どちらも同じような働きをするのですが、理解しやすいCubitから見ていきます。

## Cubit
CubitクラスはStateの型を受け取ります。
[例のカウンターアプリ](https://flutter.dev/docs/development/ui/widgets-intro#changing-widgets-in-response-to-input)を例にとると、

``` lib/blocs/counter.dart
import 'package:flutter_bloc/flutter_bloc.dart';

class CounterCubit extends Cubit<int> {  // カウンターなのでStateはint型
  CounterCubit() : super(0);  // 初期値をsuperにわたす

  void increment() {
    emit(state + 1);  // emitでstateの更新と変更通知
  }
}
```

あとはボタンで`increment`を呼べば 1 ずつ足されていきます。

## Bloc
対してBlocクラスはCubitクラスにイベント種別を渡せるようにしたものです。
上の例と同様のBLoCを実装すると、

``` lib/blocs.counter.dart
import 'package:flutter_bloc/flutter_bloc.dart';

enum CounterEvent { increment }  // incrementイベントを定義

class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0);  // ここはCubitと同じ

  Stream<int> mapEventToState(CounterEvent event) async* {  // イベント種別が渡ってくる
    switch (event) {
      case CounterEvent.increment:  // incrementイベントを受け取ったときの操作を定義
        yield state + 1;
        break;
      default:
        addError(Exception('unsupported event'));
    }
  }
}
```

こっちはBlocインスタンスに生えている`add`メソッドにイベント種別を渡すとStateの更新ができます。
詳しくは次項かGitHubで…

### Blocの利点
Cubitと同じ動作をBlocにさせる意味は無いと言って良いでしょう。
上でも述べたように、Blocを使った真骨頂は「イベント種別を渡せる」ことです。
つまり、同一のStateに2つ以上の操作を定義したいときはCubitではなくBlocを使おう、ということです。

元々のFlutterデフォアプリにはカウンターのdecrement操作はありませんが、Blocを使えば簡単に追加できます。

``` lib/blocs/counter.dart
import 'package:flutter_bloc/flutter_bloc.dart';

enum CounterEvent { increment, decrement }  // decrementイベントを追加

class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0);

  Stream<int> mapEventToState(CounterEvent event) async* {
    switch (event) {
      case CounterEvent.increment:
        yield state + 1;
        break;
      case CounterEvent.decrement:  // decrementイベントを受け取ったときの操作を定義
        if (state <= 0) {  // カウンターが負の値を取らないように管理
          break;
        }
        yield state - 1;
        break;
      default:
        addError(Exception('unsupported event'));
    }
  }
}
```

---

また、CubitはBlocを継承しているので、定義するときは少し書き方が違いますが、使う先ではかなり互換性があります。

# Provider
BLoC (State) を Widget (View) と疎な感じで結びつけて使えるようにするにはProviderを使います。
あの有名なproviderパッケージを使うのではなく、
flutter_blocの中にBlocProviderというものが提供されています。

flutter_blocさえimportしておけばなんでもできますね！

``` lib/main.dart
void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: BlocProvider(
        create: (_) => CounterBloc(),  // 予め定義しておいたCounterBlocをインスタンス化する
        child: Home(title: 'Flutter Demo Home Page'),  // childにBLoCが渡る
      ),
    );
  }
}
```

Cubitなら

``` lib/widgets/home.dart
floatingActionButton: FloatingActionButton(
  tooltip: 'Increment',
  child: Icon(Icons.add),
  onPressed: () => context.read<CounterCubit>().increment(),
)
```

で`increment`を呼べます。

Blocなら

``` lib/widgets/home.dart
floatingActionButton: FloatingActionButton(
  tooltip: 'Increment',
  child: const Icon(Icons.add),
  onPressed: () => context.read<CounterBloc>().add(CounterEvent.increment),
),
```

で`add`にincrementイベントを渡せます。

BLoCからStateを取得したいときは、表示するWidget (今回はText) をBlocBuilderで囲むだけ！

``` lib/widgets/home.dart
BlocBuilder<CounterBloc, int>(  // BlocBuilder<BLoC (Cubit/Bloc 共通), Stateの型>
  builder: (_, count) {  // builderの引数は BuildContext と State
    return Text(
      '$count',
      style: Theme.of(context).textTheme.headline4,
    );
  },
),
```

# おわり
Flutterデフォアプリの flutter_bloc版 再実装はこれがすべてだと思います。
あとは、Stateの型を変えたり表示部分のWidgetを変えたりするといろいろ応用できるはずです。

実際に最初に紹介したテスト用のリポジトリでは、ボタンを押すたびにリストに要素が追加されていくページも作ってみましたので、困ったら(?)参考にしてください。

# 追記
**2021/7/31**
flutter_bloc v7.0.0 からBuildContextの `bloc` メソッドが削除され、`read` か `watch` を使うことになりました。
これに伴って本記事のコードも書き換えています。
→ [Changelog](https://pub.dev/packages/flutter_bloc/changelog#700-dev2)