Flutter歴1週間の私が flutter_bloc - pub.dev を なんとなく 使えるようになるまでのまとめです。
いろいろ試して遊んでいたリポジトリも貼っておきます → flutter-bloc-test
以下ではソースコードを抜粋して説明していきますので、全コードを読みたいときはこのリポジトリを参照してください。
BLoCパターン
BLoC = Business Logic Component
StateをViewから切り離して管理することで、
- キレイ (疎) に実装できる!
- Widget間でのデータのやり取りもできる!
StateはRxっぽく (違うところもあるけど) 更新できる!
って感じ。違ったらごめんなさい。
- もっと厳密で詳しい説明が必要な方
- BLoCがどうやってデータを保持・更新しているか知りたい方
などには、丁寧でわかりやすい記事を書いてくださっている方がいらっしゃるので、そちらへどうぞ。
(私も練習中に参考にさせていただきました。ありがとうございました。)
Bloc型
Rxと聞いてビビっている方に朗報です。
flutter_bloc における Bloc型 は「Rxらしい」部分が
とてもいい感じにラップされているので安心してご利用いただけます。
Cubit と Bloc
flutter_bloc には Cubitクラス と Blocクラス というお膳立てされたBLoCがあるのでStreamを直接触る必要はありません。
どちらも同じような働きをするのですが、理解しやすいCubitから見ていきます。
Cubit
CubitクラスはStateの型を受け取ります。
例のカウンターアプリを例にとると、
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を実装すると、
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を使えば簡単に追加できます。
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しておけばなんでもできますね!
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なら
floatingActionButton: FloatingActionButton(
tooltip: 'Increment',
child: Icon(Icons.add),
onPressed: () => context.read<CounterCubit>().increment(),
)
でincrement
を呼べます。
Blocなら
floatingActionButton: FloatingActionButton(
tooltip: 'Increment',
child: const Icon(Icons.add),
onPressed: () => context.read<CounterBloc>().add(CounterEvent.increment),
),
でadd
にincrementイベントを渡せます。
BLoCからStateを取得したいときは、表示するWidget (今回はText) をBlocBuilderで囲むだけ!
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