はじめに
Android Advent Calendar 2023 1日目の記事です。
ViewModel に依存しない Composable
Composable が ViewModel に依存すると Preview を書くのが難しくなったり、 ViewModel を Composable から触れすぎてしまうのでコードが複雑になりやすいので避けたいです。
Android Develpers のガイドで示されているサンプルからコードを抜粋して紹介します。
Good
次のような ViewModel に依存していない Composable を書きたいです。
@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
) {
HomeContent
の呼び出し側で ViewModel や ViewState の依存を注入しています。
HomeContent(
featuredPodcasts = viewState.featuredPodcasts,
isRefreshing = viewState.refreshing,
homeCategories = viewState.homeCategories,
selectedHomeCategory = viewState.selectedHomeCategory,
onCategorySelected = viewModel::onHomeCategorySelected,
onPodcastUnfollowed = viewModel::onPodcastUnfollowed,
navigateToPlayer = navigateToPlayer,
modifier = Modifier.fillMaxSize()
)
Bad
ViewModel に依存してしまう Composable として次のように書くこともできてしまいます。
@Composable
fun HomeContent(
featuredPodcasts: PersistentList<PodcastWithExtraInfo>,
isRefreshing: Boolean,
homeCategories: List<HomeCategory>,
viewModel: HomeViewModel,
modifier: Modifier = Modifier,
navigateToPlayer: (String) -> Unit
) {
ViewModel を利用すると次のように書くこともできてしまいます。
- onClick = { onCategorySelected(category) },
+ onClick = { viewModel.onHomeCategorySelected(category) },
こうすると、 HomeContent
も HomeCategoryTabs
も HomeViewModel
に依存するので、 Preview を書くのが難しくなります。
Handle user event のコード例を見ると避けたいと書いた onClick = { viewModel.refresNews() }
としています。このコード例の影響もあって、 Composable に ViewModel を渡すことの違和感を消しているのかもしれません。
@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を子の関数に渡さない。
おわりに
2日目は「恥ずかしがり屋の ViewModel」というタイトルでより良い ViewModel について続きを書こうと思います。