BLoCパターンとは
BLoC(Business Logic Component)パターンとは、アプリケーションのビジネスロジックを
UIから分離するためのデザインパターンのことで、このパターンを採用することで、
アプリケーションの状態管理が容易になり、UIとビジネスロジック間の依存関係を
緩和することができます。
Bloc最大の特徴は Stream
を活用していることです。
Stream
を活用することで状態を監視し、リアクティブにUI側を更新することができます。
Blocの基本概念
BLoCパターンは、主に以下の4つの要素で構成されています。
-
ビジネスロジックの分離: BLoCパターンでは、アプリケーションのビジネスロジックを
UIから切り離します。これにより、ビジネスロジックの再利用性が向上し、
UIコンポーネントが単純化され、コードが簡潔になります。 -
イベント駆動: UIからの入力やアクションをイベントとして受け取り、
それに基づいて状態を変更します。そのため、入力やアクションなどの
イベントとして定義した機能をより柔軟に制御することができます。 -
状態管理: Blocは、アプリケーションの状態を管理します。
アプリケーションの状態が変化するたびに、UIが更新されるため、
アプリケーション全体の状態を一貫して追跡することができます。 -
単一責任の原則: BLoCパターンは、単一責任の原則に基づいています。
各Blocは、特定のビジネスロジックに責任を持ち、その責務を明確にします。
これにより、コードの保守性や拡張性が向上します。
Stream
Blocを理解する前に、まず Stream
を理解する必要があります。
Stream とは小川という意味で、小川の上流でデータを流して、
下流でデータを受け取るというイメージになります。
では Stream
を使用するとどのようなメリットがあるのでしょうか?
-
非同期処理の扱いが簡単
非同期で連続したデータの流れを処理できるため、非同期処理を扱いやすくなります。
Future
などと比べると、Stream
を使えば複数回のデータ受信が可能で、
より柔軟に対応することができます。 -
メモリ効率が良い
イベントがあった時だけデータを発行するため、メモリ効率が良くなります。
毎回全てのデータを更新する必要がないので、パフォーマンスの面でも有利です。
小川のイメージにもあるように、Streamに流れるデータのみを監視しておけば良いので、
データを入れる処理とデータを取り出す処理が互いに意識する必要がありません。
自身のタイミングで処理を行うことができます。
Stream実例
以下のコードは Stream
を使ったシンプルなサンプルコードです。
このコードでは、1から10までの数値を順番に生成し、
それを Stream
を通じて1秒ごとに出力しています。
import 'dart:async';
void main() {
final streamController = StreamController<int>();
// Streamを生成する
final stream = streamController.stream;
streamController.sink.add(1);
// 1秒ごとに数値をStreamに送信する
Timer.periodic(Duration(seconds: 1), (timer) {
final value = timer.tick + 1;
if (value <= 10) {
streamController.sink.add(value);
} else {
timer.cancel();
streamController.close();
}
});
// Streamから数値を受信して出力する
stream.listen((value) {
print(value);
}, onDone: () {
print('Stream is closed');
});
}
このコードでは、 StreamController
を使用して Stream
を作成し、
1秒ごとに1から10までの数値を送信しています。
StreamController
は、 Stream
を制御するためのクラスです。
<int>
は、この Stream
が int
型のデータを扱うことを示しています。
sink
は、Streamにデータを追加するためのインターフェースです。
数値を送信した後は、 listen
メソッドを使用して Stream
から数値を受信し、
それをコンソールに出力しています。
Stream
がすべての数値を送信し終えると、
'Stream is closed'というメッセージが表示されます。
上記のコードを実行すると以下のように出力されます。
1
2
3
4
5
6
7
8
9
10
Stream is closed
Streamまとめ
Stream
は、川の流れのようなもので、川の上流から水が絶え間なく下流に
向かって流れていきます。この水の流れが、 Stream
がデータを発信する
様子に例えられています。
川の下流に住む人々は、この水の流れから必要な水を汲み上げて利用することができます。
これが、プログラムが Stream
からデータを受け取り、処理を行うことに例えられます。
この川の流れには、以下のような特徴があります。
-
水は一度に全て流れ着くのではなく、少しずつ継続して流れ続けます。
Stream
も同様に、データを一括で受け取るのではなく、
少しずつ継続して受け取ることができます。 -
川の流れは止まることなく絶え間なく続きます。
Stream
も同様に、データの発信が継続する限り、
プログラムはデータを絶え間なく受け取り続けることができます。 -
川の流れは、上流で新しい水が加わると、下流の人々に新しい水が届きます。
Stream
も同様に、新しいデータが発信されると、
プログラムにリアルタイムで新しいデータが届きます。 -
川の流れが枯れた場合、下流の人々は水を得られなくなります。
Stream
でもデータの発信が止まれば、プログラムはそれ以上データを
受け取ることができなくなります。
このように、Streamは継続的にデータを発信し、プログラムが発信されるデータを
絶え間なく受け取ることができる仕組みです。他方のことを深く考慮せずとも、
データの入出力ができるため、相互の変更による影響が受けにくく、リアルタイムで
データの変更を受け取れるので、アプリの状態を常に最新に保つことができます。
BLoCパターン 主な要素
BLoCパターンは上記の Stream
の機能を使用して、ビジネスロジックを
独立したコンポーネントとして抽象化する設計パターンです。
では、どういった要素でBLoCパターンは構成されるのでしょうか?
BLoCは大きく分けて以下の4つの要素で構成されています。
-
イベント(Events): ユーザーのアクションやシステムからの通知など、
アプリケーション内で発生する事象を担当します。 -
状態(States): アプリケーションの状態を管理し、UIの表示内容を決定します。
-
Bloc: イベントと状態の間のマッピング(項目同士の紐付け)を管理し、
ビジネスロジックを実装します。Blocはイベントを受け取り、
状態を生成してUIに渡します。 -
UI レイヤー: ユーザーインターフェースを構築し、
Blocから提供される状態を受け取って表示します。
BLoCパターン実例
BLoCパターンは、 flutter_bloc
パッケージを使用して、実装することが一般的ですが、
ここでは基本的なBLoCパターンの概念を説明するために
Flutter標準の機能を使ってサンプルコードを解説します。
Flutterでは、BLoCパターンを実装するための基本的な機能が提供されており、
それを利用してBlocを作成することができます。
flutter_bloc
パッケージを使用する主な利点は、
BlocとUIレイヤー間のデータの受け渡しを簡素化し、
より簡潔なコードを記述できることです。
さらに、 StatefulWidget
や StreamBuilder
などのウィジェットを
使用せずに実装することができるため、
より直感的にUIを更新することができます。
以下は、カウンターアプリを例にしたBlocの実装です。
import 'dart:async';
import 'package:flutter/material.dart';
// BLoCのインターフェース
abstract class CounterBloc {
void increment();
void decrement();
Stream<int> get counter;
}
// カウンターのBLoC
class CounterBlocImpl implements CounterBloc {
int _counter = 0;
// ストリームコントローラー
final _counterController = StreamController<int>.broadcast();
@override
void increment() {
_counter++;
_counterController.sink.add(_counter);
}
@override
void decrement() {
_counter--;
_counterController.sink.add(_counter);
}
@override
Stream<int> get counter => _counterController.stream;
}
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: CounterPage(),
);
}
}
class CounterPage extends StatefulWidget {
@override
_CounterPageState createState() => _CounterPageState();
}
class _CounterPageState extends State<CounterPage> {
late final CounterBloc _counterBloc;
@override
void initState() {
super.initState();
_counterBloc = CounterBlocImpl();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Counter App'),
),
body: Center(
child: StreamBuilder<int>(
initialData: 0,
stream: _counterBloc.counter,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text('Count: ${snapshot.data}',
style: TextStyle(fontSize: 24.0));
} else {
return CircularProgressIndicator();
}
},
),
),
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
FloatingActionButton(
onPressed: _counterBloc.increment,
tooltip: 'Increment',
child: Icon(Icons.add),
),
SizedBox(height: 16),
FloatingActionButton(
onPressed: _counterBloc.decrement,
tooltip: 'Decrement',
child: Icon(Icons.remove),
),
],
),
);
}
}
Events(イベント)
このサンプルコードでは、イベントは明示的に定義されていませんが、 CounterBloc
の increment()
と decrement()
メソッドがイベントを表しています。
これらのメソッドが呼び出されたときに、状態が更新されます。
States(状態)
状態は、CounterBlocImpl
クラスの _counter
変数によって表現されています。
この変数は現在のカウントの値を保持しており、カウンターの状態を示しています。
Bloc
Bloc
は、 CounterBlocImpl
クラスによって表現されています。
このクラスはカウンターのビジネスロジックを実装しており、
イベントの受け取りと状態の更新を行います。
UI レイヤー
UI レイヤーは、 CounterPage
クラスによって表現されています。
このクラスは、ユーザーインターフェースを構築し、
Blocから提供された状態に基づいてUIを更新します。
具体的には、 StreamBuilder
ウィジェットを使用してカウンターの状態を監視し、
その状態に応じてUIを更新します。
また、ボタンをタップすることでイベントを発生させ、Blocにそれを伝えます。
// BLoCのインターフェース
abstract class CounterBloc {
void increment();
void decrement();
Stream<int> get counter;
}
-
CounterBloc
クラスは、Blocのインターフェースを定義しています。
increment()
とdecrement()
メソッドでカウンターの値を増減させ、
counter
Streamでカウンターの現在値を公開しています。
// カウンターのBLoC
class CounterBlocImpl implements CounterBloc {
int _counter = 0;
// ストリームコントローラー
final _counterController = StreamController<int>.broadcast();
@override
void increment() {
_counter++;
_counterController.sink.add(_counter);
}
@override
void decrement() {
_counter--;
_counterController.sink.add(_counter);
}
@override
Stream<int> get counter => _counterController.stream;
}
-
CounterBlocImpl
クラスでは、CounterBloc
インターフェースを
実装しています。_counter
変数でカウンターの値を保持し、
StreamController
を使ってカウンターの値をStream
として提供しています。
またbroadcast()
メソッドを使用して、複数のリスナーが同じイベントを
受信できるように設定しています。 -
increment()
とdecrement()
メソッドでは、_counter
の値を増減させた後、_counterController.sink.add(_counter)
でStreamに新しい値を追加しています。 -
counter
ゲッターでは、カウンターの値をStreamとして返し、
カウンターの変更を監視しています。
class _CounterPageState extends State<CounterPage> {
late final CounterBloc _counterBloc;
@override
void initState() {
super.initState();
_counterBloc = CounterBlocImpl();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Counter App'),
),
body: Center(
child: StreamBuilder<int>(
stream: _counterBloc.counter,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text('Count: ${snapshot.data}',
style: TextStyle(fontSize: 24.0));
} else {
return CircularProgressIndicator();
}
},
),
),
floatingActionButton: Column(
-
initState()
メソッドは、ウィジェットが初期化されたときに呼び出されます。CounterBlocImpl
クラスのインスタンスを作成して、
_counterBloc
フィールドに割り当ててます。 -
StreamBuilder<int>
ウィジェットを使用して、
_counterBloc
のストリームからカウンターの値を受信し、UIを更新しています。
カウンターの値がある場合は、Text ウィジェットにカウンターの値を表示し、
データがまだ到着していない場合はローディングインジケーターを表示します。
まとめ
このコードでは、UIとビジネスロジックが明確に分離されています。
UIの役割は、Blocからカウンターの値をストリームとして受け取り、
表示することだけです。
一方、Blocはカウンターの値を管理し、インクリメントやデクリメントのロジックを
実装しています。このように責務が分離されているため、
BLoCパターンを用いて実装することで、コードの保守性とテスト容易性が向上します。
これを機にぜひBLoCパターンを用いて実装してみてください。
参考資料
告知
最後にお知らせとなりますが、イーディーエーでは一緒に働くエンジニアを
募集しております。詳しくは採用情報ページをご確認ください。
みなさまからのご応募をお待ちしております。