Jetpack ComposeでViewModelに任意のパラメータを渡したい

この記事は[ZOZO Advent Calendar 2023 シリーズ 3](https://qiita.com/advent-calendar/2023/zozo)の1日目の記事です。

# 概要

Android Architecture Componentsの[ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel?hl=ja)を利用する機会は多いかと思います。[Hilt](https://developer.android.com/training/dependency-injection/hilt-android?hl=ja)と一緒に使用すると次のようになります。

```kotlin
@HiltViewModel
class SampleViewModel @Inject constructor() : ViewModel() {
    // ViewModelの実装
}
```

この時、次のようにコンストラクタにパラメータを持たせ、ViewModelのインスタンス作成時に任意の値を与えたい場合が往々としてあります。

```kotlin
@HiltViewModel
class SampleViewModel @Inject constructor(
    val name: String,
) : ViewModel() {
    // ViewModelの実装
}
```

# InitializerViewModelFactoryによるパラメータの受け渡し

ViewModelProvider.Factoryを実装する方法がありますが面倒です。[InitializerViewModelFactory](https://developer.android.com/reference/androidx/lifecycle/viewmodel/InitializerViewModelFactoryKt)を使うと簡単に書けます。[viewModelFactory](https://developer.android.com/reference/androidx/lifecycle/viewmodel/InitializerViewModelFactoryKt#viewModelFactory(kotlin.Function1))や[initializer](https://developer.android.com/reference/androidx/lifecycle/viewmodel/InitializerViewModelFactoryKt#(androidx.lifecycle.viewmodel.InitializerViewModelFactoryBuilder).initializer(kotlin.Function1))といったDSLでViewModelFactoryを構築することが可能です。

```kotlin
@Composable
fun SampleScreen() {
    val viewModel: SampleViewModel = viewModel(
        factory = viewModelFactory {
            initializer {
                SampleViewModel(
                    name = "horie1024",
                )
            }
        },
    )
}
```

さらに、[viewModel](https://developer.android.com/reference/kotlin/androidx/lifecycle/viewmodel/compose/package-summary#viewModel(androidx.lifecycle.ViewModelStoreOwner,kotlin.String,kotlin.Function1))はinitializerを受け取れるので、より簡潔に書けます。

```kotlin
@Composable
fun SampleScreen() {
    val viewModel: SampleViewModel = viewModel {
        SampleViewModel(
            name = "horie1024",
        )
    }
}
```

# HiltによるDIとの共存

ViewModelが例えばSampleRepositoryに依存している場合を考えます。

```kotlin
@HiltViewModel
class SampleViewModel @Inject constructor(
    val name: String,
    val repository: SampleRepository
) : ViewModel() {
    // ViewModelの実装
}
```

SampleRepositoryは次のような形で、SampleViewModelにはSampleRepositoryの実装SampleRepositoryImplがHiltによってDIされます。

```kotlin
interface SampleRepository {}
class SampleRepositoryImpl @Inject constructor(): SampleRepository {}
```

この場合にInitializerViewModelFactoryでSampleViewModelのインスタンスを作ろうとすると、SampleRepositoryImplのインスタンスを自分で用意する必要があります。

```kotlin
@Composable
fun SampleScreen() {
    val viewModel: SampleViewModel = viewModel {
        SampleViewModel(
            name = "horie1024",
            repository = SampleRepositoryImpl()
        )
    }
}
```

nameには自分で任意の値を渡したいですが、SampleRepositoryのインスタンスはHitlに用意してもらえると嬉しいです。

## SavedStateHandleでのパラメータの受け渡し

AssitedInjectを利用する方法もあるのですが、今回はSavedStateHandleを介してViewModelにパラメータを渡すことにしました。

viewModelでViewModelのインスタンスを作成する際に`extras`を指定できるのでここで[MutableCreationExtras](https://developer.android.com/reference/androidx/lifecycle/viewmodel/MutableCreationExtras)を使い指定したパラメータをセットします。MutableCreationExtrasには`defaultViewModelCreationExtras`を渡します。

[viewModel Composableの実装](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:lifecycle/lifecycle-viewmodel-compose/src/main/java/androidx/lifecycle/viewmodel/compose/ViewModel.kt;l=80)でも`defaultViewModelCreationExtras`を取得してextrasにセットしています。

```kotlin
@Composable
fun SampleScreen() {
    val owner = LocalViewModelStoreOwner.current
    val defaultViewModelCreationExtras =
        (owner as? HasDefaultViewModelProviderFactory)?.defaultViewModelCreationExtras
            ?: CreationExtras.Empty
    val viewModel: SampleViewModel = viewModel(
        extras = MutableCreationExtras(defaultViewModelCreationExtras).apply {
            set(DEFAULT_ARGS_KEY, bundleOf(NAME_KEY to "horie1024"))
        },
    )
}
```

これで次のようにsavedStateHandleから値を取得できます。

```kotlin
@HiltViewModel
class SampleViewModel @Inject constructor(
    savedStateHandle: SavedStateHandle,
    val repository: SampleRepository
) : ViewModel() {
    
    val name = savedStateHandle.get<String>(NAME_KEY) 

    // ViewModelの実装

    companion object {
        const val NAME_KEY = "name"
    }
}
```


## HiltViewModelExtensions.withCreationCallback
[Daggerの次のリリースで導入される](https://github.com/google/dagger/issues/2287#issuecomment-1804752590)ようですが、`@HiltViewMmodel`と`@AssistedInject`を統合して利用できるようになるそうです[^1]。

[^1]:12/2追記: リリースされました。https://github.com/google/dagger/releases/tag/dagger-2.49

SampleViewModelを例にすると実装は次のようになります。　Composeからも`hiltViewModel`を使って利用できるようになりそうなので楽しみです。

2024/2/6追記: Composeで `hiltViewModel` からの利用もできるようになりました https://qiita.com/nkshigeru/items/54a07ba1ce03bf24716f

```kotlin
@HiltViewModel(assistedFactory = SampleViewModelFactory::class)
class SampleViewModel @AssistedInject constructor(
    @Assisted val name: String,
    val repository: SampleRepository
) : ViewModel() {
    // ViewModelの実装
}

@AssistedFactory
interface SampleViewModelFactory {
  fun create(val name: String): SampleViewModel
}

@AndroidEntryPoint
class SampleActivity : AppCompatActivity() {
  private val name = "horie1024"
  private val sampleViewModel by viewModels<SampleViewModel>(
    extrasProducer = {
      defaultViewModelCreationExtras.withCreationCallback<
          SampleViewModelFactory> { factory ->
              factory.create(name)
          }
    }
  )
}

```

# まとめ

簡単にですがJetpack ComposeでViewModelに任意のパラメータを渡す方法についてまとめました。

明日のAdvent Calendarの担当は @ssssota さんです。どんな記事が投稿されるのか楽しみです。

# 参考リンク
- https://star-zero.medium.com/initializerviewmodelfactory%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6viewmodel%E3%82%92%E7%94%9F%E6%88%90-25f46c6cb4e1
- https://star-zero.medium.com/viewmodel%E3%81%A8creationextras-c7d586b7a2c6
- https://github.com/google/dagger/issues/2287
- https://dagger.dev/hilt/view-model.html