ViewModel周りの作り方、ちょっともやもやしていたので、改めてまとめてみました。
ViewModelの作り方
何も工夫せずにViewModelを取得すると以下のようになります
class SampleViewModel() : ViewModel()
// (Fragment内でのコード)
ViewModelProviders.of(this).get(SampleViewModel::class.java)
これは内部的に以下と同じです。 FragmentからviewModelStore
というのを取得して、それと AndroidViewModelFactory
を渡して作ります。
(Fragment内でのコード)
val factory = ViewModelProvider
.AndroidViewModelFactory
.getInstance(requireActivity().application)
ViewModelProvider(this.viewModelStore, factory).get(SampleViewModel::class.java)
つまり、内部的なロジックだと以下と同じで、fragment.viewModelStore
でキャッシュしていて、AndroidViewModelFactory
で作っているだけです。
(Fragment内でのコード)
private fun createViewModel(): SampleViewModel {
val factory = ViewModelProvider
.AndroidViewModelFactory
.getInstance(requireActivity().application)
// ViewModelStore内からViewModelを取得
val existsViewModel: ViewModel? = this
.viewModelStore
.get(SampleViewModel::class.java.canonicalName)
// viewModelStore内にあればそれを返す
if (existsViewModel != null) {
return existsViewModel as SampleViewModel
}
// 無ければAndroidViewModelFactoryで作る
return factory.create(SampleViewModel::class.java)
}
引数ありのViewModelの作り方
このFactoryを変えることができれば、引数ありで初期化できそうです。
AndroidViewModelFactoryをオーバーライドして実装すると以下のようになります。
class SampleViewModel(val sampleParameter: String) : ViewModel()
...
// Fragment内のコード
val factory = object : ViewModelProvider
.AndroidViewModelFactory(requireActivity().application) {
// **AndroidViewModelFactoryをオーバーライドしてカスタムしたcreateロジックを入れる**
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass == SampleViewModel::class.java) {
// 普通にnewする!
return SampleViewModel("sample parameter") as T
}
return super.create(modelClass)
}
}
println(
ViewModelProvider(this.viewModelStore, factory)
.get(SampleViewModel::class.java).sampleParameter
)
I/System.out: sample parameter
DaggerでのViewModelの作り方
さて、初期化するときにRepositoryとか渡したいので、みなさんはDaggerを使いたいですよね?
class SampleViewModel @Inject constructor(val repository: SessionRepository) : ViewModel()
Fragmentに直接以下のように書くとViewModelStoreで管理されないインスタンスができてしまいます。
☓
// ☓ ViewModelStoreに保存されていないインスタンス!!
@Inject lateinit var viewModel: SampleViewModel
そこでDaggerのProviderというgetを呼ぶまでインスタンスが作られないものを利用します。
そして、Factoryで返させることで、ViewModelStoreで管理されるようにします。
○
@Inject lateinit var viewModelFactory: Provider<SampleViewModel>
val factory = object : ViewModelProvider
.AndroidViewModelFactory(requireActivity().application) {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass == SampleViewModel::class.java) {
return viewModelFactory.get() as T
}
return super.create(modelClass)
}
}
ViewModelProvider(this.viewModelStore, factory).get(SampleViewModel::class.java)
Daggerでのカスタムした引数をもつViewModelの使い方
例えばページの詳細画面などではpageIdなどを渡して、ViewModelを作りたくなりますよね?
AssistedInjectというライブラリを使うとDaggerでRepositoryとかをinjectしつつ、カスタムした引数をFactoryで渡して、カスタムした引数を渡せます。
class SampleViewModel @AssistedInject constructor(
// カスタムした引数を使いたいものに@Assistedをつける!
@Assisted val pageId: String,
val repository: SessionRepository
) : ViewModel() {
@AssistedInject.Factory
interface Factory {
fun create(pageId: String): SampleViewModel
}
}
@Inject lateinit var viewModelFactory: SampleViewModel.Factory
val factory = object : ViewModelProvider
.AndroidViewModelFactory(requireActivity().application) {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass == SampleViewModel::class.java) {
// 引数を渡して作る!
return viewModelFactory.create("sample_page") as T
}
return super.create(modelClass)
}
}
ViewModelProvider(this.viewModelStore, factory).get(SampleViewModel::class.java)
実用的なコードに落とし込む
Android Jetpackのfragment-ktxを使うと viewModels()
でViewModelを初期化できます。内部的には普通にFragmentからviewModelStoreを使ったりなど、同じことをやっているだけです。
class SimpleViewModel() : ViewModel()
...
// Fragment内
val viewModel: SimpleViewModel by viewModels()
viewModels
はFactoryを渡せるので、以下のように書くことができます。
// Fragment内
val viewModel:SampleViewModel by viewModels {
object : ViewModelProvider.AndroidViewModelFactory(this.requireActivity().application) {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
@Suppress("UNCHECKED_CAST")
return viewModelFactory.create("sample_page") as T
}
}
}
自分で viewModels
を使ったextension functionを定義すると以下のようになり、かなり実用的にかけるようになりました。
class SampleViewModel @AssistedInject constructor(
@Assisted val pageId: String,
val repository: SessionRepository
) : ViewModel() {
@AssistedInject.Factory
interface Factory {
fun create(pageId: String): SampleViewModel
}
}
// Fragment内
@Inject lateinit var viewModelFactory: SampleViewModel.Factory
val viewModel: SampleViewModel by assistedViewModels {
viewModelFactory.create("sample_page")
}
inline fun <reified T : ViewModel> Fragment.assistedViewModels(
crossinline body: () -> T
): Lazy<T> {
return viewModels {
object : ViewModelProvider.AndroidViewModelFactory(this.requireActivity().application) {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
@Suppress("UNCHECKED_CAST")
return body() as T
}
}
}
}
まとめ
自分はなんとなく完全に理解できました。
こうしたらわかりやすいとか、ツッコミなどあれがコメントやTwitterなどでください