BLoCパターンを解説
こんにちはnasustです。 BLoCパターンを解説を解説します。BLoCパターンはFlutterの状態管理のデザインパターンです。
BLoCとはBusiness Logic Componentの略です。ビジネスロジックをコンポーネント化する方法です。 BLoCのルールは以下の通りです。
- 入力と出力はStream/Sinkのみ
- 依存関係は注入可能で、プラットフォームに依存しない
- プラットフォームの分岐処理は禁止
- 前述のルールに従えば、実装は任意
BLoCパターンはイベントの送信、受信のパターンです。Stream/Sinkというインターフェイスを使用することによりViewとビジネスロジックの疎結合を高くしています。
BLoCパターンを解説する前にStreamとSinksを解説します。
Streamとは
Streamとは非同期のイベントやデータのシーケンスを処理ができます。
see also: Asynchronous programming: streams | Dart
see also: Stream class - dart:async library - Dart API
サンプルコード
Asynchronous programming: streams | Dartにあるコードをprintで非同期処理のタイミングを分かりやすくしたものです。
// Copyright (c) 2015, the Dart project authors.
// Please see the AUTHORS file for details.
// All rights reserved. Use of this source code is governed
// by a BSD-style license that can be found in the LICENSE file.
import 'dart:async';
Future<int> sumStream(Stream<Function> stream) async {
var sum = 0;
await for (var value in stream) {
sum += value();
print("sum: ${sum}");
}
return sum;
}
Stream<Function> countStream(int to) async* {
for (int i = 1; i <= to; i++) {
yield () {
print("yield: ${i}");
return i;
};
}
}
main() async {
print("start");
var stream = countStream(10);
print("next");
var sum = await sumStream(stream);
print(sum); // 55
}
結果
start
next
yield: 1
sum: 1
yield: 2
sum: 3
yield: 3
sum: 6
yield: 4
sum: 10
yield: 5
sum: 15
yield: 6
sum: 21
yield: 7
sum: 28
yield: 8
sum: 36
yield: 9
sum: 45
yield: 10
sum: 55
55
Stream<Function> countStream(int to) async*
でyield
の部分が遅延実行されていることが分かります。
Streamの具体的な使用例は以下のコードが分かり易いです。
var lineNumber = 1;
var stream = File('quotes.txt').openRead();
stream.transform(utf8.decoder)
.transform(const LineSplitter())
.listen((line) {
if (showLineNumbers) {
stdout.write('${lineNumber++} ');
}
stdout.writeln(line);
});
utf8.decoderとLineSplitter
は、StreamTransformerBaseを継承しており、Streamの内容の処理を委譲しています。
Fileの内容が非同期で流れて委譲した処理の結果をlisten
で受け取っています。
以下の動画でStreamが分かりやすく解説されています。
Sinkとは
Sinkとは、紐付いているStreamにイベントやデータを送信するインターフェイスを提供します。後で解説するStreamControllerで実装されています。
see also: Sink class - dart:core library - Dart API
StreamController
そして主にBloCで使用されるのがStreamControllerです。
see also: StreamController class - dart:async library - Dart API
Streamを扱い易いようにしたコントローラーです。扱いやすくなっているのでBloCパターンの解説で、このクラスの登場が多いです。
.sink.add
メソッドでstreamにデータを流し、.stream.listen
で受け取ることができます。
サンプルコード
import 'dart:async';
void main() {
final stringController = StreamController<String>();
stringController.stream.listen(print);
for (int i = 0; i < 5; i++) {
stringController.sink.add('hello ${i + 1}');
}
}
結果
hello 1
hello 2
hello 3
hello 4
hello 5
StreamControllerを用いたBloCパターン
プロジェクト作成時の初期のソースをBLoCに置き換えて実装しました。 プロジェクトは以下のページにあります。
BLoC
StreamControllerでBLoCを実装したソースは以下の通りです。
import 'dart:async';
class CounterBloc {
final _incrementController = StreamController<void>();
final _countController = StreamController<int>();
var _count = 0;
CounterBloc() {
_incrementController.stream
.map((v) => _count++)
.pipe(_countController);
}
Sink<void> get increment => _incrementController.sink;
Stream<void> get counter => _countController.stream;
void dispose() async {
await _incrementController.close();
await _countController.close();
}
}
_incrementController.stream.map
で_count
のインクリメントした値をvoid型からint型に変換します。.map
が返すStream
のpipe
で_countController
のStream
にデータが流れるようにします。
_incrementController.sink.add
の値が_countController.stream.listen
で受け取れるようになります。
see also: map method - Stream class - dart:async library - Dart API
see also: pipe method - Stream class - dart:async library - Dart API
UI
import 'package:flutter/material.dart';
import 'bloc.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter BLoC Counter'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final _bloc = CounterBloc();
void dispose() {
super.dispose();
_bloc.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
StreamBuilder<int>(
stream: _bloc.counter,
initialData: 0,
builder: (context, snapshot) {
return Text(
'${snapshot.data}',
style: Theme.of(context).textTheme.display1,
);
},
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => _bloc.increment.add(null),
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
int _count
の代わりにBLoCのオブジェクトを使用します。
カウントの数値の更新は、StreamBuilder
を使用します。
StreamBuilder
はイベントを受信するとUIを更新します。
see also: StreamBuilder class - widgets library - Dart API
onPressed: () => _bloc.increment.add(null)
でイベントが追加され、StreamBuilder
で更新されます。
まとめ
BLoCパターンでUIのビジネスロジックが分離でき、StreamControllerとStreamBuilderでイベントベースのUIの更新ができました。