Edited at

ステップバイステップでDaggerを使った引数ありのViewModelの初期化を理解する

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などでください :pray: