Closed4

【Jetpack Compose】Hilt を利用してインスタンス化する ViewModel に依存している場合、どのように Preview を機能させればいいのか

wk8oorwk8oor

【当スクラップの疑問点】

Composable 関数が ViewModel に依存している。
この時、Jetpack Compose の Preview 機能が利用できなくなる。

wk8oorwk8oor

【問題となるコードの例】

問題を起こすためのサンプルコードなのでやってる事がショボいのは許して欲しい。
(Hilt 導入済み前提である。)

@HiltViewModel
class MyScreenViewModel @Inject constructor(private val myCalculate: MyCalculate) : ViewModel() {
    private val _count = MutableStateFlow(0)
    val count = _count.asStateFlow()

    fun increment() {
        _count.value = myCalculate.increment(_count.value)
    }
}

class MyCalculate @Inject constructor() {
    fun increment(n: Int): Int = n + 1
}

@Composable
fun MyScreen(viewModel: MyScreenViewModel = viewModel()) {
    val count by viewModel.count.collectAsState()

    Column {
        Text(text = count.toString())
        Button(onClick = { viewModel.increment() }) {
        }
    }
}

@Preview(showBackground = true)
@Composable
private fun MyScreenPreview() {
    MyScreen()
}
wk8oorwk8oor

【他の解決策】

React などで使われている Container/Presentational Pattern を利用する。
https://www.patterns.dev/posts/presentational-container-pattern

@HiltViewModel
class MyScreenViewModel @Inject constructor(private val myCalculate: MyCalculate) : ViewModel() {
    private val _count = MutableStateFlow(0)
    val count = _count.asStateFlow()

    fun increment() {
        _count.value = myCalculate.increment(_count.value)
    }
}

class MyCalculate @Inject constructor() {
    fun increment(n: Int): Int = n + 1
}

// 先ほどのコードから UI を分離した。
// 役割としては、
// 1. State の保持をする。
// 2. UI へ State を引数で渡す、ViewModel のメソッドを呼び出せるように渡す。
// アプリケーションで使うときは MyScreenContent ではなく、こちらを呼び出す。
@Composable
fun MyScreen(viewModel: MyScreenViewModel = viewModel()) {
    val count by viewModel.count.collectAsState()

    MyScreenContent(count = count.toString()) { viewModel.increment() }
}

// こちらで UI の描画を行うように分離した。
// これによって Stateless になる。
// Preview ではこちらを使う。
@Composable
private fun MyScreenContent(count: String, onClick: () -> Unit) {
    Column {
        Text(text = count)
        Button(onClick = { onClick() }) {
        }
    }
}

// State の保持と UI の表示の役割を分離したことによって Preview が ViewModel に依存しなくなった。
// なので引数として Preview 用の処理を渡す。
@Preview(showBackground = true)
@Composable
private fun MyScreenPreview() {
    val count = remember { mutableStateOf(0) }

    MyScreenContent(count.value.toString()) { count.value++ }
}
このスクラップは2023/08/13にクローズされました