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

Android VM LiveData Transformation

MVVM を考える時、ViewModel はデータをモデルから取ってくるというイメージがありますが、実際に ViewModel の特性を生かすためにはどう実装するべきかを考えました。

そこで今回は TransformationsLiveData builder などの Architecture 関連クラスを使用して VM を実装した TwitterAPI を叩くサンプルアプリを紹介します。

MVVM のデータフローと ViewModel

MVVM では View(プレゼンテーション)と Model(データ)の間を取り持つ ViewModel の実装方法はとても重要です。

ViewModelView のアクションを拾ったり、単純にデータを Model に問い合わせるだけの実装でもそれなりに機能しますが、内部的な LiveData の初期化・更新などのマネジメントに試行錯誤することがあります。

また、ViewModel の「ライフサイクルオーナーがいる間は生き続ける」という性質を使って、savedInstanceState の代わりのようにデータホルダーとしてのみ使用している例もあると思います。これにより、複数のフラグメントから同じ ViewModel のデータを参照できるというのも魅力です。

ViewModel のデータ

ちなみにこの性質について、画面回転などの構成変化を受けて ActivityonDestroy が呼ばれているのに、ViewModel のデータがクリアされていないことが疑問に思えます。これは、onDestroyisFinishingtrue の場合( OS に消された場合)のみ完全に破棄される仕様だということです。

では、今回サンプルアプリで採用した構成を見ていきます。

Tweets 検索アプリ

基本設計

View
↑↓ ( LiveData<UIData>
ViewModel (Translate or Filter)
↑↓ ( ModelData
Model

設計は上記のように、Model( Repository )から受け取った DataViewModel によって View が必要な形に変換して渡すという形で、データは LiveData<ModelData> で渡されます。

構成

  • OAuth2 で BearerToken を取得する InitializeFragment
  • 実際にキーワードで tweets を検索する MainActivityFragment

Token 取得フロー

Search API を叩くのに必要な BearerToken を取得します。本来は xml などのファイルに記述して、ビルド環境で読み込みを分けるかバックエンドに置くのが正攻法だと思いますが、丁度いいサンプルになるので手入力にしました。

Token は基本的に一度初期化すればずっと使えるので、Preference に保存しておくことにします。

アプリ起動時に Preference に値がなければ、InitializeFragment を開いて Consumer キーのペアを入力してもらい、OAuth のエンドポイントを叩いて成功したら値を保存し、InitializeFragment が閉じて検索画面が表示されます。

oauth app top

Flow

InitializeFragment - fetchToken()
↑↓ observe: LiveData<NetworkState>
MainViewModel - getBearerToken()
↑↓ Coroutine: TwitterBearerTokenResult
TwitterBearerTokenRepository - getToken()

ポイントは observe しているデータの型が Token ではなく NetworkState だという所です。

使用クラス

TwitterBearerTokenResultTokenNetworkState をラップするクラスです。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 が再生成されても ViewModeltweet のリストを保持しておくということも考えた上で、検索の関数での observe ではなく、onCreate の時点で observer をセットし、ViewModelLiveData を逐一監視することにします。

tweets feed

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

この実装は間違いではありませんが、LiveDataimmutableexpose する為のコードが冗長な気がします。特に search() の内部で変換する必要がないのに MediatorLiveData を生成しているのも無駄に感じられます。

expose した LiveData の値を、search() が呼ばれる度に更新するいい方法はないのでしょうか。

Transformations で解決

そこで登場するのが、TransformationsAPI リンク)です。

このクラスは map()switchMap() という関数をもちますが、基本的にはどちらも同じなので、ソースを見て解説していきます。

Transformations.java

どちらも指定の型の LiveData を返す関数です。第一引数にはトリガーとなる LiveData をとり、第二引数にはそのトリガーの返り値を受け取る関数が入ります。この関数の返り値が map() の場合は値を返し、switchMap()LiveData を返す必要があります。

先程の MediatorLiveData の実装と似ていると思うかもしれませんが、実際に返しているのは MediatorLiveData ですので2段階で機能するということ以外はほとんど同じです。以下は switchMap() の解説です。( validation は無視)

switchMap Flow

  1. まず MediatorLiveData(result) を作成し、これを返します
  2. そしてトリガーの LiveData(trigger)addSource します
  3. トリガーの値が変更されて通知が来たら、第二引数の関数へその値を渡して invoke し、それが返す LiveData(mSource)addSource します
  4. その LiveData(mSource) の値が通知されたら MediatorLiveData(result) の値を更新して、通知します

これを踏まえて MainViewModel を書き換えます。

検索ロジック - MainViewModel.kt

これでスッキリしました。まずトリガーとして、検索ワードを持つ MutableLiveData(searchWords) を作成します。Viewexpose するのは、tweetDataResults という LiveData で、この実体は Transformations.switchMap(searchWords) が返してくる MediatorLiveData です。View はこれを observe しておきます。

検索アクションが発生したら search() を呼んで、トリガーの値を更新します。これが switchMap に通知され、ラムダが流れ、まずは LOADING の状態が通知されます。その後レポジトリが tweets かエラーを返して来たところで結果が通知され、UI が更新されます。

呼び出し側 - MainActivityFragment.kt

検索は searchWords の値を更新してトリガーするだけになったので、だいぶスッキリしました。

TL;DR

LiveData builderTransformations を使うことによって、ViewModel の特性を生かす実装ができました。

もっと複雑で多種多様なデータを Model が返してきても、View に通知されるのは ViewModel によって整形されたデータだけにすることができるのが MVVM 設計の美しさだと思います。

このプロジェクトのレポジトリはこちらです。

参考

COPYRIGHT © 2023 Kohei Ando