MVVM + Flux?それ,Flutterなら簡単にできますよ
CAUTION
ここに書いてある方法は多分あんまりいい方法ではないので推奨しません.
普通にMVVMパターンをStateNotifierで実装するのが今の所いいと思います(StateNotifierのstateにstreamが標準実装されたのもあるので).
TL;DR
MVVMはネイティブとかだとRxとかを使ってViewModelのデータを管理する方式だった
Flutterではデータ更新の通知がViewに行くため,Modelの状態はViewが直接参照する形になる
つまりStreamで通知するなどの小細工をすること無く複数画面間のデータの共有が可能
MVVMのModelは,Flutter上ではFluxのStoreと解釈することができてしまう
ぶっちゃけModelなんて名前じゃなくてControllerとかでいい
アーキテクチャの変遷
アプリ開発ではよく耳にするアーキテクチャパターンのMVVM,そして後発でFBが出してきたFlux.今ではWebでもアプリでも単一方向のデータフローが流行っていて(というか流行ってしばらく経つ),Fluxベースの書き方がよく見られるようになりました.
Flutterではというと,最初からStatefulWidgetなるものが入っているため,小規模の状態管理ならこれで十分できますよと.更に,Flutterが正式リリースされた辺りではストリームベースで状態を扱うBLoCパターンや,ChangeNotifierを使った状態管理パターン(明確な名前が無い気がするけど,前にScoped Modelと言われていたものとほとんど同じ)が使われるようになっていきました.
昨今ではproviderパッケージの作者Remi氏が,ValueNotifierを状態管理向けに再実装したStateNotifierや,Providerそのものをより実行時エラーを出にくくし,かつテストとの親和性が高いProviderの提供手段としてRiverpodをリリースするなど,新たな動きも出てきているところです. FlutterでMVVM + Fluxに至るまで
さて,ココ最近元気にFlutterで業務開発しているところですが,やってることはというと,新規機能の追加もしつつの大規模なリファクタリング.自分が関わる前にリリースしていたアプリでしたが,リリースに至るまでに色々な人が触っていたようで,状態管理や記法が大変なことになっていたプロジェクトでした.
コードベースでもそこそこの規模に膨れ上がっていたうえ,ディレクトリ訳がmodels,pages,その他utilやapiなどって感じで,実際に中身を見ているとMVCのCがVに寄っている感じでした.
そんなこんなでリファクタリングを始めたわけですが,最初の方針としては,まずViewにあるロジックを別のところに逃がそうという思想のもと,新たにViewModelを構築することにしました.
ViewModelができたのならば,やはりModelからViewModelにデータを渡して,何か更新があればそれをModelに渡す設計で(要するにMVVM)いいんじゃないかな〜という感じで整理していきました.しかし,ここで一つ問題が生じます.そう,あの「一つの画面で更新した情報がもう一つの画面で更新されない」問題です.
https://gyazo.com/f3577eefba0a186fd794bcdc45a10c92
たまにはProcreate使ったろと思って手描きしたけど,絶対draw.io使ったほうが早いです.
状態管理はChangeNotifier一本に絞っているため(もともと導入していたため学習コストが一番低かった),これを解決するにはModelとViewModelの通信手段をStreamにするなどの工夫が必要でした.ただ,それをやろうとすると,テストも含めてかなりの時間がかかってしまうため,何かいい方法は無いかと探りながら,とりあえず「表示のみする部分はViewModelから,更新がある部分はModelから参照する」と言うような方法を取ろうとしていました.今思うとカオスの種でしかない.
https://gyazo.com/9ff2d444e77863de28c2cfc1e78f9c87
なんとかならんか…と考えていたところ,ふと天啓が降りてきました.「これ,ChangeNotifierで通知が行くのはView側だけなんだから,最初から状態の参照はViewだけということにすればいいのでは?」
ということで,最終的に以下の図のような構成にたどり着きました.
https://gyazo.com/1ae3e4bcc3f5926bced43438042f5cf1
命名:「MVVMの皮を被ったFluxアーキテクチャ」
Modelに管理対象の状態(DataClass型のフィールドstate)を置き,Viewではcontext.selectなどでstateを参照,アクションがあった場合はViewModelを介してModelにアクセス(Locatorを使って実現),Modelの中で定義されたstateを更新するメソッドに処理を委譲するという構図です.
この図は複数画面で管理したい状態にフォーカスしたものなので書いてませんが,1画面で完結する状態,例えばViewの表示・非表示を制御するisLoadingフラグなどはViewModelで持ちます.この場合,ViewModelもChangeNotifierを継承する形になります.
上の方では特に言及してませんでしたが,Modelに持つデータはfreezedで生成したデータクラス相当のクラスです.故にstateの中身そのものはimmutableで,何ならサーバーからフェッチしたデータをそのまま状態として格納できることになります. 考えた後でMVVMとFluxの関係性を調べていたところ,ネイティブ開発でも同じようなアプローチをとる試みがあったようです.
上の例では,ModelとViewModelの通信をRxで行うことによって状態の変更をModelに同期しています.Flutterでも同様のことは可能で,何ならRxなど使わなくともDart標準のStreamで実現できます.というかその場合は,MVVM + BLoCのような構成になるのかなと思います.
どれを選択するかは好みの問題だと思いますが,上の図で紹介したMVVM + Fluxは,FlutterではViewで直接stateを参照できるのでわざわざViewModelを介さなくてもいいですよ という主張になります.
なにが嬉しいのか
このような構造にすることで,
状態管理として必要なツールがChangeNotifier(+freezed)に限定され,比較的とっつきやすい
データフローそのものは単一方向のため,Fluxの利点である保守性の高さがついてくる
と言う感じになります.今のところ,そこそこの規模のアプリでもなんとかなるんじゃないかという気はしています.
逆に懸念点として,
notifyListeners()の衝突による障害が発生するかもしれない
stateを参照する際,Modelにあるメソッドも参照できてしまうため注意する必要がある
これはChangeNotifierを使っている限り起こりうる問題
StateNotifierを使うことで完全に解消可能(後述)
stateがnullableになりうるので,参照するときのケアが必要
これもChangeNotifierを使っている時にありがち
stateを全て読み込むまで画面を表示しない,View側でnullのときはプレースホルダーを表示するなど
Listなどのコレクションをstateに持つ場合,更新方法によっては細かいリビルドの制御ができない
StateNotifierのようにリストを新しく生成する方法を取ろうと思うと,View側で更新された範囲を把握できない
逆にStateNotifierであれば気にするところは減る
が挙げられます.ここ書いてから思ったけどChangeNotifierだと色々厳しくなってくるかも…
ところでそれ,Modelって名前変えたほうが良くない?
察しの良い方ならおわかりだと思いますが,ここまでくるともはやModelがModelである意味がよくわかりません.上の図をFluxの概念図と比較してみると…
https://gyazo.com/59ad86c5f39483e31136775d6ac273a7
ViewModelがDispatcherの役割を,そしてModelはStoreの役割を持っていることになります.ただ,この比較で言うとModelはstateを更新するためのメソッドを持つ必要があるので,Actionの定義をModelが持ち,ViewからはViewModelを介してActionをDispatchする,という解釈になると思います.もはやModelという名前だと役割がよくわからないので,Controllerかなんかにしたほうがいいかと思います.
ところでそれ,StateNotifierで良くない?
懸念点の部分でも書きましたが,ModelがChangeNotifierを使っている場合,View側でstateを参照する際にメソッドが見えてしまいます.
これはもはやChangeNotifierを使っている以上,取り決めやレビューでカバーするしか無い問題だと思いますが,ChangeNotifierではなくStateNotifierを使うことで解消可能です.
StateNotifierでは,StateNotifierProviderでStateそのものとStateNotifier<State>を継承したクラスをそれぞれ同時に降らせることができます.そのおかげで,下位からアクセスする際にStateのみを取得する,といったことが可能です.
https://gyazo.com/06da69571d562094a2c1571399063e7a
これで,Viewから参照する時にはStateを,ViewModelから参照するときにはStateNotifierをというように打ち分けすることができ,事故を防げる構成になります.
実装例
ChangeNotifier版
StateNotifier版
ValueNotifier版
があります.READMEに動かし方が書いてあるので参考にどうぞ.