Flutter BLoCの基礎から実務活用まで初心者でもわかるステート管理
はじめに
Flutterでアプリを開発する際には、画面の状態をどのように管理するかが大きなテーマになります。
その中でよく聞くのがBLoCパターンです。
BLoCは「Business Logic Component」の略称で、ビジネスロジックをUIから分離して管理する方法としてよく使われています。
初心者の方の中には「BLoCの仕組みが難しい」「状態管理をどうコードに落とし込めばいいのかわからない」といった疑問を持つことがあるかもしれませんね。
ただ、考え方をしっかり理解してしまえば、画面ごとや機能ごとにコードが整理しやすくなります。
この記事ではBLoCのメリットやイベントと状態の流れ、そして実務での活用シーンなどを順番に解説していきます。
少しでもアプリ開発のヒントになれば幸いです。
この記事を読むとわかること
- BLoCパターンの概要や基本的な仕組み
- イベントと状態を分離して管理するメリット
- 簡単なサンプルコードを通じた実装イメージ
- 実務レベルで考えたときの利用シーンや構成例
BLoCパターンとは何か
BLoCパターンは、UIのロジックとビジネスロジックを分離することで、アプリの可読性や保守性を高める仕組みです。
たとえば画面でボタンを押したり、テキストフィールドに入力があったりするとイベントが発生します。
このイベントをBLoCクラスで受け取り、必要な処理を行ってから状態を更新します。
するとUIは更新された状態を元に再描画されるため、ビジネスロジックの変更がUIに直接影響しにくくなるのです。
一方で、UIはBLoCの状態を監視して、変化があったら画面をリビルドするという単純な役割を担います。
こういった整理された構造は、機能追加や不具合対応のときにコードを見通しやすくします。
そのため、複数人で開発を行う場面でも重宝されやすいといえるでしょう。
イベントと状態が分離されるメリット
BLoCパターンでは イベント (Event) と状態 (State)を分離して考えます。
イベントは「ユーザーがボタンを押した」や「APIからレスポンスを受け取った」など、アプリ内で起こる具体的な出来事です。
状態は「ロード中」「データを表示中」「エラーが発生」など、そのときのアプリの見た目や振る舞いを表します。
この2つを明確に分けるメリットは次のようなものがあります。
- UIの再利用がしやすくなる
- テストがしやすくなる
- バグの原因を特定しやすくなる
UIパーツは状態に応じて見た目を切り替えるだけなので、ロジックの細かい部分をあまり気にしなくても済みます。
また、UIを変えたとしてもビジネスロジックはBLoCクラスに閉じ込められているため、実装者が手を加える箇所をすぐに判断できるでしょう。
テスト面でも、BLoC単体でテスト可能になるケースが多いので、UIテストとは別にビジネスロジックの確認が行いやすいです。
こうしたメリットを考えると、Flutterでアプリを作るときにBLoCを取り入れる価値は大いにあると言えます。
BLoCパターンを支える3つの要素
BLoCパターンの中心となる要素は、大きく分けると次の3つに整理されることが多いです。
- イベント (Event)
- ユーザーやシステムから発生するアクションを定義する
- 状態 (State)
- イベント処理の結果として変化するアプリの状態を保持する
- BLoCクラス
- イベントを受け取り、必要なロジックを実行して状態を更新する本体
実際にFlutterのコードでBLoCを使う場合は、flutter_bloc
パッケージを導入し、Bloc
クラスやBlocProvider
などを活用します。
これによって、イベントから状態へと流れる仕組みがフレームワーク的にサポートされるため、開発者が実装しやすいようになっています。
簡単なコード例でイメージをつかむ
イベントと状態をどう定義すればいいのか、実際のコード例があると分かりやすいですよね。
ここでは数値をカウントアップするBLoCを、簡単に示してみます。
import 'package:flutter_bloc/flutter_bloc.dart'; // イベントの定義 abstract class CounterEvent {} class Increment extends CounterEvent {} class Decrement extends CounterEvent {} // 状態の定義 class CounterState { final int count; CounterState(this.count); } // BLoCクラス class CounterBloc extends Bloc<CounterEvent, CounterState> { CounterBloc() : super(CounterState(0)) { on<Increment>((event, emit) { emit(CounterState(state.count + 1)); }); on<Decrement>((event, emit) { emit(CounterState(state.count - 1)); }); } }
CounterEvent
は「ボタンを押したときに値を増やす」「減らす」といったイベントを想定しています。
CounterState
は現在のカウント数だけを持ち、その値によってUIの表示が変わるわけです。
CounterBloc
は、super(CounterState(0))
で初期状態を0に設定し、その後on<Increment>
やon<Decrement>
でイベントを受け取ったら状態を更新する役割を担います。
このコード例ではカウントを増減するだけの単純な例ですが、実務ではサーバーからデータを取得したり、入力値を検証したりといった処理がBLoC側に組み込まれていきます。
BLoCをUI側で利用する流れ
上記のBLoCを実際の画面でどのように使うのかを、もう少しイメージしてみましょう。
FlutterではBlocProvider
とBlocBuilder
(あるいはBlocListener
)を組み合わせて使うことが多いです。
import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class CounterPage extends StatelessWidget { const CounterPage({Key? key}) : super(key: key); Widget build(BuildContext context) { return BlocProvider( create: (context) => CounterBloc(), child: Scaffold( appBar: AppBar( title: Text('Counter Example'), ), body: BlocBuilder<CounterBloc, CounterState>( builder: (context, state) { return Center( child: Text( 'Count: ${state.count}', style: TextStyle(fontSize: 24), ), ); }, ), floatingActionButton: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ FloatingActionButton( onPressed: () { context.read<CounterBloc>().add(Increment()); }, child: Icon(Icons.add), ), SizedBox(width: 8), FloatingActionButton( onPressed: () { context.read<CounterBloc>().add(Decrement()); }, child: Icon(Icons.remove), ), ], ), ), ); } }
BlocProvider
はBLoCのインスタンスを生成・提供し、BlocBuilder
は状態の変化に応じてUIを再構築するために利用します。
ボタンが押されたときにはcontext.read<CounterBloc>().add(Increment())
のようにイベントを送信します。
これがBLoCに伝わり、状態が更新されるとBlocBuilder
のbuilder
メソッド内が呼ばれて画面が再描画されます。
実務での具体的な活用シーン
実務レベルの開発を考える場合、BLoCパターンは以下のようなシーンで特に力を発揮します。
1. フォームの入力管理
たとえば会員登録画面で複数の入力欄がある場合、入力内容のバリデーションや送信処理をBLoCに集約できます。
2. 非同期処理の進捗管理
ネットワークアクセスやデータベース問い合わせが発生する場面では、ロード中や完了時、エラー時などの状態をBLoCが一元管理します。
3. 複数画面にわたるデータ共有
ショッピングカートやユーザープロフィールなど、複数の画面で共通のデータを使いたいときにもBLoCが便利です。
特に非同期処理においては、イベントが「リクエスト開始」、状態が「ロード中」→「成功」→「エラー」と変化する様子をわかりやすく表せます。
UI側は状態が「ロード中」であればプログレスインジケータを表示し、「成功」であればデータを表示、「エラー」であればエラーメッセージを表示するといった具合に、明快なロジックが書きやすくなります。
大規模開発でのBLoCアーキテクチャ
大規模開発になれば、BLoCをどのようにディレクトリ構造やファイルに分割するかも考える必要があります。
たとえば画面ごとに以下のようなディレクトリ構成に分けるケースがあります。
lib/ features/ login/ bloc/ login_bloc.dart login_event.dart login_state.dart view/ login_page.dart ... home/ bloc/ home_bloc.dart home_event.dart home_state.dart view/ home_page.dart ... ...
このように、機能単位(あるいは画面単位)でディレクトリを分けることで、BLoCに関連するコードがひとつのまとまりとして整理しやすくなります。
複数人で作業するときも、「どのblocに手を加えればいいか」がすぐ分かるため、開発速度や品質の維持に貢献します。
他の状態管理との比較ポイント
FlutterではProvider
やRiverpod
、GetX
など、さまざまな状態管理の方法があります。
BLoCを選ぶ理由としては、イベントと状態が厳密に分離される点が挙げられるでしょう。
この仕組みは公式ドキュメントにも紹介されていることから、多くのFlutter開発者が採用している印象があります。
一方で、BLoCはイベントと状態クラスを作る手間が増えることや、コード量が多くなる傾向があるのも事実です。
しかし、しっかりとロジックを見通せるようになるため、大規模なアプリほどBLoCの恩恵を感じやすいと言われています。
これは「小さな手間をかけることで大きな混乱を回避する」という考え方に近いかもしれませんね。
導入時に気をつけたいポイント
BLoCの実装で気をつけたいこととしては、以下のような点が挙げられます。
- イベントや状態の数が増えすぎる場合は、整理が必要
- イベント名や状態名をわかりやすく命名する
- データの取得・変換ロジックはBLoC内にまとめておく
- 画面全体が複雑化する場合、BLoCを複数に分割することを検討する
イベントや状態を細かく分けすぎると、かえって管理が煩雑になりがちです。
特に大きな機能では、BLoC自体を分割するかどうかを早めに検討すると良いでしょう。
また、状態のクラスが増えるときは命名を工夫して、機能の意図がすぐにわかるようにしておくと開発チーム内で混乱しにくくなります。
機能が増えてコードが追いにくくなってきたと感じたら、早めにBLoCの分割やディレクトリ構成の見直しを検討することがおすすめです。
BLoCを学ぶときの流れ
初心者の方がBLoCを学ぶときは、まず小さなサンプルアプリを作りながら「イベント → 状態更新 → UIのリビルド」の流れをしっかり体感するのがよいでしょう。
先ほど示したカウンター例のようなシンプルな機能から始めると、イベントと状態の仕組みがイメージしやすいです。
ある程度慣れたらネットワーク通信を伴うサンプルを作り、データ取得やエラー処理をどうBLoCに組み込むかを試してみるのも良いステップです。
これによって実際のアプリ開発に近いロジックが理解しやすくなります。
学習を進めるときは、機能を一気に追加しすぎず、一歩ずつ段階的にコードを増やしていく方が理解しやすくなります。
まとめ
BLoCパターンを使うことで、FlutterのUIとビジネスロジックを明確に切り分けられます。
初心者にとってはイベントや状態クラスの追加が少し大変に見えるかもしれませんが、アプリ全体が大きくなってきたときにその整理のしやすさを実感できるでしょう。
実務では画面ごとにBLoCを分けたり、非同期処理をまとめたりといった柔軟な活用が行われることが多いです。
まずは小さなサンプルアプリを作りながら、イベントと状態の流れをつかんでみてください。
そうすれば、どうやってコード全体を構成すれば良いのかが自然と見えてくるのではないでしょうか。