Flutter で MVVM をやる

趣味で flutter を始めました。MVVMを書く時どうしたらいいのか調べてみました。 この記事を参考にやってみました。

App architecture: MVVM in Flutter using Dart Streams - QuickBird Studios Blog

他にもこのスライドが超役に立ちました。まだ使ってないことも書いてるので今度試します。

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

実現すること

  1. メニューを選択する。
  2. ダイアログから選ぶ
  3. 選んだデータがメニューのところに反映される。

みたいなことを実現したいと思います。

ViewModelの書き方

Flutter は標準で Stream があります。これを使うとreactive programing っぽい事ができます。※別途rxDartもありますがここでは紹介しません。

まずは ViewModel の Interface を定義します。Dartは暗黙的にInterfaceが定義されているので、abstract class で作ります。 データの源流をSink、データが流れるところをStreamと呼ぶようです。rxとはちょっと呼び名が違うので気をつけます。

ここではわかりやすく、選ばれた犬のサイズを受け取る dogSizeSink と、それが流れる dogSizeStream という名前で定義しました。

abstract class DogViewModel {
  Sink get dogSizeSink;
  Stream<DogSize> get dogSizeStream;
}

このインターフェースを実装したクラスを作ります。

class DogViewModelImpl implements DogViewModel {
  final _dogSizeController = StreamController<DogSize>.broadcast();

  @override
  void dispose() {
    _dogSizeController.close();
  }

  @override
  Sink get dogSizeSink => _dogSizeController;

  @override
  Stream<DogSize> get dogSizeStream => _dogSizeController.stream;
}

StreamController.broadcast() で stream を作ります。型はStreamControllerですが、rxでいうところのObservableみたいなのだと思います。 これ自体はSink型でもあるので、get dogSizeSink が呼ばれた場合はこれをそのまま返します。 _dogSizeController.stream とすれば、Sinkに追加したデータが流れるストリームオブジェクトを取得できます。 今回は使いませんが、stream.listen() を使えば、sink.add(hoge) されたデータを観測することができます。

ダイアログ選択などを使ってデータが決まったら、それをsinkへ追加します。

final dogSize = await showDialog<DogSize>(
  context: context,
  builder: (context) => DogSizeList(),
);
viewModel.dogSizeSink.add(dogSize);

sink.add するとデータは stream へと流れていきます。viewModel.dogSizeStreamを観測することで追加したデータを利用することができます。 前述したように stream.listen() を使ってlistenerを実装すれば、動かなくはないのですが、もっとシンプルに実装することができます。 streamをwidgetで利用する場合は、StreamBuilderを利用すると以下のように実装できます。

InkWell(
  child: StreamBuilder<DogSize>(
    stream: viewModel.dogSizeStream,
    builder: (context, snapshot) {
      if (!snapshot.hasData) return Text('サイズを選んでください');
      return Text(snapshot.data.label);
    },
  ),
  onTap: _selectDogSizeDialog,
),

このように、StreamBuilder() の引数に stream を直接指定することができます。 どこかで sink.add したら、builder: で指定したラムダが呼び出され、snapshot 引数にデータが入っています。

まとめ

こんな作り方で Flutter の ViewModel を実装することができます。Androidでrxやってたらすんなりやれると思います。rxJava, rxAndroidよりもスッキリ書けるのがとてもいいです。