ViewModel に依存しない Composable を書こう

# はじめに
Android Advent Calendar 2023 1日目の記事です。

https://qiita.com/advent-calendar/2023/android

# ViewModel に依存しない Composable

Composable が ViewModel に依存すると Preview を書くのが難しくなったり、 ViewModel を Composable から触れすぎてしまうのでコードが複雑になりやすいので避けたいです。

Android Develpers のガイドで示されているサンプルからコードを抜粋して紹介します。

https://developer.android.com/topic/architecture/ui-layer#samples

## Good
次のような ViewModel に依存していない Composable を書きたいです。

```kotlin
@Composable
fun HomeContent(
    featuredPodcasts: PersistentList<PodcastWithExtraInfo>,
    isRefreshing: Boolean,
    selectedHomeCategory: HomeCategory,
    homeCategories: List<HomeCategory>,
    modifier: Modifier = Modifier,
    onPodcastUnfollowed: (String) -> Unit,
    onCategorySelected: (HomeCategory) -> Unit,
    navigateToPlayer: (String) -> Unit
) {
```

https://github.com/android/compose-samples/blob/d8d341b1151c38bb3f63cb67776485213e21e113/Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/Home.kt#L161-L171

`HomeContent` の呼び出し側で ViewModel や ViewState の依存を注入しています。

```kotlin
        HomeContent(
            featuredPodcasts = viewState.featuredPodcasts,
            isRefreshing = viewState.refreshing,
            homeCategories = viewState.homeCategories,
            selectedHomeCategory = viewState.selectedHomeCategory,
            onCategorySelected = viewModel::onHomeCategorySelected,
            onPodcastUnfollowed = viewModel::onPodcastUnfollowed,
            navigateToPlayer = navigateToPlayer,
            modifier = Modifier.fillMaxSize()
        )
```

https://github.com/android/compose-samples/blob/d8d341b1151c38bb3f63cb67776485213e21e113/Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/Home.kt#L101-L110

## Bad
ViewModel に依存してしまう Composable として次のように書くこともできてしまいます。

```kotlin
@Composable
fun HomeContent(
    featuredPodcasts: PersistentList<PodcastWithExtraInfo>,
    isRefreshing: Boolean,    
    homeCategories: List<HomeCategory>,
    viewModel: HomeViewModel,
    modifier: Modifier = Modifier,
    navigateToPlayer: (String) -> Unit
) {
```

ViewModel を利用すると次のように書くこともできてしまいます。

```diff
-               onClick = { onCategorySelected(category) },
+               onClick = { viewModel.onHomeCategorySelected(category) },
```

https://github.com/android/compose-samples/blob/d8d341b1151c38bb3f63cb67776485213e21e113/Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/Home.kt#L293

こうすると、 `HomeContent` も `HomeCategoryTabs` も `HomeViewModel` に依存するので、 Preview を書くのが難しくなります。

[Handle user event](https://developer.android.com/topic/architecture/ui-layer/events#handle-user-events
) のコード例を見ると避けたいと書いた `onClick = { viewModel.refresNews() }` としています。このコード例の影響もあって、 Composable に ViewModel を渡すことの違和感を消しているのかもしれません。

```kotlin
@Composable
fun LatestNewsScreen(viewModel: LatestNewsViewModel = viewModel()) {

    // State of whether more details should be shown
    var expanded by remember { mutableStateOf(false) }

    Column {
        Text("Some text")
        if (expanded) {
            Text("More details")
        }

        Button(
          // The expand details event is processed by the UI that
          // modifies this composable's internal state.
          onClick = { expanded = !expanded }
        ) {
          val expandText = if (expanded) "Collapse" else "Expand"
          Text("$expandText details")
        }

        // The refresh event is processed by the ViewModel that is in charge
        // of the UI's business logic.
        Button(onClick = { viewModel.refreshNews() }) {
            Text("Refresh data")
        }
    }
}
```

## 関連

> Twitter Jetpack Compose Rulesでは以下のルールを定めています。
>
> - ViewModelを子の関数に渡さない。

https://zenn.dev/yagiryuuu/articles/fab62b0590b2a5

## おわりに
2日目は「恥ずかしがり屋の ViewModel」というタイトルでより良い ViewModel について続きを書こうと思います。

https://qiita.com/ykws/items/49d34da6177d235eb3f1