Android で LiveData Builder と Transformations で ViewModel を効果的に使う

MVVM を考える時、ViewModel
はデータをモデルから取ってくるというイメージがありますが、実際に ViewModel
の特性を生かすためにはどう実装するべきかを考えました。
そこで今回は Transformations
や LiveData builder
などの Architecture
関連クラスを使用して VM を実装した TwitterAPI を叩くサンプルアプリを紹介します。
MVVM のデータフローと ViewModel
MVVM では View
(プレゼンテーション)と Model
(データ)の間を取り持つ ViewModel
の実装方法はとても重要です。
ViewModel
は View
のアクションを拾ったり、単純にデータを Model
に問い合わせるだけの実装でもそれなりに機能しますが、内部的な LiveData
の初期化・更新などのマネジメントに試行錯誤することがあります。
また、ViewModel
の「ライフサイクルオーナーがいる間は生き続ける」という性質を使って、savedInstanceState
の代わりのようにデータホルダーとしてのみ使用している例もあると思います。これにより、複数のフラグメントから同じ ViewModel
のデータを参照できるというのも魅力です。
ViewModel のデータ
ちなみにこの性質について、画面回転などの構成変化を受けて Activity
で onDestroy
が呼ばれているのに、ViewModel
のデータがクリアされていないことが疑問に思えます。これは、onDestroy
で isFinishing
が true
の場合( OS に消された場合)のみ完全に破棄される仕様だということです。
では、今回サンプルアプリで採用した構成を見ていきます。
Tweets 検索アプリ
基本設計
View
↑↓ ( LiveData<UIData>
)
ViewModel
(Translate or Filter)
↑↓ ( ModelData
)
Model
設計は上記のように、Model
( Repository )から受け取った Data
を ViewModel
によって View
が必要な形に変換して渡すという形で、データは LiveData<ModelData>
で渡されます。
構成
- OAuth2 で
BearerToken
を取得するInitializeFragment
- 実際にキーワードで
tweets
を検索するMainActivityFragment
Token 取得フロー
Search API を叩くのに必要な BearerToken
を取得します。本来は xml などのファイルに記述して、ビルド環境で読み込みを分けるかバックエンドに置くのが正攻法だと思いますが、丁度いいサンプルになるので手入力にしました。
Token は基本的に一度初期化すればずっと使えるので、Preference
に保存しておくことにします。
アプリ起動時に Preference
に値がなければ、InitializeFragment
を開いて Consumer キーのペアを入力してもらい、OAuth のエンドポイントを叩いて成功したら値を保存し、InitializeFragment
が閉じて検索画面が表示されます。

Flow
InitializeFragment
- fetchToken()
↑↓ observe: LiveData<NetworkState>
MainViewModel
- getBearerToken()
↑↓ Coroutine: TwitterBearerTokenResult
TwitterBearerTokenRepository
- getToken()
ポイントは observe
しているデータの型が Token
ではなく NetworkState
だという所です。
使用クラス
TwitterBearerTokenResult
は Token
と NetworkState
をラップするクラスです。NetworkState
は主に Observe
している View
が状況を見てプログレスを出したり、結果を出したり、エラー画面を出したりするのに使います。
View にトークンは必要ない
Repository は HTTP リクエストを投げて Token
を取ってくるのが仕事で、リクエストの状態を持っているので NetworkState
も管理します。しかしこれをそのまま View
に渡すとなると、表示する必要のない Token
まで渡されてしまいます。View
がそれを Preference
に書き込むというのも、仕事違いです。View
が知りたいのは Token
の取得が成功したかだけです。そこで、ViewModel
に Repository の返すデータから NetworkState
のみを View
に渡させて、Token
の保存もやってもらいます。
LiveData Builder
こ こで便利なのが、LiveData builder
です。レポジトリなどの Suspend Function
を指定の CoroutineScope
でコールし、その状態・結果を LiveData
して返すことができます。ここで渡されるコールバックは、この LiveData への Active Observer が存在した時点で発火されます。
UI から呼ばれる関数 - MainViewModel.kt
この例ではまず LOADING
を発火し、UI
がロード画面になっている間にレポジトリが Token
を取得するリクエストを Coroutine
で投げ、その結果を元に NetworkState
を更新します。ここでのポイントは Success
なら Token
を保存し、Error
ならログを吐きますが、いずれにせよ NetworkState
のみを抽出して view
に通知するということです。
呼び出し側 - InitializeFragment.kt
これによって、View
は成功か失敗のみを見て UI を変えるという処理が実現できました。
Tweets 取得フロー
続いて、Tweets の検索 API からデータを取得し、それを RecyclerView
で適当に表示するという処理を実装していきます。
今回は画面回転などで Fragment
が再生成されても ViewModel
が tweet
のリストを保持しておくということも考えた上で、検索の関数での observe
ではなく、onCreate
の時点で observer
をセットし、ViewModel
の LiveData
を逐一監視することにします。

Flow
MainActivityFragment
- fetchTweets()
↑↓ observe: LiveData<TweetDataResult>
MainViewModel
- search()
↑↓ Coroutine: TweetDataResult
TwitterBearerTokenRepository
- getToken()
今回はデータの型が共通ですが、検索によって値を更新する、再生成時に値を保持しておくというところが問題になります。
使用データクラス
MutableLiveData と MediatorLiveData を使った場合
思いつく実装として、MutableLiveData(_tweets)
を保持し、LiveData(tweets)
として expose
して View
がそれを observe
し、検索などが走った場合に Repository
を呼んで、その値を MediatorLiveData
で受け取り、MutableLiveData(_tweets)
の値を更新することで View
に通知するということが考えられます。
検索関数 - MainViewModel.kt
この実装は間違いではありませんが、LiveData
を immutable
で expose
する為のコードが冗長な気がします。特に search()
の内部で変換する必要がないのに MediatorLiveData
を生成しているのも無駄に感じられます。
expose
した LiveData
の値を、search()
が呼ばれる度に更新するいい方法はないのでしょうか。
Transformations で解決
そこで登場するのが、Transformations
( API リンク)です。
このクラスは map()
と switchMap()
という関数をもちますが、基本的にはどちらも同じなので、ソースを見て解説していきます。
Transformations.java
どちらも指定の型の LiveData
を返す関数です。第一引数にはトリガーとなる LiveData
をとり、第二引数にはそのトリガーの返り値を受け取る関数が入ります。この関数の返り値が map()
の場合は値を返し、switchMap()
は LiveData
を返す必要があります。
先程の MediatorLiveData
の実装と似ていると思うかもしれませんが、実際に返しているのは MediatorLiveData
ですので2段階で機能するということ以外はほとんど同じです。以下は switchMap()
の解説です。( validation
は無視)
switchMap Flow
- まず
MediatorLiveData(result)
を作成し、これを返します - そしてトリガーの
LiveData(trigger)
をaddSource
します - トリガーの値が変更されて通知が来たら、第二引数の関数へその値を渡して
invoke
し、それが返すLiveData(mSource)
をaddSource
します - その
LiveData(mSource)
の値が通知されたらMediatorLiveData(result)
の値を更新して、通知 します
これを踏まえて MainViewModel
を書き換えます。
検索ロジック - MainViewModel.kt
これでスッキリしました。まずトリガーとして、検索ワードを持つ MutableLiveData(searchWords)
を作成します。View
に expose
するのは、tweetDataResults
という LiveData
で、この実体は Transformations.switchMap(searchWords)
が返してくる MediatorLiveData
です。View
はこれを observe
しておきます。
検索アクションが発生したら search()
を呼んで、トリガーの値を更新します。これが switchMap
に通知され、ラムダが流れ、まずは LOADING
の状態が通知されます。その後レポジトリが tweets
かエラーを返して来たところで結果が通知され、UI が更新されます。
呼び出し側 - MainActivityFragment.kt
検索は searchWords
の値を更新してトリガーするだけになったので、だいぶスッキリしました。
TL;DR
LiveData builder
と Transformations
を使うことによって、ViewModel
の特性を生かす実装ができました。
もっと複雑で多種多様なデータを Model
が返してきても、View
に通知されるのは ViewModel
によって整形されたデータだけにすることができるのが MVVM 設計の美しさだ と思います。
このプロジェクトのレポジトリはこちらです。