Edited at

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間で状態変数を受け渡しするためのものです。


イメージ図

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