完成デモ

GitHub にあげてあります => https://github.com/sensuikan1973/Flutter_RxDart_GetStarted
公式チュートリアル「GetStarted」に、「いいね件数の表示機能」を追加しただけ!
「BLoCパターン」と「RxDart」をテーマに実装しました。1
なお追加機能は単純ですがテーマのもと実装したので、チュートリアルとは全然違うコードになってます。
また、Dartには標準でStreamがある ため、RxDartの使用は部分的になっています。(RxDartの登場はここ)
備考
- BLoC + Rx で実装するために見るもの という記事に、前提となる知識を書いたのでよろしければそちらも。
- この記事は Google I/O '18 | Build reactive mobile apps with Flutter の発表直後に書いたものです。
- GitHub の Issue には書きましたが、Flutter や RxDart のアプデなどに追従するとより望ましい書き方ができます。(時間に余裕ができたら push しておきます...)
 
そもそもRxDartを使うモチベは?
**Google I/O '18 | Build reactive mobile apps with Flutter**を見ると、図とともに以下のような説明がある。
「UIプログラミングで状態の変更を伝えるにあたっては、DartStreamsとRxDartを使うのがオススメ」(意訳)

上記のGoogle I/Oの動画は、Flutterと状態管理(Rx等)をわかりやすく説明していて必見だと感じました。
このデモのソースコード、めちゃくちゃ参考にしました!
RxDart
導入方法
Dart Package | rxdartに記載のある通り。
導入したら、import 'package:rxdart/rxdart.dart';だけを追加してデバッグが成功することを確認しましょう。
なお、シミュレータを起動するのは以下のような手順。
[iOS]
open -a Simulator.app
[Android]
- Android Studioを起動し、Tools > AVD Managerで起動しておく。
- VSCodeからRunすると、それを認識してAndroidで起動してくれる
使い方
本記事では、WordBlocクラスでRxDartを使用しています。(具体的にはBehaviorSubject)
実践的なサンプルはここ等が参考になります。
余談ですが、READMEにコナミコマンドが登場してて面白いです。
実装
ディレクトリ構成は以下の通り。 今回関係あるのは、lib以下のみ。

Modelを定義
WordItem
文字列をnameとして持つだけのクラス
class WordItem {
  final String name;
  const WordItem(this.name);
  @override
  String toString() => "$name";
}
Word
今回はWordを表示するアプリであり、それをモデル化したもの
import 'package:sample/models/word_item.dart';
import 'dart:collection';
class Word {
  final List<WordItem> _items = <WordItem>[];
  Word();
  Word.clone(Word word) {
    _items.addAll(word._items);
  }
  int get itemCount => _items.length;
  // 変更不可なリスト
  UnmodifiableListView<WordItem> get items => UnmodifiableListView(_items);
  void add (String name) {
    _updateCountAdd(name);
  }
  void remove (String name) {
    _updateCountRemove(name);
  }
  @override
  String toString() => "$items";
  void _updateCountAdd(String name) {
    for (int i = 0; i < _items.length; i++) {
      final item = _items[i];
      // 既に追加済みなら無視
      if (name == item.name) { return; }
    }
    _items.add(WordItem(name));
  }
  void _updateCountRemove(String name) {
    for (int i = 0; i < _items.length; i++) {
      final item = _items[i];
      if (name == item.name) {
         _items.removeAt(i);
      }
    }
  }
}
Suggestion
今回のアプリは、ランダムな単語を生成する外部パッケージを使用しているわけだが、その生成による単語の候補をモデル化したもの。
WordPairはチュートリアルで説明がある通り、english_wordsパッケージのもの。
import 'dart:collection';
import 'package:english_words/english_words.dart';
final Suggestion suggestion = Suggestion();
class Suggestion {
  final List<WordPair> _suggestedWords = <WordPair>[]; // 生成された単語を保持
  Suggestion();
  Suggestion.clone(Suggestion suggestion) {
    _suggestedWords.addAll(suggestion._suggestedWords);
  }
  int get suggestionCount => _suggestedWords.length;
  UnmodifiableListView<WordPair> get suggestedWords => UnmodifiableListView(_suggestedWords);
  void add (WordPair wordPair) {
    _updateCount(wordPair);
  }
  // WordPairを一気に生成して、一気に追加するために定義
  void addMulti (List<WordPair> wordPairs) {
    for (int i = 0; i < wordPairs.length; ++i) {
      _updateCount(wordPairs[i]);
    }
  }
  @override
  String toString() => "$suggestedWords";
  void _updateCount(WordPair wordPair) {
    _suggestedWords.add(wordPair);
  }
}
Widgetを定義
CountLabel
いいね件数を表示するラベルを定義
import 'package:flutter/material.dart';
import 'package:meta/meta.dart';
class CountLabel extends StatefulWidget {
  final int favoriteCount; //表示したいカウント
  CountLabel({
    Key key,
    @required this.favoriteCount,
  })  : assert(favoriteCount >= 0),
        super(key:key);
  @override
  CountLabelState createState() {
    return CountLabelState();
  }
}
class CountLabelState extends State<CountLabel> {
  @override
  Widget build(BuildContext context) {
    return Text(
      widget.favoriteCount.toString() ,
      style: new TextStyle(
        fontWeight:  FontWeight.bold,
        fontSize: 35.0, //目立つようにでかくしてある
        color: Colors.pink),
      );
  }
}
FavoritePage
いいねした単語を閲覧するページのViewを定義
import 'package:flutter/material.dart';
import 'package:sample/models/word.dart';
class FavoritePage extends StatelessWidget {
  FavoritePage(this.word);
  final Word word;
  static const routeName = "/favorite";
  @override
  Widget build(BuildContext context) {
    final tiles = word.items.map(
      (item){
        return new ListTile(
          title: new Text(item.name)
        );
      }
    );
    final divided = ListTile
      .divideTiles(
        context: context,
        tiles: tiles,
      )
      .toList();
    return new Scaffold(
      appBar: AppBar(
        title: Text("Your Favorite")
      ),
      body: new ListView(children: divided),
    );
  }
}
Componentを定義
WordProvider
WordBlocのラッパーであり、WordBlocへの参照を持つ。
import 'package:flutter/widgets.dart';
import 'package:sample/word_bloc.dart';
class WordProvider extends InheritedWidget {
  final WordBloc wordBloc;
  WordProvider({
    Key key,
    WordBloc wordBloc,
    Widget child,
  })  : wordBloc = wordBloc ?? WordBloc(),
        super(key: key, child: child);
@override
bool updateShouldNotify(InheritedWidget oldWidget) => true;
// WordBlocへの参照を提供
static WordBloc of(BuildContext context) =>
  (context.inheritFromWidgetOfExactType(WordProvider) as WordProvider)
    .wordBloc;
}
WordBloc
今回の要
import 'dart:async';
import 'package:sample/models/word.dart';
import 'package:sample/models/word_item.dart';
import 'package:rxdart/subjects.dart';
class WordAddition {
  final String name;
  WordAddition(this.name);
}
class WordRemoval {
  final String name;
  WordRemoval(this.name);
}
class WordBloc {
  final Word _word = Word();
  final BehaviorSubject<List<WordItem>> _items =
      BehaviorSubject<List<WordItem>>(seedValue: []);
  final BehaviorSubject<int> _itemCount =
      BehaviorSubject<int>(seedValue: 0);
  final StreamController<WordAddition> _wordAdditionController =
      StreamController<WordAddition>();
  final StreamController<WordRemoval> _wordRemovalController =
  StreamController<WordRemoval>();
  WordBloc() {
    _wordAdditionController.stream.listen((addition){
      int currentCount = _word.itemCount;
      _word.add(addition.name);
      _items.add(_word.items);
      int updateCount = _word.itemCount;
      if (updateCount != currentCount) {
        _itemCount.add(updateCount);
      }
    });
    _wordRemovalController.stream.listen((removal){
      int currentCount = _word.itemCount;
      _word.remove(removal.name);
      print(_word.items.toString());
      _items.add(_word.items);
      int updateCount = _word.itemCount;
      if (updateCount != currentCount) {
        _itemCount.add(updateCount);
      }
    });
  }
  Sink<WordAddition> get wordAddition => _wordAdditionController.sink;
  Sink<WordRemoval> get wordRemoval => _wordRemovalController.sink;
  Stream<int> get itemCount => _itemCount.stream;
  Stream<List<WordItem>> get items => _items.stream;
  void dispose() {
    _items.close();
    _itemCount.close();
    _wordAdditionController.close();
    _wordRemovalController.close();
  }
}
BehaviorSubjectを使ってる
今回登場する、RxDartはココだけ。
BehaviorSubjectは、StreamControllerの一種。
役割としては、「最新の値が追加されたら、それを購読者に発行する」ことであり、最後に発行した値をキャッシュしている。
今回は、「いいねされた単語のリスト」と「その数」をBehaviorSubjectに追加することで、購読者にそれを知らせるようにしている。
BlocFavoritePage
いいねした単語の表示
import 'package:flutter/material.dart';
import 'package:sample/models/word_item.dart';
import 'package:sample/word_provider.dart';
class BlocFavoritePage extends StatelessWidget{
  BlocFavoritePage();
  static const routeName = "/favorite";
  @override
  Widget build(BuildContext context) {
    final word = WordProvider.of(context);
    return Scaffold(
      appBar: AppBar(
        title: Text("Your Favorite"),
      ),
      body: StreamBuilder<List<WordItem>>(
        stream: word.items,
        builder: (context, snapshot) {
          if (snapshot.data == null || snapshot.data.isEmpty) {
            return Center(child: Text('Empty'));
          }
          final tiles = snapshot.data.map(
            (item) {
              return new ListTile(
                title: new Text(item.name)
              );
            }
          );
          final divided = ListTile
            .divideTiles(
              context: context,
              tiles: tiles,
            )
            .toList();
          return new ListView(children: divided);
        },
      ),
    );
  }
}
StreamBuilderで、bodyを生成している。
stream: word.itemsとbuilder: (context, snapshot) {}の指定により、word.itemsというStreamに応じてViewを作っているということ。
Main_
import 'package:flutter/material.dart';
import 'package:english_words/english_words.dart';
import 'package:sample/bloc_favorite_page.dart';
import 'package:sample/models/word_item.dart';
import 'package:sample/word_bloc.dart';
import 'package:sample/word_provider.dart';
import 'package:sample/models/suggestion.dart';
import 'package:sample/widgets/count_label.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return WordProvider(
      child: MaterialApp(
        title: 'Startup Name Generator',
        theme: new ThemeData(primaryColor: Colors.white),
        home: RandomWordsHomePage(),
        routes: <String, WidgetBuilder> {
          // 「./favorite」に、BlocFavoritePageを登録
          BlocFavoritePage.routeName: (context) => BlocFavoritePage()
        },
      ),
    );
  }
}
class RandomWordsHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final wordBloc = WordProvider.of(context);
    return Scaffold(
      appBar: AppBar(
        title: Text("Startup Name Generator"),
        actions: <Widget>[
          StreamBuilder<int>(
            stream: wordBloc.itemCount,
            initialData: 0,
            builder: (context, snapshot) => CountLabel(
              favoriteCount: snapshot.data,
            )
          ),
          new IconButton(
            icon: const Icon(Icons.list),
            onPressed: (){
              Navigator.of(context).pushNamed(BlocFavoritePage.routeName);
            }
          )
        ],
      ),
      body: WordList(),
    );
  }
}
class WordList extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new ListView.builder(
      padding: const EdgeInsets.all(16.0),
      itemBuilder: (context, i) {
        if (i.isOdd) return new Divider();
        final index = i ~/ 2;
        if (index >= suggestion.suggestionCount) {
          const addNum = 10;
          suggestion.addMulti(generateWordPairs().take(addNum).toList());
        }
        return _buildRow(WordProvider.of(context), suggestion.suggestedWords[index]);
      }
    );
  }
}
// Favorite済み、すなわちword.itemsに含まれていれば、赤いハートアイコンにする
Widget _buildRow(WordBloc word, WordPair pair) {
  return new StreamBuilder<List<WordItem>>(
    stream: word.items,
    builder: (_, snapshot) {
      if (snapshot.data == null || snapshot.data.isEmpty) {
        return _createWordListTile(word, false, pair.asPascalCase);
      } else {
        final addedWord = snapshot.data.map(
          (item) {
            return item.name;
          }
        );
        final alreadyAdded = addedWord.toString().contains(pair.asPascalCase);
        return _createWordListTile(word, alreadyAdded, pair.asPascalCase);
      }
    }
  );
}
ListTile _createWordListTile(WordBloc word, bool isFavorited, String title) {
  return new ListTile(
    title: new Text(title),
    trailing: new Icon(
      isFavorited ? Icons.favorite : Icons.favorite_border,
      color: isFavorited ? Colors.red : null,
    ),
    onTap: () {
      if (isFavorited) {
        word.wordRemoval.add(WordRemoval(title));
      } else {
        word.wordAddition.add(WordAddition(title));
      }
    },
  );
}
【いいね件数】
StreamBuilder<int>で、wordBlocのitemCountに応じてCountLabelを生成するようにしている。
これで、いいねの追加に応じてCountLabelの表示も変えられる。
【いいね済みかどうかの表示】
StreamBuilder<List<WordItem>>で、word.itemsに応じてアイコンを生成するようにしている。
これで、いいね済みかどうかに合わせてアイコンを表示できる。
おわりに
Flutterにおける、状態管理「DartStreams + RxDart」をやってみました。
Flutter、慣れれば結構早く開発できそうな気がしたので、積極的に採用を検討していきたい
- 
参考「BLocについて」 
 ・ 公式の説明動画
 ・ FlutterのBLoCパターンをAngularで理解する
 ・ DartからきたBLoCの話 ↩