新型コロナの影響で自宅待機になってしまい、その間勉強するものとして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:公式ドキュメント
