🤔

FlutterのMVVM関連あれこれ

2023/02/15に公開

「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になります。

https://learn.microsoft.com/en-us/archive/msdn-magazine/2009/february/patterns-wpf-apps-with-the-model-view-viewmodel-design-pattern

このMVVMのことを、この後の文章では「WPFのMVVM」と呼び、可能な限り「MVVM」とは呼ばないようにします。
というのも、このアーキテクチャは、後ほどAndroidで採用されるMVVMとは異なるアーキテクチャと識別できるためです。「WPFのMVVM」はMVVMという設計思想WPFというフレームワークを前提にしたものであり、Androidというフレームワークを前提にしたものでも、宣言的UIを前提にしたものでもないと思っています。

なお、.Net MAUIにおいてMVVMをどのように実装するかについては、解説がMSから出ています。
ざっと眺める限り、XAMLを利用したUI構築のケースで利用している様子なため、文脈は宣言的UIのものでもないのかな〜と。実際に書いてみたら、印象が違うかもしれません。

https://learn.microsoft.com/ja-jp/dotnet/architecture/maui/mvvm


筆者はWPF向けのMVVMをほとんど書いたことがありません。このため、ドキュメントや各種ブログから推測した内容になります。

「WPFのMVVM」が登場したことの意味は、大きく3つあると思っています。

  1. (現代的な)GUIアプリケーションを前提としたアーキテクチャ
  2. データバインディング
  3. Viewにおける"ビジネスロジック"のテストが容易になる

(現代的な)GUIアプリケーションを前提としたアーキテクチャ

Presentation Domain Separationの文脈においては、MVVMはMVCやMVPなどのアーキテクチャの流れにあると理解できます。

MVVM以前にある代表的なアーキテクチャは、時代背景的に、モデルに重きが置かれているように思われます。
もちろん、これらのアーキテクチャは「アーキテクチャ」であり、適用できる範囲に制限はありません。
一方で、AndroidやiOSのアプリケーションを開発していると、日々の大半を「View」にかかわる範囲の実装や検討で過ごすことになります。WPFの話をしつつAndroidやiOSの話をするのも変なのですが、高度なGUIアプリケーションを分業して開発しているエンジニアが求めるのは、Model側ではなくView側に処理の軸足をおいたアーキテクチャです。(不具合は、Viewの実装を中心に起きるので…)

データバインディング

https://learn.microsoft.com/ja-jp/dotnet/desktop/wpf/data/?view=netdesktop-7.0

データバインディングは、フレームワークが提供する、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ベストプラクティスです。

https://speakerdeck.com/cheesesand101/mvvmbesutopurakuteisu

このMVVMでは、ViewModelは単なるクラスとなります。
DaggerなどでDIをすることはありますが、それはJavaのインスタンスをアプリケーションの内部で任意に管理しているにすぎません。


WPFを参考にしているため、このタイプの設計では「必要に応じて」ViewModelが作成されます。
例えば、ToolbarであったりButtonであったり、TextViewであったり。ロジックをまとめたいと思ったViewのクラスに対して、対応するViewModelが生成される、と言った具合です。このため、ViewModelの数は増えやすく、あるViewModelが別のViewModelと通信するような状況が生まれます。

当時はRxJavaが登場していたため、EventBusと合わせて、実装が試みられていたと記憶しています。
ただ、MVVMとして「こう実装するべき」という指針があったわけでもないので、各地でそれぞれの実装者が考えた実装がされていたハズです。

余談:MVVMが登場した頃の設計議論

WPFをベースにしたMVVMが登場した頃、MVVMは「ちょっとマイナーな」アーキテクチャだったと記憶しています。
人気があったのは、XMLとActivityFragmentButter Knifeを使って接続した設計だったかなと…。

当時の状況を知りたい方は、DroidKaigi 2017や2018のセッションを眺めてみると良いと思います。"Architecture"や"アーキテクチャ"などの単語で検索すれば、ざっと把握できるハズです。

https://droidkaigi.github.io/2017/timetable.html

https://droidkaigi.jp/2018/timetable/


手元にあったAndroidアプリ設計パターン入門を久々に開いたところ、MVPとMVVMが紹介されていました。執筆時期が2017年10月から12月ごろとなっているので、ちょうどAACのstable版がリリースされた頃となります。

https://android-developers.googleblog.com/2017/11/announcing-architecture-components-10.html

個人的にはDroidKaigiで紹介されていたMVIに感銘を受けた覚えがありますし、勉強していくとReduxをAndroidで採用しているケースもあったかなと。

若干記憶があやふやなのですが、別の話題、例えば「"Fragmentを使わない"設計」についての議論などの方が盛んだったかもしれません。BaseActivityAsyncTask、画面回転時にActivityを破棄させるかどうかなど、日々困る実装について関心が高かった頃だったかなと思います。

Androidのアーキテクチャに適合したMVVM

AACの1つとして登場し、2023年2月現在ではAndroid Jetpackとして提供されているのが、ViewModelです。

https://developer.android.com/topic/libraries/architecture/viewmodel

ViewModelはAndroid開発における、ActivityFragmentに関する多くの問題を解決しました。より正確を記すならば、AACによって提供されたLifecycleがUIの状態を再整理し、LiveDataViewModelActivityFragmentを安全に接続させました。

https://developer.android.com/topic/libraries/architecture/lifecycle

https://developer.android.com/topic/libraries/architecture/livedata

また、DataBindingライブラリを組み合わせることにより、XML上でViewModelを直接利用できるようになります。この機能により、XMLからViewModelに直接inputを渡すことができるように(=ActivityFragment上でコードを書かずにinputを渡すことができるように)なりました。

https://developer.android.com/studio/past-releases/past-android-studio-releases/as-3-1-0-release-notes#update-data-binding

これらの機能がAndroidアプリケーション開発にあたえた影響は、非常に大きなものがありました。

  • 公式ドキュメントとして、MVVMが推奨されるようになった
    • 設計パターンの共通認識を持ちやすくなった
    • Androidアプリケーション界隈で、共通の認識を持ちやすくなった
  • 画面回転やリサイズなどへの対応が、簡単に行えるようになった
    • RxJavaではミスが起こりがちだった、非同期処理のキャンセル処理に悩まされにくくなった
    • UIロジックの分離がしやすくなった

AACのMVVMがそれ以前のMVVMと大きく異なるのは、「ActivityFragmentの特徴を、AACが適切に処理している」点です。列挙すると、次のものが挙げられます。

  1. ActivityFragmentの破棄タイミングをより自然なもの(画面を離れた時に修正)にした
  2. ActivityFragmentに必要な処理と、XMLでレイアウトしたViewの処理を分離した
  3. 初期はRxJava、後期ではKotlin FlowなどのStreamをUIに簡単に反映できるようにした

このうち、特に強調したいのは1です。
AACのViewModelは、AndroidのActivityFragmentに適応しています。このため、AACのViewModelを作る単位は、対象となるActivityFragmentと対になることとなります。従来のWPF的なViewModelのようにViewごとに作ろうとすると、ViewModelの破棄タイミングが一致せず、不具合を起こすこととなります。

Androidにおいて、Activityはコア要素です。
というのも(最近のOSで動いている)全てのアプリケーションにはActivityが含まれています。
このため、全てのアプリケーションはActivityを実装する必要があり、Activityのライフサイクルを必ず意識する必要があります。

Jetpack ComposeとAAC ViewModel

2021年以降、Androidアプリケーション開発にJetpack Composeが導入され始めました。

https://android-developers.googleblog.com/2021/07/jetpack-compose-announcement.html

このJetpack Composeで実装するアプリケーションにAACのViewModelを導入するかどうかは、いくつかの論点が存在します。
まず、Androidの公式ドキュメントで推奨されているアーキテクチャには、ViewModelが存在します。

https://developer.android.com/topic/architecture?hl=ja

そして、Jetpack Composeの紹介においてもViewModelが登場します。

https://developer.android.com/jetpack/compose/mental-model?hl=ja

https://developer.android.com/jetpack/compose/state?hl=ja


一方で、Jetpack Composeはマルチプラットフォームなツールでもあります。
このため、AAC ViewModelを利用することで、他のプラットフォームにコードを移植しにくくしてしまうという意見もあります。
特にロジックをKMMで開発し、UIをJetpack Composeで作っている場合には、AAC ViewModelが利用できなくなります。

https://twitter.com/JimSproch/status/1396429288493109248

https://jakewharton.com/the-state-of-managing-state-with-compose/

まだ議論が確定しきっていない分野に(筆者には)見えるのですが、Jetpack Composeを利用したアプリケーションから、AAC ViewModelを使わなくなる日は訪れそうだと感じています。
というのも、宣言的UIを利用している場合にはデータバインディングが不要となり、Composableな関数は本質的にActivityのライフサイクルと切り離されるためです。

iOSにおけるMVVM

以下は、この3〜4年ほどiOSアプリケーションの開発に取り組んでいる筆者の私見です。
iOSにおいては、MVVMが必要になった時期はなかったと認識しています。

Androidにおいては、AAC ViewModelによりActivityFragmentのライフサイクルに関する課題を解消する必要がありました。そして、データバインディングの導入により、MVVMの更新がViewに簡単に反映されるようになっています。
しかし、iOSにおいては、そのような事情が存在しません。


iOSの開発においては"ロジックをどのように分離するか"よりも"画面間の遷移をどのように整理するか"に関心が向いていたように思います。

AndroidとiOSの実装を見比べてみると、大きく異なるのがインスタンスの扱いです。
AndroidではDaggerなどを利用し、インスタンスの生存期間をActivityFragment、もしくはアプリケーションと厳密に一致させようとします。
一方、iOSの場合、よほどのことがない限りSingletonなインスタンスとして管理できます。必要であれば、lazyを使うことで生成を遅延させるかもしれません。
ここには、Appleが品質をコントロールしているiPhoneやiPadで動くことを期待するアプリケーションと、多様なメーカーがはちゃめちゃに端末をリリースして(いた)Androidの違いがあるかもしれません。

そのほか、型情報の扱いは難しくなりますがNotificationCenterにより異なるインスタンス間で通信をすることもできますし、DispatchQueueを指定することで処理のスレッドを制御することも簡単です。
これらの仕組みを組み合わせれば、iOS SDKのみで十分に高度なアプリケーションの開発ができます。


手元にあったiOSアプリ設計パターン入門を確認してみたところ、MVVMはMVCやMVPと並列に紹介されていました。
執筆時期は2018年5月から半年とのことなので、AndroidにおいてMVVMがデファクトとなりつつあったころ、iOSではアーキテクチャの選択肢がまだ狭まっていなかったと言えるでしょう。

SwiftUIとObservedObject

https://developer.apple.com/documentation/swiftui/observedobject

iOSにおける宣言的UIといえば、SwiftUIです。
このSwiftUIでは、ObservedObjectが重要なクラスとして登場します。(ほかにもEnvironmentObjectStateObjectなどがあります)

これらの要素は、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

https://medium.flutterdevs.com/bloc-pattern-in-flutter-part-1-flutterdevs-128f90059f5c

Flutterの正式リリース前に紹介されたのはBLoCです。2018年のGoogle I/Oで紹介されたので、当時から追いかけている方は記憶の片隅に残っているでしょう。

あまり議論したことはないのですが、BLoCは「設計思想」に分類できると思っています。
というのも、BLoCは「ロジックをまとめたクラスのInput/OutputをStreamに統一」することで、「(Dartで書かれた)ビジネスロジックをプラットフォームで共有する」仕組みと解せるためです。

https://ntaoo.hatenablog.com/entry/2018/10/08/072933


Flutterのリリース当初ということもあり、まだライブラリも未成熟でした。
このため、Flutterのフレームワークに沿った実装をしようとするとInheritedWidgetを利用することになり、非常にハードルが高かったことを覚えています。

Flutter 2以降ではFlutter webが(一応)正式リリースとなっていますが、当時は実験的なプロジェクトの位置づけです。
このため、Webアプリケーションを作るのがAngularDart、AndroidやiOSアプリケーションを作るのがFlutterという割り振りになります。この2つのフレームワーク間でロジックを共有できる、もしくは、すでにDartで書かれているロジックをFlutterに持ち込める、そういった思惑があったように思います。


結果的に、BLoCパターンは普及しませんでした。
いくつか理由があると思うのですが、当時を思い返すと、下記のような事情があったように思います。

  1. 新規にFlutterを学びつつアプリケーションを新規開発する場合、BLoCパターンで流用できるロジックがない
  2. Streamを多用することになるため、実装の難度が高い
  3. 一般的なアプリケーションにおいては、アプリケーション内の処理が、Streamである必要性がない

MVVMとProvider

BLoCが公式で紹介されるも、先述のような理由でデファクトとなることはありませんでした。
このため、Flutterのアプリケーション開発に取り組むエンジニアが、それぞれ「もともと知見のある」アーキテクチャをFlutterの開発に持ち込みます。この持ち込まれた代表的な設計パターンが、Reduxであり、MVVMです。

また、Providerが登場したことで、Flutterの仕組みに乗った状態管理がしやすくなりました。特に、Providerにより複数のModelを組み合わせやすくなったことで、開発の幅が広がったように思います。
Providerは、AndroidでDaggerを利用していた人からすると、革命的なツールでした。というのも、Providerを使えば、Androidで運用していたアーキテクチャが導入できるようになったためです。

そうなると、当然、AndroidのデファクトスタンダードであるMVVMを持ち込むこととなります。
Remi氏がfreezedstate_notifierを発表したこともあり、MVVMを持ち込みやすくなった、という追い風もあったように記憶しています。


このころにProviderを利用したMVVMの設計をすることには、一定の合理性があったと、筆者は判断しています。

Providerは、その仕組み上、次に表示する画面(Scaffoldを含むWidget)をProviderMultiProviderでくくる必要が生じます。小規模なアプリケーションでは問題がないのですが、大規模なアプリケーションでは「アプリケーション内のrouingを定義するクラスの中で、ひたすらProvideするインスタンスの定義をする」こととなります。
TextFieldTextFieldButtonをまとめたCardレベルでViewModelを作成することもできるのですが、このサイズでViewModelを作成していると、1つのアプリの中で利用するViewModelの数が爆発していきます。一方、AndroidのActivityごとにViewModelを作る感覚で作っていけば、計算上、ViewModelの数は「アプリケーションの中の画面の数」で収まります。
理想的な話をすれば、ロジックごとにViewModelを分割し、テストなどを記述するべきです。しかしFlutterの習熟度を高めつつ、大規模なアプリケーションの開発に取り組んでいく中では、画面に応じたViewModelとする判断は悪くないと思います。

一方で、当時から「FlutterでMVVMは適切ではないのではないか」という声はありました。
MVVMを採用しないケースもあれば、GetXを利用するなどの選択をした方も、多いのではないかと思います。


本文章の頭でまとめた、MVVMの特徴がFlutterというフレームワークとどれだけ相性が良いかというと、正直そこまでメリットを感じるものではないと思います。
というのも、「FluterとMVVMを組み合わせることで実現できるデータバインディング」というものが存在しません。
また、Flutterの仕組みそのものがViewとModelを切り分けやすくなっているため、InheritedWidgetProviderを使えば十分なPDSが実現できます。

Riverpod

前項までの状況のなか、発表されたのがRiverpodです。

Riverpodが登場したことで、Providerで課題となる「大量にProviderMultiProviderでくくらなければならない」という課題が解消されます。
つまり、AndroidのViewModelのように画面全体を1つのStateとして管理したいというモチベーションが弱くなったと言えます。
結果として、RiverpodとMVVMを組み合わせるメリットが、Providerの頃ほど高いものとは言えなくなりました。


以下は、Riverpodが登場した後に筆者が感じている私見です。

Riverpodが登場したことで、画面全体の代わりに、1つのWidgetとしてまとめられる単位で状態管理をできるようになりつつあります。
このため、現在再利用可能な単位のWidgetを状態管理の単位とする設計に注目が集まり始めています。
AndroidのMVVMがProviderの頃に参考とされたように、RiverpodではReactが参考にされるようになってきています。

Reactにおけるreact-queryswrは、RiverpodのFutureProviderで実現できるようになりました。
また、React Hooksによる開発は、flutter_hooksによって再現できるようになっています。

今後の開発動向に期待です。

まとめ

簡単にまとめようと思っていたら、1万字を超えてしまいました。どうしてこうなった。

都度調べながら記述をしていますが、書き連ねた範囲が広いため、いくつも指摘が入るかなと思っています。
半ば自らの考えをまとめるためのメモとして書き始めたので、ある程度はご容赦いただければと。

本メモを書き始めたモチベーションとしては、MVVMについて語ろうとすると、あまりにも前提として共有するべき情報が多すぎると感じたためです。
このため、MVVMについて意見を述べようとすると、話し手と聞き手のコンテクストの違いから不毛な意見交換になることがあります。
単に「MVVMを採用した方がいい」という話ではなく、時には批判的にMVVMについて考えつつ、より良いアプリケーションの開発ができればと思っています。


この文章は、SAKANAQUARIUM ARCHIVE THEATERを聴きながら書き上げました。ありがとう。サカナクション。

サカナクション Youtube channel

GitHubで編集を提案

Discussion