Android
 Computer >> コンピューター >  >> システム >> Android

Androidアプリ開発でMVVMをプロフェッショナルに使いこなす方法

本記事の目的は、GUIアーキテクチャにおけるプレゼンテーションロジックという観点から、Model-View-ViewModel(MVVM)パターンが状況によっては非常に厄介な「関心の分離」をもたらす理由を解説することです。

さらに、MVVMには2つのバリエーション(実装方法は1つだけではありません)があり、プロジェクトの要件に応じてどちらを選ぶべきかについても掘り下げていきます。

MVVM vs MVP/MVC?

毎週日曜に開催しているライブQ&Aセッションで最もよく受ける質問は、おそらく次のようなものです。

MVVMとMVP/MVC、どちらを使うべきですか?

この質問を受けたとき、私は必ず「あらゆる場面で優れた機能を発揮する単一のGUIアーキテクチャは存在しない」という点を強調します。

なぜでしょうか? 特定のアプリケーションにとって最適なアーキテクチャ(少なくとも良い選択肢)は、その時々の要件に大きく依存するからです。

ここで、「要件」という言葉が実際に何を意味するのか、少し整理してみましょう。

  • UIはどれくらい複雑か? シンプルなUIであれば、それを制御するための複雑なロジックは通常不要です。一方、複雑なUIでは、スムーズに動作させるために膨大なロジックときめ細かな制御が必要になります。
  • テストをどれだけ重視するか? 一般的に、フレームワークやOS(特にユーザーインターフェース)と密結合しているクラスは、テストに追加の手間がかかります。
  • どれほどの再利用性と抽象化を促進したいか? アプリケーションのバックエンドやドメイン層、さらにはプレゼンテーションロジックを異なるプラットフォーム間で共有したい場合はどうでしょうか?
  • あなた自身は、本質的にプラグマティック(実用主義)なのか、完璧主義なのか、怠惰なのか、あるいは状況に応じてそのすべてなのか?

上記の要件や関心事と照らし合わせてMVVMがどのように機能するかを詳細に論じた記事を書きたいところですが、残念ながら「MVVMのやり方は1つしかない」と誤解している読者も多いようです。

そこで本記事では、それぞれ異なるメリットとデメリットを持つ2つのアプローチについて解説します。まずは共通の基本概念から始めましょう。

Viewクラスを参照してはならない

古風な英語が苦手な方のために言い換えると:ViewModelからViewクラスを参照してはいけません」。

クラス名にViewModelを使うこと(ロジックだらけのクラスだと逆に紛らわしいのですが)を除けば、MVVMアーキテクチャにおける唯一の鉄則は、ViewModelからViewを決して参照してはならないということです。

さて、最初につまずきやすいのが「参照」という言葉です。いくつかのレベルの専門用語で言い直してみます。

  • ViewModelは、いかなるViewへの参照(メンバ変数、プロパティ、可変・不変フィールド)も持ってはなりません
  • ViewModelは、いかなるViewにも依存してはなりません
  • ViewModelは、Viewと直接やり取りしてはなりません

Androidプラットフォームにおいてこのルールが重要なのは、「アーキテクチャに詳しい人が悪いと言ったから」ではありません。

Architecture ComponentsのViewModelクラス(適切な場面においてFragment/Activityのライフサイクルより長くインスタンスが永続化されるよう設計されています)を使用する場合、Viewを参照すると深刻なメモリリークを引き起こしかねません。

そもそもMVVMがこうした参照を許さない理由は、理論上はViewとViewModelの両方をよりテストしやすく、書きやすくするためです。

また、ViewModelの再利用性が高まるという指摘もありますが、まさにここでこのパターンは崩壊します

コードを見る前に注意しておくと、私は自身のプロダクションコードではLiveDataを使用していません。最近は独自のPublisher-Subscriberパターンを実装する方が好みですが、以下の内容はViewModelからViewへのPubSub/Observerパターン接続を可能にするあらゆるライブラリに当てはまります。

本記事の内容の多くは、以下の動画チュートリアルでも扱っています。

ViewLogic+ViewModel か、View+ViewModelController か?

前のセクションで「崩壊する」と述べましたが、パターンそのものが文字通り壊れるわけではありません。「崩壊して」少なくとも2つの異なるアプローチに分化することを意味します。これらは外見もメリットも結果も大きく異なります。

Androidアプリ開発でMVVMをプロフェッショナルに使いこなす方法
ボロミア曰く、MVVMはアプリのプレゼンテーションロジックを消し去る魔法の杖ではないのです。

第一のアプローチ:再利用可能なViewModelを優先する

私の見る限り、MVVMを実装する人の大多数はViewModelの再利用性を高めることを目標としており、1つのViewModelをn個の異なるViewで使い回せるようにしています(多対一の関係)。

端的に言えば、この再利用性を実現する方法は2つあります。

  • 特定のViewを参照しないこと。今となっては常識ですね。
  • UI全般の詳細について、できる限り知らないでおくこと。

2つ目のポイントは曖昧に聞こえるかもしれません(参照していないものについてどうやって知ることができるのか?)。そこで、そろそろコードを見てみましょう。

class NoteViewModel(val repo: NoteRepo): ViewModel(){
    //Note: you may also publish data to the View via Databinding, RxJava Observables, and other approaches. Although I do not like to use LiveData in back end classes, it works great with Android front end with AAC
    val noteState: MutableLiveData<Note>()
    //...
    fun handleEvent(event: NoteEvent) {
        when (event) {
            is NoteEvent.OnStart -> getNote(event.noteId)
            //...
        }
    }
    private fun getNote(noteId: String){
        noteState.value = repo.getNote(noteId)
    }
}

非常にシンプルな例ですが、ポイントは、このViewModelが公開しているのはhandleEvent関数以外では、シンプルなNoteオブジェクトだけだということです。

data class Note(val creationDate:String,
                val contents:String,
                val imageUrl: String,
                val creator: User?)

このアプローチでは、ViewModelは特定のViewからだけでなく、その詳細、ひいては特定のViewのプレゼンテーションロジックからも完全に切り離されています。

まだ伝わりづらいかもしれませんが、次のもう1つのアプローチを説明すれば、きっと明確になるはずです。

先ほどの見出し「ViewLogic+ViewModel…」は真剣に受け取るためのものではありませんが、意味するところは、高度に疎結合で再利用可能なViewModelを持つことで、今度は画面上でこのNoteオブジェクトをどのように描画・バインドするかを考える作業をView側に委ねることになる、ということです。

Viewクラスにロジックを詰め込むのを好まない人もいます。

ここからが本当に泥臭く、プロジェクトの要件に依存する部分です。以下のようなロジックをViewクラスに書くことが……

private fun observeViewModel() {
    viewModel.notes.observe(
        viewLifecycleOwner,
        Observer { notes: List<Note> ->
            if (notes.isEmpty()) showEmptyState()
            else showNoteList(notes)
        }
    )
   //..
}

……常に悪いことだとは言いません。しかし、プラットフォームと密結合したクラス(Fragmentなど)はテストが困難であり、ロジックを含むクラスこそが最もテストすべき重要なクラスなのです!

一言でいえば、これは私が優れたアーキテクチャの黄金律と考えるもの——関心の分離——を適用できていない状態です。

個人的な意見としては、関心の分離を極めて高いレベルで適用する価値があると思います。しかし、その意味をまったく理解していない人たちによって数多くの収益性の高いアプリが書かれてきたことも、決して忘れないでください。

いずれにせよ、次に説明するアプローチは、それ自体の副作用を持ちつつも、再びプレゼンテーションロジックをViewから取り除きます。

ええ、ほとんど全部取り除きますが。

第二のアプローチ:謙虚なView、支配欲の強いViewModel

ViewModelの再利用性を優先した結果として生じる、Viewに対するきめ細かな制御の欠如は、時にかなり苦痛です。

さらに前述のアプローチを無差別に適用する気を削ぐのは、多くの場合ViewModelを再利用する必要がないことに私自身気づいているからです。

皮肉なことに、「過度な抽象化」はMVPに対するMVVMの一般的な批判でもあります。

とはいえ、Viewへのきめ細かな制御を取り戻すために、単純に参照をViewModelへ戻せばよいというものではありません。それは基本的に「MVP+メモリリーク」(AACのViewModelを使い続けている場合)にすぎません。

そこで代わりの手段となるのは、与えられたViewのほぼすべての振る舞い状態プレゼンテーションロジックをViewModelに持たせるように構築することです。もちろんViewは依然としてViewModelにバインドされますが、ViewModel側にViewに関する十分な詳細が含まれているため、Viewの機能はごく一部の例外を除きワンライナーまで簡素化されます。

Martin Fowlerの命名規則では、これはPassive View/Screenとして知られています。より一般的な呼び名はHumble Objectパターンです。

これを実現するには、本質的に、View内に存在するすべてのコントロールやウィジェットに対応するオブザーバブルなフィールド(Data Binding、Rx、LiveDataなど、実現手段は問いません)をViewModelに持たせる必要があります。

class UserViewModel(
    val repo: IUserRepository,
){

    //The actual data model is kept private to avoid unwanted tampering
    private val userState = MutableLiveData<User>()

    //Control Logic
    internal val authAttemptState = MutableLiveData<Unit>()
    internal val startAnimation = MutableLiveData<Unit>()

    //UI Binding
    internal val signInStatusText = MutableLiveData<String>()
    internal val authButtonText = MutableLiveData<String>()
    internal val satelliteDrawable = MutableLiveData<String>()

    private fun showErrorState() {
        signInStatusText.value = LOGIN_ERROR
        authButtonText.value = SIGN_IN
        satelliteDrawable.value = ANTENNA_EMPTY
    }
    //...
}

その後、Viewは依然として自分自身をViewModelに接続する必要がありますが、それに必要な関数は極めて簡単に書けるようになります。

class LoginView : Fragment() {

    private lateinit var viewModel: UserViewModel
    //...
    
    //Create and bind to ViewModel
    override fun onStart() {
        super.onStart()
        viewModel = ViewModelProviders.of(
        //...   
        ).get(UserViewModel::class.java)

        //start background anim
        (root_fragment_login.background as AnimationDrawable).startWithFade()

        setUpClickListeners()
        observeViewModel()

        viewModel.handleEvent(LoginEvent.OnStart)
    }

    private fun setUpClickListeners() {
      //...
    }

    private fun observeViewModel() {
        viewModel.signInStatusText.observe(
            viewLifecycleOwner,
            Observer {
                //"it" is the value of the MutableLiveData object, which is inferred to be a String automatically
                lbl_login_status_display.text = it
            }
        )

        viewModel.authButtonText.observe(
            viewLifecycleOwner,
            Observer {
                btn_auth_attempt.text = it
            }
        )

        viewModel.startAnimation.observe(
            viewLifecycleOwner,
            Observer {
                imv_antenna_animation.setImageResource(
                    resources.getIdentifier(ANTENNA_LOOP, "drawable", activity?.packageName)
                )
                (imv_antenna_animation.drawable as AnimationDrawable).start()
            }
        )

        viewModel.authAttemptState.observe(
            viewLifecycleOwner,
            Observer { startSignInFlow() }
        )

        viewModel.satelliteDrawable.observe(
            viewLifecycleOwner,
            Observer {
                imv_antenna_animation.setImageResource(
                    resources.getIdentifier(it, "drawable", activity?.packageName)
                )
            }
        )
    }

この例の完全なコードはこちらで公開しています。

お気づきのとおり、このViewModelはおそらく他のどこでも再利用しません。同時に、Viewは十分に「謙虚」になり(コードカバレッジに対するあなたの基準や好みにもよりますが)、非常に書きやすくなっています。

時には、ViewとViewModelの間のプレゼンテーションロジックの配分について、これら2つのアプローチのどちらにも厳密に従わない、何らかの中間的な妥協案を見つけなければならない状況に遭遇することもあるでしょう。

私はどちらか一方のアプローチを推奨するのではなく、目の前の要件に基づいて柔軟に対応することを推奨しています。

好みと要件に基づいてアーキテクチャを選ぼう

本記事の狙いは、Androidプラットフォーム(他のプラットフォームにも一部通じる話です)でMVVMスタイルのGUIアーキテクチャを構築する際に、開発者が取れる2つの異なるアプローチを見てくることでした。

実際には、この2つのアプローチの中にもさらに細かい違いがあります。

  • Viewは所有する個々のウィジェット/コントロールごとのフィールドを監視すべきか、それとも全体を再描画する単一のモデルを発行するフィールドを1つ監視すべきか?
  • PresenterやControllerのようなものを加えるだけで、ViewModelを一対一にせずに済ませつつ、ViewをHumble Objectのまま保てるかもしれない?

議論は安上がりです。私が強く勧めるのは、私のような人間に指示されることを頼りにするのではなく、コードの中でこれらのことを試して学ぶことです。

結局のところ、優れたアーキテクチャを作る要素は、次の2つの考慮事項に行き着くと思います。

第一に、好みのアーキテクチャが見つかるまで、いくつかのアプローチを実際に試してみることです。これは各スタイルで実際にアプリケーション(シンプルなもので構いません)をビルドし、何がしっくりくるかを確かめるのが最良の方法です。

第二に、好みはさておき、異なるスタイルは異なるメリットを引き換えに異なるデメリットを伴う傾向があることを理解することです。やがて、盲目的な信仰ではなく、プロジェクト要件への理解に基づいて良い選択ができるようになるはずです。

ソフトウェアアーキテクチャについてさらに学ぶ:

SNSリンク

https://www.instagram.com/rkay301/
https://www.facebook.com/wiseassblog/
https://twitter.com/wiseass301
https://wiseassblog.com/

  1. ITプロのようにWindows 11/10タスクマネージャーを使いこなす方法

    Windows 11/10/8の優れた機能のひとつが「タスクマネージャー」です。長年にわたって進化を続けてきたこのツールは、現在ではMark Russinovich氏のProcess Explorerにも匹敵するほど豊富な情報を提供してくれます。本記事では、Windows 11/10のタスクマネージャーの主な機能を紹介し、ITプロフェッショナルのように使いこなす方法をわかりやすく解説します。 Windows 11/10のタスクマネージャー タスクマネージャーを起動すると、最初は実行中のプログラムのシンプルな一覧と「タスクの終了」ボタンだけが表示されます。これは、詳細な情報を必要としない初心者ユ

  2. Google画像検索をプロ級に使いこなす方法|知っておきたい便利テクニック

    壁紙の探しものからトレンドのファッションアイテム、そしてあらゆる情報検索まで、Googleは私たちにとって欠かせない定番の検索エンジンです。検索クエリを入力すれば、わずか数秒で何千件もの結果が表示されます。「情報を調べるならGoogle一択」と言っても過言ではないでしょう。 ところで、Google画像検索では、ファイル形式や色、画像サイズなどで検索結果を絞り込めることをご存じでしょうか?高度な検索演算子の活用から類似画像検索(レバースイメージサーチ)のマスターまで、Google画像検索をプロのように使いこなすためのコツや裏ワザをたっぷりご紹介します。 それでは早速、Googleの検索ツールを