FlutterのMVVM関連あれこれ
「FlutterにおけるMVVMはあり/なし」を見かけると、色々と前提とか文脈とかあるよなぁって思うので、長いメモをまとめておきます。
TL;DR
筆者の意見は、下記の表になります。
記載していないプラットフォームやツールを採用した場合は、個別の議論が発生するハズ。
プラットフォーム | 世代 | 意見 |
---|---|---|
Android | View | Android Architecuture ComponentsとMVVMを採用するべき |
Android | Jetpack Compose | 画面回転対応などの必要があれば採用する |
iOS | UIKit | 採用の必要性はなさそう |
iOS | SwiftUI | 採用の必要性はなさそう |
Flutter | Provider | 採用する弱い理由がある |
Flutter | Riverpod | 採用する強い理由はない |
MVVMのあれこれ
WPFのMVVM
MVVMは、2005年ごろに登場したアーキテクチャです。
対象となるプラットフォームはWPFになります。
このMVVMのことを、この後の文章では「WPFのMVVM」と呼び、可能な限り「MVVM」とは呼ばないようにします。
というのも、このアーキテクチャは、後ほどAndroidで採用されるMVVMとは異なるアーキテクチャと識別できるためです。「WPFのMVVM」はMVVMという設計思想とWPFというフレームワークを前提にしたものであり、Androidというフレームワークを前提にしたものでも、宣言的UIを前提にしたものでもないと思っています。
なお、.Net MAUIにおいてMVVMをどのように実装するかについては、解説がMSから出ています。
ざっと眺める限り、XAMLを利用したUI構築のケースで利用している様子なため、文脈は宣言的UIのものでもないのかな〜と。実際に書いてみたら、印象が違うかもしれません。
筆者はWPF向けのMVVMをほとんど書いたことがありません。このため、ドキュメントや各種ブログから推測した内容になります。
「WPFのMVVM」が登場したことの意味は、大きく3つあると思っています。
- (現代的な)GUIアプリケーションを前提としたアーキテクチャ
- データバインディング
- Viewにおける"ビジネスロジック"のテストが容易になる
(現代的な)GUIアプリケーションを前提としたアーキテクチャ
Presentation Domain Separationの文脈においては、MVVMはMVCやMVPなどのアーキテクチャの流れにあると理解できます。
MVVM以前にある代表的なアーキテクチャは、時代背景的に、モデルに重きが置かれているように思われます。
もちろん、これらのアーキテクチャは「アーキテクチャ」であり、適用できる範囲に制限はありません。
一方で、AndroidやiOSのアプリケーションを開発していると、日々の大半を「View」にかかわる範囲の実装や検討で過ごすことになります。WPFの話をしつつAndroidやiOSの話をするのも変なのですが、高度なGUIアプリケーションを分業して開発しているエンジニアが求めるのは、Model側ではなくView側に処理の軸足をおいたアーキテクチャです。(不具合は、Viewの実装を中心に起きるので…)
データバインディング
データバインディングは、フレームワークが提供する、ViewModelに変更があったときにViewが自動的に変更される仕組みです。データバインディングを利用することで、ViewModelの内部でViewのインスタンスに触るコードを書くことなしに、アプリケーションを開発できます。
また、ViewModelに対して、Viewから行われるInputをreactiveに実装することもできます。双方向バインディング、もしくはTwoWay bindingと呼ばれる実装をすることで、ViewModelは逐一Viewからのコールバック処理を書くことなしに、ユーザーが行ったUIイベントを処理できます。
Viewにおける"ビジネスロジック"のテストが容易になる
MVVMにおいては、ViewはViewModelの状態を反映するだけの存在となります。
よってViewの不具合、つまり「見た目がおかしい」のは「ViewModelの状態がおかしい」ことが原因となります。
実際にはViewライブラリに問題があることはありますが、それは実装しているビジネスロジックの問題ではありません。
このため、ViewModelをテストすると、Viewを対象としたビジネスロジックのテストを行うことができます。
ViewModelは「プログラミング言語で書かれたプログラム」です。
このため、「プログラミング言語向けのテスト環境」であれば、テストを実行できます。
これができない場合は、Viewを仮想的に描画し、タッチイベントや入力イベントを通してテストを行う必要があります。フレームワークがあらゆるOSに対応していれば問題がありませんが、特定のOS向けの場合には、テストを実行できる環境が限られることとなります。
そのほかにも、Viewの仕組みがフレームワークのバージョンによって異なるケースで、テスト対象とするフレームワークのすべてのバージョンで動作を確認する必要なども生じてしまいます。
AndroidにおけるMVVM
Androidにおいては、WPFをベースにしたMVVMと、Android Architecture Components(AAC)に追加されたViewModel
を利用したMVVMの2つが存在します。
最も大きな違いは、前者が「View
に対応するViewModel
」であるのに対し、後者が「Activity
またはFragment
に対応するViewModel
」である点です。
WPFをベースにしたMVVM
実装例としてわかりやすいのは、DroidKaigi 2018のMVVMベストプラクティスです。
このMVVMでは、ViewModelは単なるクラスとなります。
DaggerなどでDIをすることはありますが、それはJavaのインスタンスをアプリケーションの内部で任意に管理しているにすぎません。
WPFを参考にしているため、このタイプの設計では「必要に応じて」ViewModelが作成されます。
例えば、ToolbarであったりButtonであったり、TextViewであったり。ロジックをまとめたいと思ったViewのクラスに対して、対応するViewModelが生成される、と言った具合です。このため、ViewModelの数は増えやすく、あるViewModelが別のViewModelと通信するような状況が生まれます。
当時はRxJavaが登場していたため、EventBusと合わせて、実装が試みられていたと記憶しています。
ただ、MVVMとして「こう実装するべき」という指針があったわけでもないので、各地でそれぞれの実装者が考えた実装がされていたハズです。
余談:MVVMが登場した頃の設計議論
WPFをベースにしたMVVMが登場した頃、MVVMは「ちょっとマイナーな」アーキテクチャだったと記憶しています。
人気があったのは、XMLとActivity
やFragment
をButter Knifeを使って接続した設計だったかなと…。
当時の状況を知りたい方は、DroidKaigi 2017や2018のセッションを眺めてみると良いと思います。"Architecture"や"アーキテクチャ"などの単語で検索すれば、ざっと把握できるハズです。
手元にあったAndroidアプリ設計パターン入門を久々に開いたところ、MVPとMVVMが紹介されていました。執筆時期が2017年10月から12月ごろとなっているので、ちょうどAACのstable版がリリースされた頃となります。
個人的にはDroidKaigiで紹介されていたMVIに感銘を受けた覚えがありますし、勉強していくとReduxをAndroidで採用しているケースもあったかなと。
若干記憶があやふやなのですが、別の話題、例えば「"Fragment
を使わない"設計」についての議論などの方が盛んだったかもしれません。BaseActivity
やAsyncTask
、画面回転時にActivity
を破棄させるかどうかなど、日々困る実装について関心が高かった頃だったかなと思います。
Androidのアーキテクチャに適合したMVVM
AACの1つとして登場し、2023年2月現在ではAndroid Jetpackとして提供されているのが、ViewModel
です。
ViewModel
はAndroid開発における、Activity
やFragment
に関する多くの問題を解決しました。より正確を記すならば、AACによって提供されたLifecycle
がUIの状態を再整理し、LiveData
がViewModel
とActivity
やFragment
を安全に接続させました。
また、DataBindingライブラリを組み合わせることにより、XML上でViewModelを直接利用できるようになります。この機能により、XMLからViewModelに直接inputを渡すことができるように(=Activity
やFragment
上でコードを書かずにinputを渡すことができるように)なりました。
これらの機能がAndroidアプリケーション開発にあたえた影響は、非常に大きなものがありました。
- 公式ドキュメントとして、MVVMが推奨されるようになった
- 設計パターンの共通認識を持ちやすくなった
- Androidアプリケーション界隈で、共通の認識を持ちやすくなった
- 画面回転やリサイズなどへの対応が、簡単に行えるようになった
- RxJavaではミスが起こりがちだった、非同期処理のキャンセル処理に悩まされにくくなった
- UIロジックの分離がしやすくなった
AACのMVVMがそれ以前のMVVMと大きく異なるのは、「Activity
やFragment
の特徴を、AACが適切に処理している」点です。列挙すると、次のものが挙げられます。
-
Activity
やFragment
の破棄タイミングをより自然なもの(画面を離れた時に修正)にした -
Activity
やFragment
に必要な処理と、XMLでレイアウトしたViewの処理を分離した - 初期はRxJava、後期ではKotlin FlowなどのStreamをUIに簡単に反映できるようにした
このうち、特に強調したいのは1です。
AACのViewModelは、AndroidのActivity
やFragment
に適応しています。このため、AACのViewModelを作る単位は、対象となるActivity
やFragment
と対になることとなります。従来のWPF的なViewModelのようにViewごとに作ろうとすると、ViewModelの破棄タイミングが一致せず、不具合を起こすこととなります。
Androidにおいて、Activity
はコア要素です。
というのも(最近のOSで動いている)全てのアプリケーションにはActivity
が含まれています。
このため、全てのアプリケーションはActivity
を実装する必要があり、Activity
のライフサイクルを必ず意識する必要があります。
Jetpack ComposeとAAC ViewModel
2021年以降、Androidアプリケーション開発にJetpack Composeが導入され始めました。
このJetpack Composeで実装するアプリケーションにAACのViewModelを導入するかどうかは、いくつかの論点が存在します。
まず、Androidの公式ドキュメントで推奨されているアーキテクチャには、ViewModelが存在します。
そして、Jetpack Composeの紹介においてもViewModelが登場します。
一方で、Jetpack Composeはマルチプラットフォームなツールでもあります。
このため、AAC ViewModelを利用することで、他のプラットフォームにコードを移植しにくくしてしまうという意見もあります。
特にロジックをKMMで開発し、UIをJetpack Composeで作っている場合には、AAC ViewModelが利用できなくなります。
まだ議論が確定しきっていない分野に(筆者には)見えるのですが、Jetpack Composeを利用したアプリケーションから、AAC ViewModelを使わなくなる日は訪れそうだと感じています。
というのも、宣言的UIを利用している場合にはデータバインディングが不要となり、Composableな関数は本質的にActivity
のライフサイクルと切り離されるためです。
iOSにおけるMVVM
以下は、この3〜4年ほどiOSアプリケーションの開発に取り組んでいる筆者の私見です。
iOSにおいては、MVVMが必要になった時期はなかったと認識しています。
Androidにおいては、AAC ViewModelによりActivity
やFragment
のライフサイクルに関する課題を解消する必要がありました。そして、データバインディングの導入により、MVVMの更新がViewに簡単に反映されるようになっています。
しかし、iOSにおいては、そのような事情が存在しません。
iOSの開発においては"ロジックをどのように分離するか"よりも"画面間の遷移をどのように整理するか"に関心が向いていたように思います。
AndroidとiOSの実装を見比べてみると、大きく異なるのがインスタンスの扱いです。
AndroidではDaggerなどを利用し、インスタンスの生存期間をActivity
やFragment
、もしくはアプリケーションと厳密に一致させようとします。
一方、iOSの場合、よほどのことがない限りSingletonなインスタンスとして管理できます。必要であれば、lazy
を使うことで生成を遅延させるかもしれません。
ここには、Appleが品質をコントロールしているiPhoneやiPadで動くことを期待するアプリケーションと、多様なメーカーがはちゃめちゃに端末をリリースして(いた)Androidの違いがあるかもしれません。
そのほか、型情報の扱いは難しくなりますがNotificationCenterにより異なるインスタンス間で通信をすることもできますし、DispatchQueueを指定することで処理のスレッドを制御することも簡単です。
これらの仕組みを組み合わせれば、iOS SDKのみで十分に高度なアプリケーションの開発ができます。
手元にあったiOSアプリ設計パターン入門を確認してみたところ、MVVMはMVCやMVPと並列に紹介されていました。
執筆時期は2018年5月から半年とのことなので、AndroidにおいてMVVMがデファクトとなりつつあったころ、iOSではアーキテクチャの選択肢がまだ狭まっていなかったと言えるでしょう。
SwiftUIとObservedObject
iOSにおける宣言的UIといえば、SwiftUIです。
このSwiftUIでは、ObservedObjectが重要なクラスとして登場します。(ほかにもEnvironmentObjectやStateObjectなどがあります)
これらの要素は、AndroidのBaseObservableと役割が近い印象です。
BaseObservableの代わりにAAC ViewModelが利用できることを考えると、AAC ViewModelと役割が近いと言えるかもしれません。
踏み込んでしまえば、SwiftUIという仕組みそのものがMVVM的な設計である、と言えるのではと感じています。
ただ、ViewModelが持つ役割に、WPFやAndroidにおいてXMLに分離されていたレイアウトの設計が含まれています。
レイアウトの設計と状態管理を書くと、フレームワーク側でデータバインディングをしてくれる、それがSwiftUIなのではないでしょうか。
結果として、SwiftUIではMVVMのViewModelを開発者が個別に実装する必要がありません。
実装をしようとしても、単にフレームワークが用意している状態管理をラップするか、ビジネスロジックを適度にまとめることになるのかなと。
そうなると、WPFやAndroidのViewModel的な実装をすることが、生産性の向上に寄与しにくくなるのではないか、と考えています。
FlutterにおけるMVVM
FlutterにおいてMVVMの採用を議論するためには、いくつかのステップがあります。
まず、最初に確認をするべきなのは「Flutter開発のいつ頃を話題にしているのか」です。
2023年2月から見た時、Flutterの開発は4つほどの時期に分類できると感じています。
- BLoCの紹介
- BLoCではないアーキテクチャの模索(ReduxやMVVM)
- Provider + StateNotifier
- Riverpod(v2)
BLoCとInheritedWidget
Flutterの正式リリース前に紹介されたのはBLoCです。2018年のGoogle I/Oで紹介されたので、当時から追いかけている方は記憶の片隅に残っているでしょう。
あまり議論したことはないのですが、BLoCは「設計思想」に分類できると思っています。
というのも、BLoCは「ロジックをまとめたクラスのInput/OutputをStream
に統一」することで、「(Dartで書かれた)ビジネスロジックをプラットフォームで共有する」仕組みと解せるためです。
Flutterのリリース当初ということもあり、まだライブラリも未成熟でした。
このため、Flutterのフレームワークに沿った実装をしようとするとInheritedWidget
を利用することになり、非常にハードルが高かったことを覚えています。
Flutter 2以降ではFlutter webが(一応)正式リリースとなっていますが、当時は実験的なプロジェクトの位置づけです。
このため、Webアプリケーションを作るのがAngularDart、AndroidやiOSアプリケーションを作るのがFlutterという割り振りになります。この2つのフレームワーク間でロジックを共有できる、もしくは、すでにDartで書かれているロジックをFlutterに持ち込める、そういった思惑があったように思います。
結果的に、BLoCパターンは普及しませんでした。
いくつか理由があると思うのですが、当時を思い返すと、下記のような事情があったように思います。
- 新規にFlutterを学びつつアプリケーションを新規開発する場合、BLoCパターンで流用できるロジックがない
- Streamを多用することになるため、実装の難度が高い
- 一般的なアプリケーションにおいては、アプリケーション内の処理が、Streamである必要性がない
MVVMとProvider
BLoCが公式で紹介されるも、先述のような理由でデファクトとなることはありませんでした。
このため、Flutterのアプリケーション開発に取り組むエンジニアが、それぞれ「もともと知見のある」アーキテクチャをFlutterの開発に持ち込みます。この持ち込まれた代表的な設計パターンが、Reduxであり、MVVMです。
また、Providerが登場したことで、Flutterの仕組みに乗った状態管理がしやすくなりました。特に、Providerにより複数のModelを組み合わせやすくなったことで、開発の幅が広がったように思います。
Providerは、AndroidでDaggerを利用していた人からすると、革命的なツールでした。というのも、Providerを使えば、Androidで運用していたアーキテクチャが導入できるようになったためです。
そうなると、当然、AndroidのデファクトスタンダードであるMVVMを持ち込むこととなります。
Remi氏がfreezedとstate_notifierを発表したこともあり、MVVMを持ち込みやすくなった、という追い風もあったように記憶しています。
このころにProviderを利用したMVVMの設計をすることには、一定の合理性があったと、筆者は判断しています。
Providerは、その仕組み上、次に表示する画面(Scaffold
を含むWidget)をProvider
やMultiProvider
でくくる必要が生じます。小規模なアプリケーションでは問題がないのですが、大規模なアプリケーションでは「アプリケーション内のrouingを定義するクラスの中で、ひたすらProvideするインスタンスの定義をする」こととなります。
TextField
やTextField
とButton
をまとめたCard
レベルでViewModelを作成することもできるのですが、このサイズでViewModelを作成していると、1つのアプリの中で利用するViewModelの数が爆発していきます。一方、AndroidのActivity
ごとにViewModelを作る感覚で作っていけば、計算上、ViewModelの数は「アプリケーションの中の画面の数」で収まります。
理想的な話をすれば、ロジックごとにViewModelを分割し、テストなどを記述するべきです。しかしFlutterの習熟度を高めつつ、大規模なアプリケーションの開発に取り組んでいく中では、画面に応じたViewModelとする判断は悪くないと思います。
一方で、当時から「FlutterでMVVMは適切ではないのではないか」という声はありました。
MVVMを採用しないケースもあれば、GetXを利用するなどの選択をした方も、多いのではないかと思います。
本文章の頭でまとめた、MVVMの特徴がFlutterというフレームワークとどれだけ相性が良いかというと、正直そこまでメリットを感じるものではないと思います。
というのも、「FluterとMVVMを組み合わせることで実現できるデータバインディング」というものが存在しません。
また、Flutterの仕組みそのものがViewとModelを切り分けやすくなっているため、InheritedWidget
やProvider
を使えば十分なPDSが実現できます。
Riverpod
前項までの状況のなか、発表されたのがRiverpodです。
Riverpodが登場したことで、Providerで課題となる「大量にProvider
やMultiProvider
でくくらなければならない」という課題が解消されます。
つまり、AndroidのViewModelのように画面全体を1つのStateとして管理したいというモチベーションが弱くなったと言えます。
結果として、RiverpodとMVVMを組み合わせるメリットが、Providerの頃ほど高いものとは言えなくなりました。
以下は、Riverpodが登場した後に筆者が感じている私見です。
Riverpodが登場したことで、画面全体の代わりに、1つのWidgetとしてまとめられる単位で状態管理をできるようになりつつあります。
このため、現在再利用可能な単位のWidgetを状態管理の単位とする設計に注目が集まり始めています。
AndroidのMVVMがProviderの頃に参考とされたように、RiverpodではReactが参考にされるようになってきています。
Reactにおけるreact-queryやswrは、RiverpodのFutureProviderで実現できるようになりました。
また、React Hooksによる開発は、flutter_hooksによって再現できるようになっています。
今後の開発動向に期待です。
まとめ
簡単にまとめようと思っていたら、1万字を超えてしまいました。どうしてこうなった。
都度調べながら記述をしていますが、書き連ねた範囲が広いため、いくつも指摘が入るかなと思っています。
半ば自らの考えをまとめるためのメモとして書き始めたので、ある程度はご容赦いただければと。
本メモを書き始めたモチベーションとしては、MVVMについて語ろうとすると、あまりにも前提として共有するべき情報が多すぎると感じたためです。
このため、MVVMについて意見を述べようとすると、話し手と聞き手のコンテクストの違いから不毛な意見交換になることがあります。
単に「MVVMを採用した方がいい」という話ではなく、時には批判的にMVVMについて考えつつ、より良いアプリケーションの開発ができればと思っています。
この文章は、SAKANAQUARIUM ARCHIVE THEATERを聴きながら書き上げました。ありがとう。サカナクション。
Discussion