Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Flutter のリアクティブ戦略 set state 〜 redux まで

5,915 views

Published on

Build reactive mobile apps with Flutter (Google I/O '18) セッションのまとめです。

Flutter Meetup Tokyo #2 (LT資料)
https://flutter-jp.connpass.com/event/86352/

Build reactive mobile apps with Flutter (Google I/O '18)
https://www.youtube.com/watch?v=RS36gBEp8OI

Published in: Technology
  • Hello! I do no use writing service very often, only when I really have problems. But this one, I like best of all. The team of writers operates very quickly. It's called ⇒ www.HelpWriting.net ⇐ Hope this helps!
       Reply 
    Are you sure you want to  Yes  No
    Your message goes here
  • Sex in your area is here: ♥♥♥ http://bit.ly/39sFWPG ♥♥♥
       Reply 
    Are you sure you want to  Yes  No
    Your message goes here
  • Dating direct: ❶❶❶ http://bit.ly/39sFWPG ❶❶❶
       Reply 
    Are you sure you want to  Yes  No
    Your message goes here

Flutter のリアクティブ戦略 set state 〜 redux まで

  1. 1. Flutter のリアクティブ戦略 setState 〜 Redux まで Build reactive mobile apps with Flutter Google I/O '18 セッション 概要 
  2. 2. 自己紹介 名前 robo (兼高理恵) 好きなもの モバイル端末 おしごと アプリの設計から実装まで 2
  3. 3. [Session] Build reactive mobile apps with Flutter 2018/05/10 (Thu) 10:30AM - 11:30AM Stage 3 https://events.google.com/io/schedule/?section=may-8&sid=dab2bf45-6e44-4605-a997-9d446f95ef38 Build reactive mobile apps with Flutter (Google I/O '18) https://www.youtube.com/watch?v=RS36gBEp8OI 3 この LT は、Google I/O 2018 セッション Build reactive mobile apps with Flutter の概要です。 セッション中のスライドを利用していますが、   構成都合のためにビデオ中での説明からの意訳や  独自の説明に置き換えている点に御留意ください。
  4. 4. ビデオではコードの流れが読みにくいため Fillips さんのサンプル・プロジェクトも併せて御確認ください。 filiph/state_experiments https://github.com/filiph/state_experiments 4
  5. 5. 5 filiph/state_experiments https://github.com/filiph/state_experiments state_experiments リポジトリについて セッション講演者 Fillips さんの補助資料です。 基本的な setState() による再描画や、 Stream をつかったデータ取扱による リアクエティブなイベント反応方法まで 設計パターンごとの具体的実装が 確認できます。
  6. 6. state_experiments リポジトリの使い方 利用方法 下記コマンドでリポジトリをクローンして、 クローン先の shared ディレクトリ(プロジェクト)を IntelliJ IDE で開いてください。 設計パターンごとのアプリをビルドしたい場合は、 lib/main.dart の main 関数の設計種別を表す flavor の enum 値の書き換えが必要です。 例) final flavor = Archtecture.redux; ⇒ final flavore = Archtecture.bloc; 6 $ cd サンプルディレクトリ $ git clone https://github.com/filiph/state_experiments.git
  7. 7. 【補足事項】 ● Android アプリをビルドする場合は、 gradle-wrapper.jar を追加してください。 プロジェクトの android/gradle/wrapper には、gradle-wrapper.jar が含まれていません。 このため適当なプロジェクトを作成して、そこから gradle-wrapper.jar をコピーしてください。 ● サンプルプロジェクトは、 master channel のSDKでビルドしてください。 自分の環境の確認や master への切替は、下記を御参照ください。 7 # 現在どの channel が選択されているかチェック $ flutter channel Flutter channels: beta dev * master # channel を master に切り替え $ flutter channel master # channel の確認&SDKのアップデート $ flutter doctor
  8. 8. セッションの構成 8
  9. 9. セッションの構成 1. Flutter & state Flutter と State の基礎 2. State & the widget tree ウィジェットの State アクセス方法 3. Reactive with streams ストリームを使った State のエレガントな管理法 9 以上の3ステップから構成されています
  10. 10. 1. Flutter & state  Flutter と State の基礎 10
  11. 11. 11  Flutter の Hello World とも言える、  FAB タッチでカウントアップされるアプリでおなじみな、 最も基本的なイベント入力から出力を行う方法です。 
  12. 12. 12 class MyHomePage extends StatefulWidget { MyHomePage({Key key, this.title}) : super(key: key); final String title; @override _MyHomePageState createState() => new _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { int _counter = 0; void _increment() { setState(() { _counter += 1; }); } @override Widget build(BuildContext context) { return new Scaffold( body: …, children: <Widget>[ …, new Text( '$_counter', …, ), ], ), floatingActionButton: new Incrementer(_increment), ); } } class Incrementer extends StatefulWidget { final Function increment; Incrementer(this.increment); @override IncrementerState createState() { return new IncrementerState(); } } class IncrementerState extends State<Incrementer> { @override Widget build(BuildContext context) { return new FloatingActionButton( onPressed: widget.increment, …, ); } } 以降のスライドのコード中核は、 このような構成になっています。 詳しくは、state_experiments サンプルの hello_world ディレクトリ(プロジェクト)の lib/main.dart を参照ください。
  13. 13. 13 Flutter では、 全てのUIがウィジェットです。 このようなウィジェットツリーで…
  14. 14. 14 ここ(*1) で State (カウンター状態) を 必要とします。 (*1) MyHomePage の Text ウィジェットと Incrementer の FloatingActionButton ウィジェットの onPressed
  15. 15. 15 State (カウンター状態) は、 ここ(*1) にあります。 (*1) MyHomePage ウィジェットの _MyHomePageState
  16. 16. 16 よって親ウィジェットの State (カウンター状態) は、 子ウィジェットから参照可能にしな ければなりません。 そのためには、 クロージャを使って参照 (*1) させたり、 StatefulWidget のコンストラクタ引数に State を渡してあげる(*2) 必要があります。 (*1) MyHomePage の Text ウィジェット (*2) Incrementer ウィジェット
  17. 17. 17 サンプルでは、 State のカウンタ値を +1 して setState()で再描画を行わせる関数(*1) を コンストラクタに渡しています。 (*1) _MyHomePageState の _increment 親ウィジェットの関数をコールするための 古典的なコールバックメソッド 呼び出した先でなく、 setState() の所有ウィジェット(*2) が 再描画されることに注意してください (*2) MyHomePage ウィジェット この手法では、State を伝播させる経路の 全ウィジェットで State を扱わせてしまいます。 MyHomePageから FAB も Text も再描画 されてしまいます。
  18. 18. 課題点 この手法は、State を伝播させる経路の 全ウィジェットで State を扱わせてしまい、 再利用や関心事の分離を壊し、不要なUI更新も招くため、 以下の課題を抱えています。 ● 必要なウィジェットだけに State をアクセスさせること。 ● 再描画が必要なウィジェットのみを再描画させること。 18
  19. 19. 19 小さなアプリでは簡易ですが、        大きなアプリでは複雑になるので、           評価はこうなります。
  20. 20. 2. State & the widget tree   ウィジェットの State アクセス方法 20
  21. 21. 21 経路途中のウィジェット に State/状態 を扱わせず、 参照するもののみに State/状態 を公開する方法として、      InheritedWidget と ScopedModel を紹介します。
  22. 22. 22 Flutter の InheritedWidget クラスを継承したウィジェットを作れば、  State/状態 の保持とウィジェットツリー下流への伝播および、   State/状態 の参照と更新も実現できます。 InheritedWidget https://docs.flutter.io/flutter/widgets/InheritedWidget-class.html
  23. 23. 23 このウィジェットを State (状態)を保持する InheritedWidget を継承した ウィジェットに置き換えます。 参照側のウィジェットでは、 State を返すスタティックメソッド of(context) を使って、 ビルドコンテキストから、 直接 State (状態)にアクセスします。 InheritedWidget の具体的な使い方は、 state_experiments サンプル shared ディレクトリ(プロジェクト)の lib/src/bloc_start を参照ください。
  24. 24. 24 状態提供ウィジェット・クラスの定義 InheritedWidget を継承し、状態(state)プロパティを持った、 状態提供ウィジェット・クラス (MyInheritedWidget) を定義する 状態提供ウィジェットの生成と、状態の初期値設定 状態提供ウィジェットツリーの生成と状態の設定を ラップするウィジェットの build() メソッドで行います 状態参照ウィジェットでの状態の参照方法 状態提供ウィジェットのスタティックメソッドと引数の BuildContext から 状態提供ウィジェットの参照を取得して、状態プロパティにアクセスしています InheritedWidget を利用する実装手順
  25. 25. 課題点 InheritedWidget クラスを 継承させたウィジェットであれば、 保持している State をウィジェットツリーの下流で BuildContext を介して参照や更新することができます。 ● ですが State の参照や更新ができても、          効率的な再描画はもっていません。 25
  26. 26. 26 評価はこうなります。
  27. 27. 27 InheritedWidget を包含して拡張した、 外部パッケージ(*1) の ScopedModel クラスを使えば、 State の参照や更新と、ツリーの一部の効率的な再描画もできます。 (*1) scoped_model 0.2.0 https://pub.dartlang.org/packages/scoped_model
  28. 28. 28 ScopedModel を利用する実装手順 ScopedModel は、 InheritedWidget と同じように利用できます。 State へのアクセスには、 ScopedModelDescendant を使うところが異なります。
  29. 29. 29 scoped_model パッケージ 親ウィジェットから下流に データモデルを簡単に渡すことを 可能にする一連のユーティリティ。 モデルの更新時に、 モデルを使用する全ての 子ウィジェットの再描画も行えます。 Example のコードが セッション内容の参考になると思います。 scoped_model 0.2.0 https://pub.dartlang.org/packages/scoped_model ScopedModel ScopedModelDescendant ソースコード先 brianegan/scoped_model scoped_model/lib/scoped_model.dart https://github.com/brianegan/scoped_model/ blob/master/lib/scoped_model.dart
  30. 30. これから先は、状態の伝播を少し複雑にして説明するため、 ショッピングカートを模したアプリを使った、 State/状態 ハンドリングの説明になります。 30
  31. 31. 31 ショッピングカートアプリの特徴 ● State/状態を処理するウィジェットは3つ ● 全てのウィジェットが StatelessWidget です
  32. 32. 32 State/カート状態 をハンドルするウィジェットたち 1. 四角い Product グリッド をタッチすると、 カートにタッチした 商品 が入ります。 2. 画面右上の Cart Button には、 カートに入った 商品 数が表示されます。 3. Cart Button をタッチすると Cart Page に カートに入れた 商品リストを表示します。
  33. 33. 商品リスト/Cart Page は、 カートに何も入っていなければ、Empty を表示 33 0
  34. 34. 商品リスト/Cart Page は、 カートに商品が入っていればリストアップします。 34 3
  35. 35. 35 Product グリッドは、 タッチされるとカートに 商品を追加します。 Cart Button は、 カートに入った 商品総数を表示します Cart Page は、 Cart Button がタッチされると カートに入っている商品リストを表示します。 ショッピングカートのウィジェットツリー
  36. 36. ショッピングカート・アプリでの ScopedModel を使った State アクセス 36 このステップのコードは、 state_experiments サンプル shared ディレクトリ(プロジェクト)の  lib/src/scoped のソースを参照ください。
  37. 37. state_experiments サンプルの lib/src/scoped より抜粋 37 void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return ScopedModel<CartModel>( model: CartModel(), child: MaterialApp( …, home: CatalogHomePage(), routes: <String, WidgetBuilder>{ CartPage.routeName: (context) => CartPage(), }, …, ), ); } // 継承元の Model は、 // scoped_model パッケージの抽象クラスです。 class CartModel extends Model { final _cart = Cart(); List<CartItem> get items => _cart.items; int get itemCount => _cart.itemCount; void add(Product product) { _cart.add(product); notifyListeners(); // 全参照ウィジェットを再描画 } } カートへの商品追加時に、 カート状態を参照する全ウィジェット (ScopedModelDescendant のウィジェット)を再 描画させます。
  38. 38. 38 class CartPage extends StatelessWidget { static const routeName = '/cart'; @override Widget build(BuildContext context) { return Scaffold( …, body: ScopedModelDescendant<CartModel>( builder: (context, _, model) { if (model == null || model.items.isEmpty) { return Center( child: Text('Empty', style: Theme.of(context) .textTheme.display1), ); } return ListView( children: model.items.map((item) => ItemTile(item: item)).toList()); }), ); } } class CatalogHomePage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( …, actions: <Widget>[ ScopedModelDescendant<CartModel>( builder: (context, child, model) => CartButton( itemCount: model.itemCount, onPressed: () { Navigator.of(context).pushNamed( CartPage.routeName); }, ), ) ], ), body: ProductGrid(), ); } }
  39. 39. 39 class ProductGrid extends StatelessWidget { @override Widget build(BuildContext context) { return GridView.count( crossAxisCount: 2, children: catalog.products.map((product) { return ScopedModelDescendant<CartModel>( rebuildOnChange: false, builder: (context, child, model) => ProductSquare( product: product, onTap: () => model.add(product), ), ); }).toList(), ); } } ● ProductSquare (商品グリッド)は、 初期表示から変わらないので、 再描画する必要はありませんが … タッチされたときに カートに商品を追加するため、 CartModel を参照しているので、 ScopedModel の再描画対象になります。 ● このため rebuildOnChange フラグを false にして、 CartModel#add での notifyListeners() で、 再描画されないようにする必要があります。
  40. 40. ScopedModel サンプル実装のポイント 1. 状態モデル、 状態提供ウィジェット、状態参照ウィジェットの定義 2. 状態モデルの参照 / 状態変更 3. 全ての状態参照ウィジェットの再描画 4. 状態参照ウィジェットでの再描画の抑止 5. 補足 状態提供ウィジェットは、状態参照よりもウィジェットツリー上流にある必要があります。 ウィジェットは、StatelessWidget で構いません。(StatefulWidget である必要はありません ) 40
  41. 41. 状態モデル、状態提供ウィジェット、状態参照ウィジェットの定義 ● 状態モデル 取り扱いたい State/状態 を提供する scoped_model パッケージの Model を継承したクラス ● 状態提供ウィジェット build() メソッドの返値として、 あるいは返値の Widget 型のプロパティに対して、 ScopedModel<状態モデル> を割り当てる ウィジェット ● 状態参照ウィジェット build() メソッドの返値として、 あるいは返値の Widget 型のプロパティに対して、 ScopedModelDescendant<状態モデル> を割り当てる ウィジェット 41 CartModel クラスを参照 MyApp クラスを参照 CatalogHomePage, CartPage, ProductGrid クラスを参照
  42. 42. 状態モデルの参照 / 状態変更 ● 状態モデルの参照 / 状態変更 状態参照ウィジェットの builder() メソッド引数の model に 状態モデル への参照が割り当てられるので、model 引数を介して、 必要なプロパティの参照やメソッドの実行が行えます。 サンプルではウィジェット構築時やタッチイベントで使っています。 CatalogHomePage, CartPage, ProductGrid クラスを参照   42
  43. 43. 再描画 / 再描画抑止 ● 全ての状態参照ウィジェットの再描画 状態モデルの再描画を行いたいイベントのハンドル先で、 notifyListeners() メソッドを実行させます。 ProductSquare:onTap, CardModel#add(Product) 参照   ● ウィジェット再描画抑止 ウィジェットの再描画の必要のない状態参照ウィジェットの ScopedModelDescendant のプロパティ rebuildOnChange に false を 設定します。 ProductGrid#build() 参照   43
  44. 44. 課題点 ScopedModel クラスを使えば、 State をウィジェットツリーの下流で参照&更新できます。 State を参照する全ウィジェットの再描画もできますが、 以下の課題を抱えています。 ● State の特定プロパティへの更新を検出(対処)できない。 ● 再描画させたくないウィジェットには、 その特定と再描画抑止フラグの設定が必要である。 44
  45. 45. 45 評価はこうなります。
  46. 46. 3. Reactive with streams ストリームを使った     State のエレガントな管理法   46
  47. 47. 47 Stream と Obsevables の概念は密接に関係していて同じように扱えます。
  48. 48. 48 アプリケーション開発における、 すべての対話(データ入出力)は、ストリーム(data-flow/データの流れ)です。 ユーザ入力、システムイベント、 Web API のようなネットワークを介した外部との対話は、 非同期イベントのストリームです。 data-flow どこからどこにデータが渡され その過程でデータの扱いがどの ように変わるかの概念
  49. 49. 49 重要なことは、 UI更新の全てが非同期イベントのストリームであることです。 UIプログラミングが、 非同期イベントのストリームを管理することには意味があります。
  50. 50. 50 Dart言語は、昔からストリームをサポートしています。
  51. 51. 51 Dart言語のストリームには、 ストリーム変換、マッピング、折り畳みなど有用なメソッドがあります。
  52. 52. 52 Dart言語には、 非同期ジェネレータのようなキーワード async や await for や yield があり、 言語レベルでストリームをサポートしています。
  53. 53. 53 Dart言語において、ストリームは概念として広く使われています。
  54. 54. 54 そして Reactive Extensions (Rx / ReactiveX) と    Reactive Programming の概念も持っています。
  55. 55. 55 rxdart と呼ばれるパッケージがこれらを基にして作られています。 これはストリームの上に Reactive Extensions (Rx) を築き上げます。
  56. 56. 56 Dart 言語のストリームをウィジェットに適用させるため、 Flutterには、StreamBuilderというウィジェットがあります。
  57. 57. 57 StreamBuilder は、 データ入力用ストリームと builder() メソッドを持ち、 ストリームにデータが流れこむたびに再描画させることができます。
  58. 58. 58 このようなウィジェットツリーを持つアプリがあるとします。 アプリケーション構築のための アーキテクチャパターンを紹介するため…
  59. 59. 59 ユーザー入力のウィジェットをいくつか持っているので、 ユーザーからの非同期イベントのストリームも持っています。
  60. 60. 60 そして、ウィジェットツリー内の他のウィジェットで、 状態が変わるたびに再描画を試みるとします。
  61. 61. 61 単に両者を結びつけるだけでは不十分ですから、 必要な処置を行うビジネスロジックを設けます。
  62. 62. 62 入力を持つウィジェットからのイベントは、 ビジネスロジックの StreamController#Sink<Event>.add() に通知します。 あらかじめ StreamCntroller#Stream.listen() では、 対応する処理(入力イベントから最新データを作製する)を行うイベントハンドラを登録しておきます。
  63. 63. 63 ビジネスロジックでは、 登録済みのイベントハンドラで入力イベントから最新データを作成/更新し、 出力先のウィジェットでデータの変更を監視している StreamBuilder#Stream<Data> プロパティが反応できるようにします。 出力先ウィジェットの StreamBuilder では、 引数で渡された最新データを反映させる UI 構築ハンドラを builder() メソッドに定義しています。
  64. 64. 64 こうすることで出力先のウィジェットのUIを最新の内容で再描画させます。 出力先ウィジェットの StreamBuilder#stream プロパティ更新により、 StreamBuilder#builder() の UI 構築ハンドラが実行され、再描画されます。
  65. 65. 65 重要なのは、 ビジネスロジックに任意の側面ごとにストリームを公開することと、 出力ウィジェットが自分の関心のある側面のストリームのみ購読することです。 こうすることで、自分の関心のある側面が変更された場合のみ再描画させます。 側面 ⇒ State の構成要素 例)カートに入れられた アイテム総数やアイテムのリスト
  66. 66. 66 ビジネスロジックには、入力ストリーム と 出力ストリーム があります。 ストリーム ⇒ data-flow / データの流れ
  67. 67. 67 この入力と出力のストリームは、Dart言語でどのように実装するのでしょう。
  68. 68. 68 先ずは、入力イベントをハンドルする Sink オブジェクトを設定します。
  69. 69. 69 次に、データ出力の Streamオブジェクト を設定します。
  70. 70. 70 ショッピングカート・アプリの例では、 カートに、商品(Product)を追加する addition という入力と、 アイテム数の変化ごとに更新される itemCount という出力になります。 実際の実装で利用する、 StrteamController#Sink<Event>、 StreamBuilder#Stream<Data> の 説明が省かれていることに注意!
  71. 71. 71 入力と出力の対応により、ビジネスロジックのコンポーネントができました。
  72. 72. 72 Google 内部では、このようなビジネスロジックのコンポーネントを…
  73. 73. 73 bloc (ブロック)と略称しています。
  74. 74. 1. Sink で Widget の入力イベントを取得し 2. Stream に最新データの出力を行い 3. Widget を最新化(再描画)するような、 4. (ある責務をまとめた)任意の実装単位 ⇒       ビジネスロジックのコンポーネント              Business Logic Component を   BLoC (Business Logic Component) と呼んでいるそうです。 Build reactive mobile apps with Flutter (Google I/O '18) https://youtu.be/RS36gBEp8OI?t=1387 (22:00頃) 74
  75. 75.  Flutter での BLoC パターンやその思想については、  Dart Meetup Tokyo 管理者の laco 氏のブログ記事が 参考になります。  FlutterのBLoCパターンをAngularで理解する  https://lacolaco.hatenablog.com/entry/2018/05/22/194805 Dart Meetup Tokyo https://dartisans-jp.connpass.com/ 75
  76. 76. ショッピングカート・アプリでの Stream と Sink を使ったストリーム処理 76 このステップのコードは、 state_experiments サンプル shared ディレクトリ(プロジェクト)の  lib/src/bloc のソースを参照ください。
  77. 77. state_experiments サンプルの lib/src/bloc より抜粋 77 void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return CartProvider( child: MaterialApp( …, home: MyHomePage(), routes: <String, WidgetBuilder>{ BlocCartPage.routeName: (context) => BlocCartPage() }, ), ); } } class CartProvider extends InheritedWidget { final CartBloc cartBloc; CartProvider({ Key key, CartBloc cartBloc, Widget child, }) : cartBloc = cartBloc ?? CartBloc(), super(key: key, child: child); @override bool updateShouldNotify(InheritedWidget oldWidget) => true; static CartBloc of(BuildContext context) => (context.inheritFromWidgetOfExactType(CartProvider) as CartProvider).cartBloc; } カート状態 CartBloc (business logic含) は InheritedWidget を継承させた CartProvider の of() メソッドを介して、 ウィジェットツリー下流から 取得可能になっています。
  78. 78. 78 import 'package:rxdart/subjects.dart'; class CartAddition { final Product product; final int count; CartAddition(this.product, [this.count = 1]); } class CartBloc { final Cart _cart = Cart(); final BehaviorSubject<List<CartItem>> _items = BehaviorSubject<List<CartItem>>(seedValue: []); final BehaviorSubject<int> _itemCount = BehaviorSubject<int>(seedValue: 0); final StreamController<CartAddition> _cartAdditionController = StreamController<CartAddition>(); …右に続く CartBloc() { _cartAdditionController.stream.listen((addition) { int currentCount = _cart.itemCount; _cart.add(addition.product, addition.count); _items.add(_cart.items); int updatedCount = _cart.itemCount; if (updatedCount != currentCount) { _itemCount.add(updatedCount); } }); } Sink<CartAddition> get cartAddition => _cartAdditionController.sink; Stream<int> get itemCount => _itemCount.stream; Stream<List<CartItem>> get items => _items.stream; void dispose() { … } }
  79. 79. 79 class MyHomePage extends StatelessWidget { @override Widget build(BuildContext context) { final cartBloc = CartProvider.of(context); return Scaffold( appBar: AppBar( title: 'Bloc', actions: <Widget>[ StreamBuilder<int>( stream: cartBloc.itemCount, initialData: 0, builder: (context, snapshot) => CartButton( itemCount: snapshot.data, onPressed: () { … }, ), ) ], ), body: ProductGrid(), ); } } class ProductGrid extends StatelessWidget { @override Widget build(BuildContext context) { final cartBloc = CartProvider.of(context); return GridView.count( crossAxisCount: 2, children: catalog.products.map((product) { return ProductSquare( product: product, onTap: () { cartBloc.cartAddition .add(CartAddition(product)); }, ); }).toList(), ); } }
  80. 80. class BlocCartPage extends StatelessWidget { BlocCartPage(); static const routeName = "/cart"; @override Widget 80 class BlocCartPage extends StatelessWidget { BlocCartPage(); static const routeName = "/cart"; @override Widget build(BuildContext context) { final cart = CartProvider.of(context); return Scaffold( appBar: AppBar( title: Text("Your Cart"), ), body: StreamBuilder<List<CartItem>>( stream: cart.items, builder: (context, snapshot) { if (snapshot.data == null || snapshot.data.isEmpty) { return Center( child: Text('Empty', style:Theme.of(context) .textTheme.display1)); } …右に続く return ListView( children: snapshot.data .map((item) => ItemTile(item: item)) .toList()); })); } } Sink と Stream オブジェクトに関連する クラスとフィールドとメソッドのみ、 型ごとに色を分けています。
  81. 81. ショッピングカート・アプリ例の 入力イベント ~ UI 最新化までの処理フローについては、 スライド 62 ~ 65 を参考に、lib/src/bloc より抜粋した コードを御参照ください。 81 Productグリッドのタッチ   ⇒ CartへのProduct追加   ⇒ CartButtonアイテム総数UI の再描画のフローなら、 ・ProuctGrid 内の onTap ハンドラ、 ・CartBloc の StreamController _cartAdditionCotroller や  _cartAdittionController.stream.listen() に_cartAdditionController.sink、 ・MyHomePage での StreamBuilder<int>() 辺りが参考になると思います。
  82. 82. ビジネスロジック コンポーネントでの ポイント ● 入力〜出力のフローの始端と終端のみ公開し、依存の分離を高めます。 特定の入力イベント受付となる Sink<Event>オブジェクト と、 特定の出力データ窓口となる Stream<Data>オブジェクト のみ公開します。 入力ウィジェットからのイベントの通知やハンドル方法や、 出力ウィジェットへの最新データによる UI 最新化通知は、隠蔽します。 ● bloc では、特定の実装方法はありません。 サンプルでは、StreamController<Event>とStreamBuilder<Data>を使って、 入力イベントのハンドルと出力データによるUI 最新化を行っていますが、 bloc では、イベント入力とデータ出力のインターフェースが、 Sink<Event> と Stream<Data> であればよく、 特定の実装方法はないそうです。 82
  83. 83. StreamController を使った 入力イベントの通知とハンドルのポイント ● StreamController<Event> Event 入力をハンドルして必要な処理を行わせるコントローラの定義 stream:Stream<Event> と sink:Sink<Event> プロパティを所有しています。 ● StreamController#stream.listen((Event){ …対応処理… }) Event対応ハンドラの登録 ハンドラでは、最新のEvent情報に依存する各種 Data の更新を行う。 ● StreamController#sink.add(最新Event) Event最新情報の入力イベントの通知 最新Event を引数に与えて、Event対応ハンドラを実行させるメソッド。 83
  84. 84. StreamBuilder を使った 最新データによる UI 最新化通知のポイント ● StreamBuilder<Data> 最新Data をUI に反映(Data と UI を同期)させる StreamBuilder の定義 StreamBuilder は、stream:Stream<Data> プロパティを所有しています。 ● StreamBuilder#builder((BuildContext, AsyncSnapshot<Data>){ …UI構築… }) UI構築ハンドラの登録 引数の最新Data スナップショットから、最新Dataを反映した UI の構築を行う。 ● StreamBuilde#stream Dataの変更監視プロパティ プロパティ stream の監視先が最新Dataに更新されると、 最新Dataを引数に与えて、UI構築ハンドラが実行されます。 84
  85. 85. StreamBuilder<T> class https://docs.flutter.io/flutter/widgets/StreamBuilder-class.html プロパティとして stream:Stream<T> と_snapshot:AsyncSnapshot<T> を持ち、 ストリームのデータが変更されると、最新のスナップショットに基づいて ウィジェットを再構築させるクラス。 85
  86. 86. StreamController<T> class https://docs.flutter.io/flutter/dart-async/StreamController-class.html ストリームを制御するコントローラ・クラス プロパティとして stream:Stream<T> と sink:StreamSink<T> を持ち、 stream へのリスナー関数の登録と、sink へのデータ追加(更新)により、 データ追加(更新)やエラー発生のイベントをストリームに プッシュすることができます。 またストリームが一時停止中かサブスクライバを持つか否かの確認や、 いずれかがが変更時にコールバックを取得できます。 86
  87. 87. Stream<T> class https://docs.flutter.io/flutter/dart-async/Stream-class.html 非同期イベントのデータを扱える(連鎖的に処理できる)ようにするクラス。 非同期に提供される/変化するデータを扱うため、 そのイベントごとのデータ処置/変換を行う関数を 宣言的に定義できるようにします。 イベント対応の手法としては、 await for キーワードによるイベント契機の待機指定や、 listen(関数)によるコールバック関数のリスナ登録が利用できます。 87
  88. 88. StreamSink<T> class https://docs.flutter.io/flutter/dart-async/StreamSink-class.html 同期的あるいは非同期的なデータの追加/変更のイベント契機の受付となり、 そのデータをストリームに伝える抽象クラス。 Sink<T> class https://docs.flutter.io/flutter/dart-core/Sink-class.html データの追加/変更のイベント契機の受付となる抽象クラス。 (データ値の直接受け取りだけでなく、取得 /生成する処置関数も受け取ることもできます。) 88
  89. 89. BehaviorSubject<T> class https://github.com/ReactiveX/rxdart/blob/master/lib/src/subjects/behavior_subject.dart rxdart パッケージの特殊な StreamController クラス 追加/変更されるデータをObservable(監視対象)とする Stream を提供するようです。 注)ドキュメントが見つからなかったため詳細は不明です。 89
  90. 90. 90 rxdart 0.16.7 https://pub.dartlang.org/packages/rxdart rxdart パッケージ RxDart は ReactiveX をベースにした Dart 言語用のリアクティブ関数プログラミング ライブラリです。 RxDartは、Dart の Streams API を置き換える ものではなく、それを基にして機能を追加します。 rxdart リポジトリ ReactiveX/rxdart https://github.com/ReactiveX/rxdart
  91. 91. これから先は、  ビジネスロジック コンポーネント (bloc) を使って、   入力イベントと出力データを   役割ごとに別々に分離できるようになったことで、        どんなことが便利になるのかを紹介します。 91
  92. 92. 92 イベント入力〜データ出力を一連のストリームに分離することで、 Flutter UI は、コンポーネントで何をしているのか                  気にする必要がなくなりました。
  93. 93. 93 さらに bloc にしたことで、イベント入力に対応する 別のデータ出力 Stream を追加することだってできます。 カートの総コストやアイテムのリストは、 更新されるたびに UIも最新化したいでしょうから、 総コスト totalCost や アイテムのリスト items も追加できます。
  94. 94. 94 でも、総コスト totalCost を 整数データのストリームにすることには問題がありそうです。 たしかにコンポーネント内では意味を持ちますが…
  95. 95. 95 ウィジェット側では、 このように数値を文字列に変換しなくてはなりません、 これはビジネスロジックなので、ビューにあってはいけないでしょう。
  96. 96. 96 だからビジネスロジックコンポーネント CartBloc に戻って…
  97. 97. 97 総コスト totalCost を フォーマット済みの String の Stream に変更しましょう。 bloc ですから、出力データ型の変更だってできます。
  98. 98. 98 コンポーネント内でのロジックは、 先のウィジェットでの数値から文字列への変換のような 既にある同じビジネスロジックを再利用するだけですみます。
  99. 99. 99 新しい入力イベントを作ることだってできます。 ロケール locale という新しい入力イベント Sink<Locale> を追加すれば… ユーザは、 米国からEUの店舗に切り替えることだってできるでしょう。
  100. 100. 100 bloc ですから、locale の入力イベントから 出力データ totalCost にストリームを繋ぐことだってできます。 内部的には数値の変化がなくても、 totalCost の文字列を更新 ⇒ UI最新化 ⇒ができます。 totalCost の変更が何処で発生しようとも、対応できるのです。
  101. 101. 101 StreamController と StreamBuilder を使えば、 入力イベントの通知やハンドリング(最新データ作製)と、 出力データの監視とUI更新を実現することができます。 イベントとデータのストリーム化は、他の方法よりも有益です。
  102. 102. 改良点 ● カート状態(*1) は、InheritedWidget パターンを利用して ウィジェットツリーの下流でも取得可能にした。 ● カート状態を役割別の入力イベントと出力データの ストリームに小分けして細かな参照更新を可能にした。 ● ウィジェットは、UI更新に関係する出力データを 購読することで、その変更でのみ再描画可能になった。 (*1) CartBloc には、状態だけでなくビジネスロジックのコンポーネントも含む。 102
  103. 103. 103 評価はこうなります。
  104. 104. ビジネスロジックに イベントのハンドルや データ更新や監視およびUI更新の仕組みを移設して、 イベント入力〜データ出力をストリームにすることで、 ストリームを介してイベントの投入やデータの更新を行う エレガントなパターンができました。 104
  105. 105. その他の State管理の選択肢 105
  106. 106. ● StatefulWidget と setState() は、 浅いツリーかつ、アプリがシンプルであれば 問題ありません。 ● ScopedModelは、 モデルが比較的簡単で、 任意の深さのツリー上での状態更新に適しています。 ● reduxパターンが好きな人には、 コミュニティによって作られた優れた redux の実装… Dart言語用の redux パッケージと Flutter用の Flutter Redux があります。 106
  107. 107. 【補足】 Flutter Redux について セッションでの説明がないため詳細は不明ですが、 state_experiments サンプルには、 Flutter Redux を使ったサンプルも含まれています。 よろしければ、    state_experiments サンプル shared ディレクトリ(プロジェクト)の   lib/src/redux のソースも御参照ください。 107
  108. 108. 108 redux 3.0.0 https://pub.dartlang.org/packages/redux redux パッケージ flutter_reduxパッケージを使用すれば、 Flutterと組み合わせることができます。 Dart言語 redux リポジトリ johnpryan/redux.dart https://github.com/johnpryan/redux.dart
  109. 109. 109 flutter_redux 0.5.1 https://pub.dartlang.org/packages/flutter_redux flutter_redux パッケージ Flutterウィジェットを構築するために Reduxストアを簡単に使用できる ユーティリティのセット。 flutter_redux リポジトリ brianegan/flutter_redux https://github.com/brianegan/flutter_redux
  110. 110. 結論 110
  111. 111. Flutter のウィジェットを ストリームと組み合わせて使用することで、 データの流れを処理するリアクティブな方法が得られます。 データが変更されたときのみ、UIの更新を処理できます。 Dart言語の Streamsと rxdart パッケージを アプリの状態管理の選択肢とすることを強くお勧めします。 111
  112. 112. 112 Flutter のウィジェットを ストリームと組み合わせて使用することで、 データの流れを処理するリアクティブな方法が得られます。 データが変更されたときのみ、UIの更新を処理できます。 Dart言語の Streamsと rxdart パッケージを アプリの状態管理の選択肢とすることを強くお勧めします。
  113. 113. ご清聴、 ありがとうございました。 113
  114. 114. A Special thanks to Mr Fillip! [Meetup] Flutter developers 2018/05/10 (Thu) 12:10PM - 12:40PM Community lounge https://events.google.com/io/schedule/?section=may-9&sid=ba156a8e-3d16-4b29-9788-878f1f12ead9 114

×