hiltViewModel()でviewModelを初期化する時の落とし穴

# 結論
viewModelをhiltViewModelで初期化すると、intentで渡したパラメータをSavedStateHandleで受け取れない！！！！！

※間違ってたらコメント頂けると嬉しいです！🙇‍♂️

# 背景
viewModelを初期化したと同時にSavedStateHandle経由で値を取得したいことがままあると思います。
例えば、下記のようにinitした時にActivityからintent経由でもらった値を使用したい時などです。

**ViewModel**
```kotlin
@HiltViewModel
class HogeViewModel @Inject constructor(
    savedStateHandle: SavedStateHandle,
    private val hogeRepository: HogeRepository,
) : ViewModel() {

    object Contract {
        val key: String = "params"
    }

    private val hogeParams = savedStateHandle.get<HogeParameter>(Contract.key) ?: HogeParameter()

    init {
        Log.d("Hoge", "hogeParams: $hogeParams")
    }
}
```

**Intentを発行するActivity**
```kotlin
@AndroidEntryPoint
class HogeActivity : AppCompatActivity() {

    companion object {
        fun createIntent(
            context: Context,
            hogeParameter: HogeParameter,
        ): Intent {
            return Intent(context, HogeActivity::class.java).apply {
                putExtras(
                    bundleOf(
                        HogeViewModel.Contract.key to hogeParameter,
                    )
                )
            }
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            HogeTheme {
                HogeScreen()
            }
        }
    }
}
```

**viewModelをhiltViewModel()で初期化するScreen**
```kotlin
@Composable
fun HogeScreen(
    modifier: Modifier = Modifier,
    hogeViewModel: HogeViewModel = hiltViewModel(),
) {
    // 省略
}
```

一見、問題なくviewModelでHogeParameterを取得できそうな実装ですが、残念ながらHogeViewModel内のLogはHogeParameterの中身に関係なく、nullと表示されてしまいます。

ちなみにhiltのバージョンは"2.53.1"です。


# 対応方法
下記のようにhiltViewModel()ではなく、composeもしくはComponentActivityのviewModelで初期化すると解決します。

```kotlin
@AndroidEntryPoint
class HogeActivity : AppCompatActivity() {

// 省略

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            // composeで初期化
            val viewModel = viewModel<HogeViewModel>()
            
            // activityで初期化する場合はこちら
            // val viewModel : HogeViewModel by viewModels()
            
            HogeTheme {
                HogeScreen(hogeViewModel = viewModel)
            }
        }
    }
}
```

# なぜ？
hiltViewModel()とcomposeのviewModel<>()では、**使用するviewModelFactoryが異なっているため**でした。それぞれコードを追っていきましょう。


**composeのviewModel<>()の場合**
```kotlin
@Suppress("MissingJvmstatic")
@Composable
public inline fun <reified VM : ViewModel> viewModel(
    viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
        "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
    },
    key: String? = null,
    factory: ViewModelProvider.Factory? = null,
    extras: CreationExtras = if (viewModelStoreOwner is HasDefaultViewModelProviderFactory) {
        viewModelStoreOwner.defaultViewModelCreationExtras
    } else {
        CreationExtras.Empty
    }
): VM = viewModel(VM::class, viewModelStoreOwner, key, factory, extras)
```
ポイントはfactoryがnullで渡っていることです。

**hiltViewModel()の場合**
```kotlin
@Composable
inline fun <reified VM : ViewModel> hiltViewModel(
    viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
        "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
    },
    key: String? = null
): VM {
    val factory = createHiltViewModelFactory(viewModelStoreOwner)
    return viewModel(viewModelStoreOwner, key, factory = factory)
}
```
hiltViewModelの場合は、どうやら独自でfactoryを生成していることがわかります。
両者のコードを追っていくと、どちらも下記のコードに行き着きます。

```kotlin
internal fun <VM : ViewModel> ViewModelStoreOwner.get(
    modelClass: KClass<VM>,
    key: String? = null,
    factory: ViewModelProvider.Factory? = null,
    extras: CreationExtras = if (this is HasDefaultViewModelProviderFactory) {
        this.defaultViewModelCreationExtras
    } else {
        CreationExtras.Empty
    }
): VM {
    val provider = if (factory != null) {
        ViewModelProvider.create(this.viewModelStore, factory, extras) // hiltViewModel()だとここを通る
    } else if (this is HasDefaultViewModelProviderFactory) {
        ViewModelProvider.create(this.viewModelStore, this.defaultViewModelProviderFactory, extras) // composeのviewModel<>()だとここを通る
    } else {
        ViewModelProvider.create(this)
    }
    return if (key != null) {
        provider[key, modelClass]
    } else {
        provider[modelClass]
    }
}
```

hiltViewModelの場合は、独自でfactoryを生成しているため、`ViewModelProvider.create(this.viewModelStore, factory, extras)`を返します。

一方で、composeのviewModel<>()の場合は、factoryがnullのため、`ViewModelProvider.create(this.viewModelStore, this.defaultViewModelProviderFactory, extras)`を返します。

factoryがnullの場合には`defaultViewModelProviderFactory`を使用しているようです。
中身はインターフェースになっていて、実行部分を見ると、`@AndroidEntoryPoint`を設定したActivityのジェネレータ？内のデフォルトのviewModelFactoryを返すメソッドに辿り着きました。
```kotlin
  @Override
  public ViewModelProvider.Factory getDefaultViewModelProviderFactory() {
    return DefaultViewModelFactories.getActivityFactory(this, super.getDefaultViewModelProviderFactory());
  }
```

さらに`super.getDefaultViewModelProviderFactory()`の中身を見ると、
```kotlin
override val defaultViewModelProviderFactory: ViewModelProvider.Factory by lazy {
    SavedStateViewModelFactory(
        application,
        this,
        if (intent != null) intent.extras else null
    )
}
```

viewModelFactory生成時にintentの中身を渡していました。
だから、SavedStateHadleからintentの中身を取得できていたのですね。ふむふむ納得。

では、本題のhiltViewModel()のviewModelFactoryはどうなのでしょうか？
```kotlin
@Composable
inline fun <reified VM : ViewModel> hiltViewModel(
    viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
        "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
    },
    key: String? = null
): VM {
    val factory = createHiltViewModelFactory(viewModelStoreOwner)
    return viewModel(viewModelStoreOwner, key, factory = factory)
}
```
再度登場したこちらのコードから、今度は`createHiltViewModelFactory`の中身を見ていきましょう。

```kotlin
@Composable
@PublishedApi
internal fun createHiltViewModelFactory(
    viewModelStoreOwner: ViewModelStoreOwner
): ViewModelProvider.Factory? = if (viewModelStoreOwner is HasDefaultViewModelProviderFactory) {
    HiltViewModelFactory(
        context = LocalContext.current,
        delegateFactory = viewModelStoreOwner.defaultViewModelProviderFactory
    )
} else {
    // Use the default factory provided by the ViewModelStoreOwner
    // and assume it is an @AndroidEntryPoint annotated fragment or activity
    null
}
```
デバックするとわかるのですが、hiltViewModel()でviewModelを初期化すると、`HiltViewModelFactory`を返すようになっています。

そしてまた登場しました。`defaultViewModelProviderFactory`です。

・・・あれ？

defaultViewModelProviderFactory使ってfactory生成しているなら、**このまま`delegateFactory`を使えば**composeのviewModel<>()と同じ結果になりそうだけど。。。

さらに`HiltViewModelFactory`の中身コードを追っていくと、Hilt内でfactoryを生成している下記のコードに辿り着くと思います。
```kotlin
public final class HiltViewModelFactory implements ViewModelProvider.Factory {

// 省略

public HiltViewModelFactory(
      @NonNull Map<Class<?>, Boolean> hiltViewModelKeys,
      @NonNull ViewModelProvider.Factory delegateFactory,
      @NonNull ViewModelComponentBuilder viewModelComponentBuilder) {
    this.hiltViewModelKeys = hiltViewModelKeys;
    this.delegateFactory = delegateFactory;
    this.hiltViewModelFactory =
        new ViewModelProvider.Factory() {
          @NonNull
          @Override
          public <T extends ViewModel> T create(
              @NonNull Class<T> modelClass, @NonNull CreationExtras extras) {
            RetainedLifecycleImpl lifecycle = new RetainedLifecycleImpl();
            ViewModelComponent component =
                viewModelComponentBuilder
                    .savedStateHandle(createSavedStateHandle(extras))
                    .viewModelLifecycle(lifecycle)
                    .build();
            T viewModel = createViewModel(component, modelClass, extras);
            viewModel.addCloseable(lifecycle::dispatchOnCleared);
            return viewModel;
          }

    // 省略
```
どうやらコンストラクタ内で`delegateFactory`と`hiltViewModelFactory`の二種類のfactoryが作られていることがわかりました。

そしてここで唐突に答え合わせです。
このクラスはFactoryをimplementsしているため、createをオーバーライドしています。
中身を見ると、
```kotlin
  @NonNull
  @Override
  public <T extends ViewModel> T create(
      @NonNull Class<T> modelClass, @NonNull CreationExtras extras) {
    if (hiltViewModelKeys.containsKey(modelClass)) {
      return hiltViewModelFactory.create(modelClass, extras);
    } else {
      return delegateFactory.create(modelClass, extras);
    }
  }
```

**hiltViewModelKeysがmodelClassを含んでいるとき、hiltViewModelはdelegateFactoryを返しません。**
つまり、先ほど登場したintentで渡した値をSavedStateHandleに渡してくれる下記のfactoryを使っていないのです。

```kotlin
override val defaultViewModelProviderFactory: ViewModelProvider.Factory by lazy {
    SavedStateViewModelFactory(
        application,
        this,
        if (intent != null) intent.extras else null
    )
}
```

長かったですが、ここまででhiltViewModel()とcomposeのviewModel<>()では、(条件次第ではあるが)viewModelを初期化する時に使用するfactoryが異なっていることがわかりました。

しかしながら、ここまでだと使ってるfactoryが違うことまでは説明できますが、`hiltViewModelFactory`でintentの中身が取得できていない理由にはなりません。

実際、`hiltViewModelFactory`の方でも、createする時に`.savedStateHandle(createSavedStateHandle(extras))`しているため、SavedStateHadle自体は渡っていそうです。

つまり、憶測にはなってしまいますが、`hiltViewModelFactory`ではSavedStateHadle自体は渡しているものの、その中身にはintentが含まれていないということなのだと思います。

(何回も言いますが、間違っていたら教えてください！)



# 感想
もう少し調べた方が良さそうなのですが、ハマりそうなので一旦ここらでアプトプットとさせてください。

初めてQuitaの記事を投稿してみました。どなたかのお力になれたら嬉しいです。
ここまで見てくれた人、嬉しいです。ありがとうございます。