新型コロナの影響で自宅待機になってしまい、その間勉強するものとしてSunflower
リポジトリを
勧めてもらいました。
JetPackのライブラリのうち、今回はViewModel
編です
DataBinding編、LiveData編などでも、
チラチラ出てきてはいましたが、見てみぬふりをしていました笑
尚、引用しているソースは明記しているところ以外は、基本的には全てSunflower
のリポジトリのものです。
環境
- 確認時は
Android Studio
のバージョンは3.6.2
を使用しました -
JetPack
はAndroidX
ライブラリを利用するのでCompile SDK
を28
以上にする必要があります
そもそもViewModel
ってなに?
公式の説明によると
-
ViewModel
は、ライフサイクル
を意識した方法で UI 関連のデータを保存および管理するためのクラスです -
ViewModel
クラスを使用すると、画面の回転などの設定の変更後にデータを引き継ぐことができます。
です!
ViewModel
ってなに?(詳細)
公式にはこんな感じのことが書いてありました。
-
UI コントローラで利用するデータをバンドルから復元して利用しないで済む
- アクティビティが
onSaveInstanceState()
メソッドを使用してonCreate()
のバンドルからデータを復元しなくてもよくなります。 - そもそも
この方法(onSaveInstanceState()利用)
が適しているのは少量のデータの場合だけです。 - ユーザーやビットマップのリストのようにデータの量が多くなる可能性がある場合には適していません。
- アクティビティが
画面をぐるぐるする度に、バンドルからデータをとって。。。とするのはめんどくさかったので、
それをしないで済むのは良さそうですね!
-
メモリリークの心配がない
- UI コントローラでは実行に時間がかかる非同期呼び出しを頻繁に行う必要があります。それらを管理し、破棄された後にシステムが呼び出しのクリーンアップを行ってメモリリークが発生しないようにする必要がありますが、
ViewModel
を利用した場合それをする必要がありません。
- UI コントローラでは実行に時間がかかる非同期呼び出しを頻繁に行う必要があります。それらを管理し、破棄された後にシステムが呼び出しのクリーンアップを行ってメモリリークが発生しないようにする必要がありますが、
画面のライフサイクルに合わせて、データの破棄や再取得を考えて設計・実装するのは大変ですね。。。
それをしないで済むのはこちらも良さそうです!
-
リソースの無駄がない
- この管理ではメンテナンスを何度も実施する必要があります
- 設定の変更によってオブジェクトが再作成された場合にはすでに行った呼び出しを再度行わなければならないこともあるため、リソースが無駄になります。
メモリリークしない作りにするために、冗長な処理になってしまいがちだったかもしれません。
それをしないで済むのはこちらもありがたいです!
-
UIコントローラクラスの肥大化防止とテスト効率の向上
- UI コントローラに対してデータベースやネットワークからのデータ読み込みも行うよう要求すると、クラスが肥大化することになります
- UI コントローラに過度の役割を割り当てると、アプリの作業を他のクラスに任せずに 1 つのクラスですべて処理しようとすることになり、テストも困難になります。
- ビューデータの所有権を UI コントローラのロジックから切り離すことで、複雑さが軽減され、効率性が高まります。
処理が増えてくると、Activity
やFragment
が巨大化してしまい、本当のところ実際にUIをコントロールしているところが
見えにくくなってしまう、という状況になってしまいがちだと思います。
また、処理とUIコントロールの部分が分割できるよう、設計を工夫してなんとかしていたと思いますが、
公式でサポートするViewModel
で出来るのは頼もしいですね!!!
ViewModel
クラス
その名の通り、上記のようなUIコントローラー向けにUIデータを準備するためのViewModel
ヘルパークラスが用意されています!!!
それを使えば上記の問題が解決できるはずです!
使用箇所
Sunflower
リポジトリの中ではどのようにViewModel
が使われているか、実際に見てみましょう
ViewModel
の定義
class GardenPlantingListViewModel internal constructor(
gardenPlantingRepository: GardenPlantingRepository
) : ViewModel() {
val plantAndGardenPlantings: LiveData<List<PlantAndGardenPlantings>> =
gardenPlantingRepository.getPlantedGardens()
}
-
plantAndGardenPlantings
を取得する処理は、アクティビティやフラグメントではなく、GardenPlantingListViewModel
内に割り当てられてられています。 -
ViewModel内で定義しているデータは
LiveData
となっています- こうすると更新を監視できて、非アクティブになった時に破棄できるなどメリットがあるため、一緒に使う場合が多いと思います。
- ※詳しくはLiveData編も併せて確認してください。
-
ちなみに、ここでは
plantAndGardenPlantings
の値自体の取得処理は、GardenPlantingRepository
を利用し抽象化されており、さらに外部でインスタンス化されたものをコンストラクターで渡されています。(依存性注入)- こうすることで、取得先がローカルDBからサーバーに変わったとしても、このクラスとの依存関係がないため、変更は不要であり、変更に強い設計になっています。
- 尚、依存性注入については
Dagger2
を利用して実施する場合が多いですが、Sunflowerリポジトリ
では、Dagger2
の依存性注入については、対応しない!と明言しています笑
ViewModel
利用箇所
UI コントローラー
(ここではFragment
)ではこのようにして、ViewModel
を利用していました。
class GardenFragment : Fragment() {
:
:
private val viewModel: GardenPlantingListViewModel by viewModels {
InjectorUtils.provideGardenPlantingListViewModelFactory(requireContext())
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentGardenBinding.inflate(inflater, container, false)
:
:
subscribeUi(adapter, binding)
return binding.root
}
private fun subscribeUi(adapter: GardenPlantingAdapter, binding: FragmentGardenBinding) {
viewModel.plantAndGardenPlantings.observe(viewLifecycleOwner) { result ->
binding.hasPlantings = !result.isNullOrEmpty()
adapter.submitList(result)
}
-
viewModel
を定義している部分ですが、by viewModels {ラムダ式}
となっています。- これは
Android KTX
の機能のうち、Properties Delegete
というもので、この場合は下記のパターンです。 - →ラムダ式の中に自作の
Factory Method
を書くことができ、そこで作られたインスタンスをViewModel
に与えることができます
- これは
class MyFragment : Fragment() {
val viewmodel: MYViewModel by viewmodels { myFactory }
}
ここでは、InjectorUtils#provideGardenPlantingListViewModelFactory()
メソッドを実施し、このViewModel
のインスタンスを取得しています
-
viewModel.plantAndGardenPlantings
はLiveData
のため、observe()
して変更を監視します。 - この処理は
Fragment
が作成された時にコールされる処理、onCreateView()
メソッド内で実施しています。 - 値がセットされる度に
onChange()
メソッドがコールされ、ラムダ式内の処理が実施されます。そこで、UIの更新を実施しています。
ViewModel
とFactory
メソッドについて
ViewModel
のFactory
メソッドにはandroidx.lifecycle.ViewModelProvider
を使用します。
引数無しの場合
val viewmodel: MYViewModel by viewmodels{
ViewModelProvider.NewInstanceFactory().create(MYViewModel::class.java)
}
これでOKです。(新たなクラスの定義は不要です。)
引数ありの場合(今回のパターン)
ただし、今回は引数がある場合なので、
引数があるViewModelFactory
クラスを作成しています。
こちらはViewModelFactory
クラスのFactory
メソッド(Factoryクラス
のコンストラクターメソッド)を呼び出している処理です。
Sunflower
リポジトリでは、kotlin:InjectorUtils
クラスに各ViewModel
のFactory
メソッドをまとめて記載してあります。
object InjectorUtils {
:
:
fun provideGardenPlantingListViewModelFactory(
context: Context
): GardenPlantingListViewModelFactory {
val repository = getGardenPlantingRepository(context)
return GardenPlantingListViewModelFactory(repository)
}
:
:
}
続いて、GardenPlantingListViewModel
のFactory
クラスであるkotlin:GardenPlantingListViewModelFactory
を見てみましょう
class GardenPlantingListViewModelFactory(
private val repository: GardenPlantingRepository
) : ViewModelProvider.NewInstanceFactory() {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return GardenPlantingListViewModel(repository) as T
}
}
GardenPlantingRepository
を引数として渡す形で定義されています。
Sunflower
リポジトリの他のViewModel
クラスも同じ形式となっていたので、別途作成する場合もこの形式を踏襲し、以下の部分の変更のみすればよさそうです
- クラス名(◯◯◯Factory)
-
Constructor
の引数 - オーバーライドする
create
メソッドが返す引数(新たに作成したViewModel
のクラス名)
ViewModel
とテストについて
-
ViewModel
のライフサイクルは長めに設計されています。そのため、ViewModel
のテストをしやすい設計となっています。
- 画面が無くなった(
Activity#onDestroy()
)**「後」**に、なにかやる、などもViewmodelScope.onCleared()
とかでできます。 - この図はActivityについて書かれてますが、
Fragment
などでも基本的には同じのようです
まとめ
- ViewModel は、ライフサイクルを意識した方法で UI 関連のデータを保存および管理するためのクラスで、画面の回転などの設定の変更後にデータを引き継ぐことができる。
- UIコントローラー(
Activity
,Fragment
など)とライフサイクルを共にするため、メモリリークの心配やリソースの無駄がない - UIコントローラーから
ViewModel
を切り出すことにより、UIコントローラーの肥大化を防止し、テストをしやすくすることができる -
Android KTX
のProperties Delegete
でFactory
メソッドでインスンタンスを作ることができる。 -
ViewModel
クラスのコンストラクタが、引数なし
の場合は自前のFactory
クラスを作らないでもよいが、引数あり
の場合はFactory
クラスを作る必要がある。
以上です!
参考サイト
- Sunflowerリポジトリ
- ViewModel: 公式ドキュメント
- LiveData: 公式ドキュメント
- DataBinding: 公式ドキュメント
- Android-KTX: @mangano-ito さんの記事
- Android-KTX Fragment.app:公式ドキュメント