こんにちは!
アカウントを変えたのですが、今後も変わらず備忘録と新しく勉強したのをアウトプットしていく目的で更新していきたいです
今回は、Flutterの状態管理のBloCについて詳しく書いていこうと思います。
今までの状態管理はRiverpodを使用していたのですが、blocを使う機会があったので忘れないように備忘録です!
Riverpodは比較的シンプルに導入しやすく、学びやすいと言われている一方でBloCは設計ルールが多く難易度は上がるそう。
設計図
わかりやすい画像があったので、お借りしました。
1. UI Screen(UI画面)
役割:ユーザーが直接触れる部分(ボタン、フォーム、リストなど)
説明:ユーザーに表示される画面。ユーザーの操作はBlocに伝わり、結果は画面に反映される
👉 ユーザーに見える層
2. BLoC
役割:UIとビジネスロジックの仲介(Presenter / ViewModel)
説明:イベントを受け取り、ロジックを処理して状態(State)を発行する。UIはこのStateを監視して再描画される
👉 ロジックを管理する中心的な層
3. Repository(リポジトリ)
役割:データ取得・保存の仲介(Data Handler)
説明:Blocとデータソース(APIやDBなど)の間でやりとりを担当する層。複数のデータソースをまとめて隠蔽する
👉 「どこからデータを取るか」をBlocに意識させない
4. Network Provider(ネットワークプロバイダー)
役割:データを提供する層(Data Provider)
説明:実際にAPI通信やDBアクセスを行う層。外部とやり取りするコードがここに集約される
👉 最下層。外部データとの接続役
試作
簡単なカウントアプリを実装します
階層
lib/
├── screens/ ← 画面単位の機能をまとめたディレクトリ
│ ├── bloc/ ← 状態管理(BLoC)関連をまとめる
│ │ ├── counter_bloc.dart ← イベントを受けて状態を変えるロジック(Presenter相当)
│ │ ├── counter_event.dart ← どんな操作が起きたかを表す(例:Increment, Decrement)
│ │ └── counter_state.dart ← 今の状態を表す(例:カウント値)
│ ├── model/
│ │ └── counter.dart ← データ構造の定義(ここではカウント値のモデルなど)
│ ├── repository/
│ │ └── counter_repository.dart ← データ取得・保存などの処理(今回は加算・減算のロジック)
│ ├── widget/
│ │ └── counter_display.dart ← UI部品を分離(表示専用Widget)
├── main.dart ← アプリ起動&ルートWidget
1. Model作成
lib/screens/model/counter.dart
class Counter {
final int value;
Counter(this.value);
}
2. Repository作成
lib/screens/repository/counter_repository.dart
class CounterRepository {
int increment(int current) => current + 1;
int decrement(int current) => current - 1;
}
3. Event・Stateを定義
lib/screens/bloc/counter_event.dartlib/screens/bloc/counter_event.dart
abstract class CounterEvent {}
class Increment extends CounterEvent {}
class Decrement extends CounterEvent {}
lib/screens/bloc/counter_state.dart
class CounterState {
final int count;
const CounterState(this.count);
}
4. BloC本体を実装
lib/screens/bloc/counter_bloc.dart
class CounterBloc extends Bloc<CounterEvent, CounterState> {
final CounterRepository repository;
CounterBloc(this.repository) : super(const CounterState(0)) {
on<Increment>((event, emit) {
emit(CounterState(repository.increment(state.count)));
});
on<Decrement>((event, emit) {
emit(CounterState(repository.decrement(state.count)));
});
}
}
5. UIパーツを作る
lib/screens/widget/counter_display.dart
class CounterDisplay extends StatelessWidget {
final int count;
const CounterDisplay({super.key, required this.count});
@override
Widget build(BuildContext context) {
return Text('カウント: $count', style: TextStyle(fontSize: 32));
}
}
6. 呼び出し
/lib/main.dart
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => CounterBloc(CounterRepository()),
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: const MyHomePage(),
),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Bloc Counter')),
body: Center(
child: BlocBuilder<CounterBloc, CounterState>(
builder: (context, state) {
return CounterDisplay(count: state.count);
},
),
),
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
onPressed: () => context.read<CounterBloc>().add(Increment()),
child: const Icon(Icons.add),
),
const SizedBox(height: 10),
FloatingActionButton(
onPressed: () => context.read<CounterBloc>().add(Decrement()),
child: const Icon(Icons.remove),
),
],
),
);
}
}
結果
まとめ
BlocはUIと動きの部分を別ファイルの分けられるので、綺麗にまとまっていて見やすいと実感してます。
あと状態管理のながれも見やすいので、テストしやすい!
いいお勉強になりました