Help us understand the problem. What is going on with this article?

Stream/RxDart初心者のためのBLoC入門(Flutter)

本記事でできるようになること

  1. BLoCの定義を理解する
  2. BLoCとその周辺の用語を理解する
  3. BLoCを使ったアプリを作ってみる

続き

Stream/Sinkを使いこなす! Stream/RxDart初心者のためのBLoC入門 part2
https://qiita.com/tetsufe/items/7b2f8592f5161104d1cd

BLoC(BLoCパターン)とは

Business Logic Componentの略。ビジネスロジックを切り出して再利用・修正などをしやすくするためのコンポーネントであり、状態管理に関するアーキテクチャパターンです。

BLoCは、クラスで作ります。 例を以下に示します。詳しい説明は後に行うので、ここではそれほどBLoCは巨大なものではないということを感じていただければそれでOKです。

counter_bloc.dart
import 'dart:async';

class CounterBloc {
  final _actionController = StreamController<void>();
  Sink<void> get increment => _actionController.sink;

  final _countController = StreamController<int>();
  Stream<int> get count => _countController.stream;

  int _count = 0;

  CounterBloc() {
    _actionController.stream.listen((_) {
      _count++;
      _countController.sink.add(_count);
    });
  }

  void dispose() {
    _actionController.close();
    _countController.close();
  }
}

BLoCの定義

https://youtu.be/PLHln7wHgPE?t=1438 より、以下のルールに従うものがBLoCであるとされています。(動画中では英語なので筆者訳。参考にした訳: https://medium.com/flutter-jp/bloc-provider-70e869b11b2f)

  1. BLoCの入出力はStream/Sinkを使う
  2. BLoCの依存は必ず注入可能でプラットフォームに依存しない
  3. BLoC内にプラットフォームごとの分岐処理は書いてはいけない

単純な用途では、「1. 入出力はStream/Sinkを使う」を意識していればOKだと思います。(複雑な例でなければ2や3を意識する必要がないため)

Stream/Sink は後で説明します。要はBLoCとWidget間で状態変数を受け渡しするためのものです。

イメージ図

複数Widget間でBLoCを通じて(Sink/Streamを使って)値を受け渡すイメージです。

必ずしもWidget間でなくてもよく、APIのレスポンスをBLoCを通じてWidgetに渡すパターンもよくあります。

v2-6b7fcb8fe60be04e6250828ea06f6359_1200x500.jpg

引用: https://pic1.zhimg.com/v2-6b7fcb8fe60be04e6250828ea06f6359_1200x500.jpg

BLoCの使い所

  • web APIなどを使った 非同期処理を扱うロジックとUIを絡めたいとき (RxをBLoCに組み込める)
  • ボタン操作などユーザからのアクションで状態変数が更新され、その 状態変数を複数のWidgetで共有したいとき
    • Flutterでよく見るカウンターアプリなど

また、僕自身誤解していたのですが、BLoC自体は「複数のWidget間で一つの状態を共有/操作する」という機能は持ち合わせていません。比較されがちなScopedModel / Provider / InheritedWidget / Reduxなどのパターンとは異なるポイントです。それどころか、これらのパターンとBLoCは共存可能です。

ではなぜScopedModelなどと比較されることがあるのかというと、個人的な見解ではありますが、BLoCは「複数のWidget間で一つの状態を共有 / 操作する」という使い方と相性が良いからだと思っています。

相性が良いとはどういうことかという話になりますが、まず、Providerパッケージなどを使って複数のWidgetで一つのBLoCを共有することで、InheritedWidgetやScopedModelと同じように、「複数のWidget間で一つの状態を共有 / 操作する」ことができます。

また、BLoCの定義として提唱されている、「Stream/Sinkの使用」も良いポイントです。FlutterのStreamBuilderというWidgetを活用することで、Streamの値の変更を検知してその値に関係する最小限のWidgetだけを更新することができ 、パフォーマンス面で効率よく使えます。一方で、Flutter標準のsetState()を使う場合StatefulWidget全体がリビルドされてしまいます。

登場人物・キーワード

BLoCは初心者にとっては初見のキーワードが多く、混乱してしまいがちなので、先に整理します。

BLoC本体

BLoCは一つのクラスとして表現されることが多く、BLoCクラスを作るために必要な要素が以下になります。

  • Stream
    • listen()メソッドなどで、値が流れてきた時に自動で行う処理を設定できる。
  • Sink
    • add()メソッドを使って、Streamに新しい値を流す。
  • StreamController
    • StreamとSinkをつなげる役割。
  • Rx・Observable
    • Stream/StreamControllerの拡張

BLoCと相性が良いもの(実質セット)

BLoCクラスを用いるときに相性が良いものです

  • Provider(Providerパッケージなど)
    • 一つのBLoCを複数のWidgetで使えるようにする
      • InheritedWidgetやScopedModelDescendantなどと用途が似ている
    • Widgetが使われなくなった時にBLoCのメモリ解放をしてくれるように設定することもできます
  • StreamBuilder
    • これでWidgetをラップすることで、BLoCのstreamを使ってピンポイントにUIを更新できる

実際に書いてみる

さっそくBLoCを書いてみましょう。今回はProviderパッケージのみを使い、Stream/SinkはDart標準のものを使うことにします(Stream/SinkはRxDartにも置き換えられますが、それはまた次の記事で扱います)。

今回作るのはこんな感じのアプリです。右下のボタンを押すと中央のテキストがカウントアップされていく、おなじみのアプリです

Simulator Screen Shot - iPhone 8 - 2019-07-15 at 03.24.00.png

Flutterデフォルトアプリのコード

Android StudioでFlutterのアプリを作成したら、以下のようなコードが生成されます。今回はこれをBLoCを使ったものに変更していきます。(ページの都合上、コメントを削除しています)

以下のような構成になっています。

  • MyApp
    • MyHomePage( StatefulWidget )
      • counter( int: 状態変数 )
      • Scaffold
        • Text
        • FloatingActionButton
main.dart
import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  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:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

元のコードの問題

  • ボタンを押すたびにsetState()でbuild()メソッドを呼び出しているため、ページ全体がリビルドされてしまい、非効率
  • カウンターの値を他のページや他のWidgetで使いたい時に不便

BLoCでこれらを解決していきます。

BLoCを使った構成

  • MyApp
    • MyHomePage( StatelessWidget )
      • counterBloc
      • Scaffold
        • StreamBuilder
          • Text
        • FloatingActionButton

BLoC

  • _actionControllerでボタンによるカウントアップ入力を受け付けます
  • _actionControllerに流れてきた値を使って、_countControllerにカウントアップした値を流します。
    • わざわざ二つStreamControllerを使っているのは、型が異なるためです
counter_bloc.dart
import 'dart:async';

class CounterBloc {
  final _actionController = StreamController<void>();
  Sink<void> get increment => _actionController.sink;

  final _countController = StreamController<int>();
  Stream<int> get count => _countController.stream;

  int _count = 0;

  CounterBloc() {
    _actionController.stream.listen((_) {
      _count++;
      _countController.sink.add(_count);
    });
  }

  void dispose() {
    _actionController.close();
    _countController.close();
  }
}

main.dart / Provider

main.dart
import 'package:counter_bloc/counter_bloc.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: Provider<CounterBloc>(
          builder: (context) => CounterBloc(),
          dispose: (context, bloc) => bloc.dispose(),
          child: MyHomePage(title: 'Flutter Demo Home Page'),
        ));
  }
}

class MyHomePage extends StatelessWidget {
  MyHomePage({this.title});

  final String title;

  @override
  Widget build(BuildContext context) {
    final counterBloc = Provider.of<CounterBloc>(context);
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            StreamBuilder(
              initialData: 0,
              stream: counterBloc.count,
              builder: (context, snapshot) {
                return Text(
                  '${snapshot.data}',
                  style: Theme.of(context).textTheme.display1,
                );
              },
            )
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          counterBloc.increment.add(null);
        },
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

main.dartの細かい説明は以下でします。

Provider<T> の利用

今回はあまり特に大きな意味はありませんが、MyApp()コンストラクタ内でProviderパッケージのProvider<T>を使っています。

これを使うことで、childパラメータに指定したWidget以下全てのWidgetで、同じBLoCインスタンスにアクセスすることができます。

また、disposeパラメータを使って、Widgetとblocの生存期間を一緒にします。これをしないと必要ないblocがいつまでも残ってしまうことになります。

home: Provider<CounterBloc>(
  builder: (context) => CounterBloc(),
  dispose: (context, bloc) => bloc.dispose(),
  child: MyHomePage(title: 'Flutter Demo Home Page'),

BLoCは、MyHomePage(子Widget)のbuild()メソッドで呼ぶのが定番です。

  @override
  Widget build(BuildContext context) {
    final counterBloc = Provider.of<CounterBloc>(context);
    return Scaffold(

Sink<T>.add() でBLoCに値を送る

Sink<T>.add() ( この例では counterBloc.increment )を使って、カウントアップアクションをBLoCに送ります。

floatingActionButton: FloatingActionButton(
  onPressed: () {
    counterBloc.increment.add(null);
  },
  tooltip: 'Increment',
  child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.

StreamBuilder でBLoCの値を受け取る

StreamBuilderを使って、Streamの値を反映します。StreamBuilderを使うことで、build()メソッドを呼ぶことなくStreamの値に応じてこの箇所だけUIを更新することができます。
このStreamBuilderのおかげで、MyHomePage()ウィジェットはStatefulWidgetからStatelessWidgetに置き換えることができていることに気付いたでしょうか。

StreamBuilder(
  initialData: 0,
  stream: counterBloc.count,
  builder: (context, snapshot) {
    return Text(
      '${snapshot.data}',
      style: Theme.of(context).textTheme.display1,
    );
  },

終わりに

ここまでで、なんとなくBLoCがどういったものか、どういうときに使えるのか理解の助けとなっていれば嬉しいです。

次回は、APIを使った処理のサンプルを紹介しながら、Stream/Sinkについてより深くご説明する予定です。

続き

Stream/Sinkを使いこなす! Stream/RxDart初心者のためのBLoC入門 part2
https://qiita.com/tetsufe/items/7b2f8592f5161104d1cd

参考

BLoCパターンとはなにか - FlutterとAngularの間でModelのコードを再利用する実践を通じての考察
https://ntaoo.hatenablog.com/entry/2018/10/08/072933

FlutterのBLoC(Business Logic Component)のライフサイクルを正確に管理して提供するbloc_providerパッケージの解説
https://medium.com/flutter-jp/bloc-provider-70e869b11b2f

FlutterのBLoCパターンについて得た知見
https://sudame.hatenablog.com/entry/2019/04/24/235702

providerパッケージ
https://pub.dev/packages/provider

[Flutter] package:provider の各プロバイダの詳細
https://qiita.com/kabochapo/items/a90d8438243c27e2f6d9

Flutterで有名なBLoCを使ってカウントアップアプリを作る
https://www.shogogeek.com/entry/2018/09/13/073303

在 Flutter 中使用 Bloc 来处理数据并更新 UI
https://zhuanlan.zhihu.com/p/55842307

tetsufe
Flutter、Djangoなどを使ってWeb開発やってます。「ホクマ」「スタマチ」など https://hufurima.com サツドラでFlutterアプリ開発のバイトを1年ほどしていました。
https://twitter.com/tetsufe_
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした