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

ViewModel周りの作り方、ちょっともやもやしていたので、改めてまとめてみました。

# ViewModelの作り方

何も工夫せずにViewModelを取得すると以下のようになります



```kotlin
class SampleViewModel() : ViewModel()

// (Fragment内でのコード)
ViewModelProviders.of(this).get(SampleViewModel::class.java)
```

これは内部的に以下と同じです。 Fragmentから`viewModelStore` というのを取得して、それと `AndroidViewModelFactory` を渡して作ります。

(Fragment内でのコード)

```kotlin
val factory = ViewModelProvider
        .AndroidViewModelFactory
        .getInstance(requireActivity().application)
ViewModelProvider(this.viewModelStore, factory).get(SampleViewModel::class.java)
```

つまり、内部的なロジックだと以下と同じで、`fragment.viewModelStore`でキャッシュしていて、`AndroidViewModelFactory`で作っているだけです。

(Fragment内でのコード)

```kotlin
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をオーバーライドして実装すると以下のようになります。

```kotlin
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を使いたいですよね？


```kotlin
class SampleViewModel @Inject constructor(val repository: SessionRepository) : ViewModel()
```

Fragmentに直接以下のように書くとViewModelStoreで管理されないインスタンスができてしまいます。

☓

```kotlin
// ☓ ViewModelStoreに保存されていないインスタンス！！
@Inject lateinit var viewModel: SampleViewModel 
```

そこでDaggerのProviderというgetを呼ぶまでインスタンスが作られないものを利用します。
そして、Factoryで返させることで、ViewModelStoreで管理されるようにします。

○

```kotlin
@Inject lateinit var viewModelFactory: Provider<SampleViewModel>
```


```kotlin
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で渡して、カスタムした引数を渡せます。

```kotlin
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を使ったりなど、同じことをやっているだけです。

```kotlin
class SimpleViewModel() : ViewModel()
...
// Fragment内
val viewModel: SimpleViewModel by viewModels()
```

`viewModels`はFactoryを渡せるので、以下のように書くことができます。


```kotlin
// 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を定義すると以下のようになり、かなり実用的にかけるようになりました。


```kotlin
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: