BLoCパターン解説

こんにちはnasustです。 BLoCパターンを解説を解説します。BLoCパターンはFlutterの状態管理のデザインパターンです。

BLoCとはBusiness Logic Componentの略です。ビジネスロジックをコンポーネント化する方法です。 BLoCのルールは以下の通りです。

  • 入力と出力はStream/Sinkのみ
  • 依存関係は注入可能で、プラットフォームに依存しない
  • プラットフォームの分岐処理は禁止
  • 前述のルールに従えば、実装は任意

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
}
dart

結果

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
bash

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);
      });
dart

utf8.decoderLineSplitterは、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}');
  }
}
dart

結果

hello 1
hello 2
hello 3
hello 4
hello 5
bash

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();
  }
}
dart

_incrementController.stream.map_countのインクリメントした値をvoid型からint型に変換します。.mapが返すStreampipe_countControllerStreamにデータが流れるようにします。

_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),
      ),
    );
  }
}
dart

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の更新ができました。

prevnext