Bloc
Bloc
とは、 Cubit
の上位概念で、ビジネスロジックを含む
コンポーネントのことを指します。 Bloc
を使用することで、イベントを受け取り、
受け取ったイベントに応じて状態を変更することができます。
BlocパターンのBlocとは?
Blocパターンでは States
と Events
を使って状態を管理します。
その状態とイベントを管理する際に、使用されるのが Bloc
です。
Bloc
は、 Events
を受け取り、結果を States
に反映する役割を担っています。
また、 イベント駆動型のアーキテクチャのため、
アプリケーションのビジネスロジックとUIを明確に分離することができ、
複数の状態を管理し、イベントに基づいて状態を遷移させるので、
テストが容易になり、コードの再利用性も向上します。
Bloc
は Cubit
と比較して、より複雑な状態管理が必要な場合に使用されます。
特徴
-
複雑なビジネスロジックを処理できる:
複数のイベントを処理したり、非同期処理を扱ったりすることができます。 -
複数のBlocを組み合わせることができる:
複数のBlocを組み合わせることで、複雑な状態管理を実現することができます。 -
テストが実装しやすい:
ビジネスロジックとUIを明確に分離するため、テスト実装が容易になります。
インストール
Bloc
を使用する際には flutter_bloc
パッケージをインストールする必要があります。
以下のリンクからパッケージをインストールしましょう。
dependencies:
flutter:
sdk: flutter
flutter_bloc: ^8.0.0
flutter_blocパッケージとは
flutter_bloc
パッケージを使うと、FlutterアプリケーションにBlocパターンを
簡単に導入することができるようになります。
Blocパターンとは?
Blocパターンは、ビジネスロジックとUIを分離させ、
アプリケーションの状態管理を効率的に行うためのデザインパターンです。
Blocの構成要素
Bloc(Business Logic Component)は、主に以下の3つの要素から構成されています。
- Events: Blocに送信される入力を表すオブジェクト
- States: Blocの状態を表すオブジェクト
-
Blocクラス:
Events
を受け取り、ロジックを実行し、
States
を更新してUIに通知する
Events
Events
とは、ユーザーの操作や外部からの入力を表すオブジェクトです。
Bloc
は、受け取った Events
に応じて対応するロジックを実行し、 States
を更新します。
例えば、カウンターアプリの場合、カウンタの増加・減少のイベントが
Events
に相当します。
abstract class CounterEvent {}
class IncrementEvent extends CounterEvent {}
class DecrementEvent extends CounterEvent {}
上記の例では、 CounterEvent
を継承した IncrementEvent
と DecrementEvent
を
定義しています。これらのイベントが Bloc
に送信されると、
Bloc
はそれぞれのイベントに対応するロジックを実行し、 States
を更新します。
Events
は、ユーザーの操作に限らず、タイマーの時間管理やAPI呼び出しの結果、
ディープリンクの起動など、様々な入力を定義することがあります。
イベントを定義する際のポイント:
-
明確な命名:
イベント名はその動作を明確に表すようにします。(IncrementEvent、DecrementEvent) -
必要なデータの保持:
イベントに必要なデータをプロパティとして保持し、
Bloc
が適切に処理できるようにします。 -
抽象クラスの利用:
共通の親クラス(抽象クラス)を作成し、具体的なイベントごとにサブクラスを
作成することで、コードの拡張性を高めます。
アプリケーションの要件に応じて適切に Events
を設計することが、
Blocパターンを効果的に活用するための鍵となります。
Statesとは
States
は、Blocの状態を表すオブジェクトです。
Bloc
はイベントを受け取ると、ロジックに基づいて新しい状態を
生成し、その状態をUIに通知します。
UIは通知された新しい状態に基づいて、ウィジェットを再構築します。
class CounterState {
final int count;
CounterState(this.count);
}
上記の例では、 CounterState
クラスが状態を表しています。
count
フィールドにカウンターの値を保持しています。
単純なアプリでは上記のように整数値だけでも良いかもしれませんが、
より複雑なアプリケーションでは、複数のフィールドを持つクラスや、
ネストされたオブジェクトで状態を表現する必要があります。
適切に States
を設計することで、以下のようなメリットがあります。
- 状態を明確に定義できる
- 状態の一部分だけを更新することができる
- ネストされたオブジェクトでも状態を表現できる
- 状態のコピーや比較が容易になる
また、 States
を定義する際は、イミュータブル(不変)なオブジェクトとして扱うことで、
状態の変更を制御でき、予期せぬ副作用を防ぐことができます。
Blocクラス
Blocクラスには、イベントを受け取り、ロジックを実行して、
新しい状態を生成する処理が含まれています。
また、 状態の変化をUIに通知する機能も持っています。
// BlocクラスCounterBloc
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(CounterState(0));
@override
Stream<CounterState> mapEventToState(CounterEvent event) async* {
if (event is IncrementEvent) {
yield CounterState((state.count + 1));
} else if (event is DecrementEvent) {
yield CounterState((state.count - 1));
}
}
}
上記の例では、 CounterBloc
クラスが Bloc
クラスを
継承しています。Bloc
クラスはジェネリック型で、
第1型引数にイベントの型、第2型引数に状態の型を指定します。
-
CounterBloc
のコンストラクタでは、
初期状態としてCounterState(0)
を指定しています。 -
mapEventToState
メソッドは、イベントを受け取り、
ロジックを実行して新しい状態を生成する役割を担っています。 -
IncrementEvent
が来た場合は、カウンターの値を1
増やした新しいCounterState
を
yield
で生成します。 -
同様に、
DecrementEvent
が来た場合は、カウンターの値を1
減らした新しい
CounterState
をyield
で生成しています。
flutter_bloc
パッケージでは、 Bloc
クラスが Stream
を
内部で管理しています。 mapEventToState
で新しい状態を yield
すると、
その状態がストリームに追加されるため、ストリームの管理が容易になります。
また、 flutter_bloc
パッケージには、 Bloc
クラスをUIで
使用するためのウィジェットも提供されているため、これらのウィジェットを
使用すると、 Bloc
パターンを簡単に実装できます。
Bloc
クラスの定義に関しては、上記の Bloc
クラス以外にも
以下のような定義方法も存在します。
import 'package:flutter_bloc/flutter_bloc.dart';
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(const CounterState(0)) {
on<Increment>((event, emit) => emit(CounterState(state.counter + 1)));
on<Decrement>((event, emit) => emit(CounterState(state.counter - 1)));
}
}
先ほどのコードは、 mapEventToState
メソッドをオーバーライドして、
イベントに対するロジックを定義していた一方で、このコードでは、
on<Event>
を使用して、イベントに対するロジックを定義しています。
使い分け
mapEventToState:
mapEventToState
は Bloc
内でイベントに対する処理を記述する従来の方法です。
イベントごとに処理を記述し、新しい状態を発行します。
@override
Stream<CounterState> mapEventToState(
CounterEvent event,
) async* {
if (event is IncrementEvent) {
yield CounterState(state.count + 1);
} else if (event is DecrementEvent) {
yield CounterState(state.count - 1);
}
}
mapEventToState
は Stream<State>
を返す関数です。
yield
キーワードを使って新しい状態を生成します。
イベントごとの処理をif文やswitch文で分岐させる必要があります。
on<Event>
:
on<Event>
メソッドは mapEventToState
以降に導入された新しい方法です。
イベントの種類ごとにコールバック関数を登録することで、
コードの可読性が向上します。
on<IncrementEvent>((event, emit) {
emit(CounterState(state.count + 1));
});
on<DecrementEvent>((event, emit) {
emit(CounterState(state.count - 1));
});
on<Event>
にイベントの型を指定し、コールバック関数を登録します。
コールバック関数の引数には、イベントオブジェクト(event
)と状態を発行するための
関数(emit
)が渡されます。 emit
関数を呼び出して新しい状態を発行します。
mapEventToState
は従来の方法で、旧バージョンとの互換性のためにサポートされています。
on<Event>
は新しい方法で、コードの可読性と保守性が向上するため、
Bloc
クラスを定義する際はこちらを使用することが推奨されています。
-
イベントの種類が少ない場合は
mapEventToState
でも問題ありませんが、
イベントの種類が増えるにつれてon<Event>
の方が望ましくなります。 -
ネストされたif文やswitch文が複雑になる場合は、
on<Event>
を使うことでコードがシンプルになります。
状況に応じて適切な方法を使い分けることが重要です。
flutter_bloc
パッケージでは柔軟性の高い実装が可能で、
アプリケーションの要件に合わせて最適な方法を選択できます。
Blocの動作の流れ
Blocの動作の流れを以下のコードをもとに説明します。
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
// 1. Eventsの定義
abstract class CounterEvent {}
class IncrementEvent extends CounterEvent {}
class DecrementEvent extends CounterEvent {}
// 2. Statesの定義
class CounterState {
final int count;
CounterState(this.count);
}
// 3. Blocの定義
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(CounterState(0)) {
on<IncrementEvent>((event, emit) {
emit(CounterState(state.count + 1));
});
on<DecrementEvent>((event, emit) {
emit(CounterState(state.count - 1));
});
}
}
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: BlocProvider(
create: (context) => CounterBloc(),
child: const CounterPage(),
),
);
}
}
class CounterPage extends StatelessWidget {
const CounterPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Counter App'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
BlocBuilder<CounterBloc, CounterState>(
builder: (context, state) {
return Text(
'Count: ${state.count}',
style: const TextStyle(fontSize: 24),
);
},
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
context.read<CounterBloc>().add(IncrementEvent());
},
child: const Text('Increment'),
),
const SizedBox(width: 16),
ElevatedButton(
onPressed: () {
context.read<CounterBloc>().add(DecrementEvent());
},
child: const Text('Decrement'),
),
],
),
],
),
),
);
}
}
イベントのディスパッチ:
UIやその他のコンポーネントから、 Bloc
にイベントがディスパッチされます。
上記の例では、 onPressed
内でそれぞれ対象のメソッドを呼び出すことで、
IncrementEvent
と DecrementEvent
を Bloc
にディスパッチしています。
イベントの処理:
Blocは受け取ったイベントを処理し、対応するビジネスロジックを実行します。
上記の例では、 CounterBloc
クラスの on<IncrementEvent>
と
on<DecrementEvent>
でイベントに対応するロジックが定義されています。
-
IncrementEvent
が来た場合、state.count + 1
で新しいカウント値を計算し、 -
DecrementEvent
が来た場合、state.count - 1
で新しいカウント値を計算します。
新しい状態のemit:
イベントの処理結果として、新しい状態を生成しています。
Bloc
では emit()
関数を使って新しい状態を生成します。
上記の例では、 emit(CounterState(newCount))
で
新しい CounterState
を生成しています。
状態の監視:
BlocProvider
を使って CounterBloc
のインスタンスを作成し、
CounterPage
に提供しています。
BlocBuilder
を使って CounterBloc
の状態を監視し、状態が変更されるたびに
UIを再構築しています。 ElevatedButton
を使って、 IncrementEvent
と DecrementEvent
をBloc
に追加しています。
この一連の流れを繰り返すことで、Blocパターンによる状態管理が実現されます。
まとめ
Bloc
はビジネスロジックを含むコンポーネントです。
Events
を受け取り、ロジックに基づいて States
を更新します。
UIとビジネスロジックを分離することができ、
テスト容易性やコードの再利用性が高まります。
Blocの全体の流れ
- UIやその他のコンポーネントからイベントがBlocにディスパッチされる
- イベントを受け取り、対応するビジネスロジックを実行する
- ビジネスロジックの結果として新しい状態を生成し、その状態をエミットする
- UIコンポーネントはBlocからの状態の変更を監視し、必要に応じてUIを更新する
Blocを使用することで、複雑なビジネスロジックを持つアプリケーションでも、
可読性が高く、保守しやすいコードを実現することができます。
これを機にぜひ使用してみてください!
告知
最後にお知らせとなりますが、イーディーエーでは一緒に働くエンジニアを
募集しております。詳しくは採用情報ページをご確認ください。
みなさまからのご応募をお待ちしております。