すべての UI 副作用を ViewModel Event に変換する必要はない

Composeでは、すべてのUI操作を
UI
↓
ViewModel
↓
Event
↓
UI
↓
Effect
という経路に乗せる必要はありません。
UI だけで意味が完結する処理は、Compose 側に残して、必要なら拡張関数として共通化する方が自然です。
例えば以下です。
├── Navigation
├── Snackbar
├── Toast
├── Scroll
├── Focus
├── Keyboard
├── Clipboard
├── Haptic feedback
├── Share
├── Open URL
├── Permission launcher
└── File picker
これらは「副作用」ではありますが、副作用だから ViewModel Event にする必要があるわけではありません。
🤔 ViewModel Event にすることの問題
Navigation 3を例にします。
sealed interface UiEvent {
data class NavigateToDetail(
val id: String
) : UiEvent
}
fun onItemClick(id: String) {
viewModelScope.launch {
_events.emit(
UiEvent.NavigateToDetail(id)
)
}
}
UI 側では Event を受け取ります。
LaunchedEffect(Unit) {
viewModel.events.collect { event ->
when (event) {
is UiEvent.NavigateToDetail -> {
backStack.add(
Detail(event.id)
)
}
}
}
}
このパターン自体が間違いというわけではありません。
しかし、ここで一つ質問できます。
Detail へ移動するために、ビジネスロジックは必要でしょうか?
もし必要ないなら、UI 操作を ViewModel に渡して、再び UI に戻しています。
🤔 Navigation 3 は UI で完結できる
Navigation 3 では、BackStack が明示的に存在します。
Button(
onClick = {
backStack.add(
Detail(id)
)
}
) {
Text("Open")
}
拡張関数にすると、より読みやすくできます。
fun NavBackStack<NavKey>.navigateTo(
destination: NavKey
) {
add(destination)
}
UIはこうなります。
Button(
onClick = {
backStack.navigateTo(
Detail(id)
)
}
) {
Text("Open")
}
ここには、
UiEvent
Channel
SharedFlow
LaunchedEffect
が必要ありません。
UI → Navigation 3 で完結します。
🤔 Snackbar の場合
従来なら、
ViewModel
↓
ShowSnackbar Event
↓
UI
↓
showSnackbar()
とします。
これを、UI側の処理を拡張関数にできます。
suspend fun String.showSnackbar(
hostState: SnackbarHostState
) {
hostState.showSnackbar(this)
}
LaunchedEffect(uiState.message) {
uiState.message?.showSnackbar(
snackbarHostState
)
}
この UI に対して ViewModel は「大事な状態」を持っています。
data class UiState(
val message: String? = null
)
🤔 重要なのは「UI の責務」と「ビジネスロジック」の境界
例えば、次の2つは明確に違います。
これはUI側に置けます。
backStack.navigateTo(
Detail(id)
)
これは ViewModel / UseCase側です。
viewModel.save()
🤔 まとめ
- UIだけで完結する処理は、UIに置く。
- 繰り返し使うUI操作は、Compose の拡張関数として共通化する。
🤔 参考
Related Categories : Android・JetpackCompose・Kotlin・Recommended