具有 ViewModel 参数时不显示 Jetpack Compose Preview

Jetpack Compose Preview is not showing when having a ViewModel parameter

提问人:MeLean 提问时间:11/20/2022 最后编辑:z.g.yMeLean 更新时间:8/10/2023 访问量:1325

问:

我正在使用 Jetpack Compose,但注意到预览未显示。我读过这样的文章,但似乎我的问题有不同的根本原因。即使我为compose函数中的所有参数添加了默认值,如下所示:

@OptIn(ExperimentalLifecycleComposeApi::class)
@Composable
@ExperimentalFoundationApi
@Preview
fun VolumeSettingsScreen(
    speech: SpeechHelper = SpeechHelper(), // my class that converts text to speech
    viewModel: VolumeSettingsViewModel = hiltViewModel(), // using Hilt to inject ViewModels
    navController: NavHostController = rememberNavController() // Compose Navigation component
) {
    MyAppheme {
        Box(
             ...
        )
    }
}

当我回滚一些更改时,我意识到无论它们是否注入了 Hilt,它都不支持。@PreviewviewModels

知道如何解决这个问题吗?

Android Kotlin android-jetpack-compose android-jetpack-compose-preview

评论

0赞 Mobin Yardim 11/20/2022
这回答了你的问题吗?Jetpack compose 预览崩溃,并显示 hiltViewModel<>()
0赞 MeLean 11/20/2022
谢谢,但关于这篇文章:proandroiddev.com/......最佳做法不是在可组合函数中注入 ViewModel。

答:

4赞 MeLean 11/20/2022 #1

我设法通过将 ViewModel 的函数包装到数据类中来可视化屏幕预览,如下所示:

@OptIn(ExperimentalLifecycleComposeApi::class)
@Composable
@ExperimentalFoundationApi
@Preview
fun VolumeSettingsScreen(
    modifier: Modifier = Modifier,
    speechCallbacks: SpeechCallbacks = SpeechCallbacks(),
    navigationCallbacks: NavigationCallbacks = NavigationCallbacks(),
    viewModelCallbacks: VolumeSettingsScreenCallbacks = VolumeSettingsScreenCallbacks()
) {
    MyAppheme {
        Box(
             ...
        )
    }
}

我没有直接在 compose 中传递 ViewModel,而是在 Data 类中传递了所需的函数,例如,如下所示:

data class VolumeSettingsScreenCallbacks(
    val uiState: Flow<BaseUiState?> = flowOf(null),
    val onValueUpSelected: () -> Boolean = { false },
    val onValueDownSelected: () -> Boolean = { false },
    val doOnBoarding: (String) -> Unit = {},
    val onScreenCloseRequest: (String) -> Unit = {} 
)

我制作了一个在 ViewModel 中生成这些回调的方法,如下所示:

@HiltViewModel
class VolumeSettingsViewModel @Inject constructor() : BaseViewModel() {

    fun createViewModelCallbacks(): VolumeSettingsScreenCallbacks =
        VolumeSettingsScreenCallbacks(
            uiState = uiState,
            onValueUpSelected = ::onValueUpSelected,
            onValueDownSelected = ::onValueDownSelected,
            doOnBoarding = ::doOnBoarding,
            onScreenCloseRequest = ::onScreenCloseRequest
        )

 ....
}

在 NavHost 中,我像这样提升了 ViewModel 的创建:

    @Composable
    @ExperimentalFoundationApi
    fun MyAppNavHost(
        speech: SpeechHelper,
        navController: NavHostController,
        startDestination: String = HOME.route,
    ): Unit = NavHost(
        navController = navController,
        startDestination = startDestination,
    ) {
        ...
    
        composable(route = Destination.VOLUME_SETTINGS.route) {
            hiltViewModel<VolumeSettingsViewModel>().run {
                VolumeSettingsScreen(
                    modifier = keyEventModifier,
                    speechCallbacks = speech.createCallback() // my function,
                    navigation callbacks = navController.createCallbacks(), //it is mine extension function                  
                    viewModelCallbacks = createViewModelCallbacks()
                )
            }
        }
    
        ...
    }

这有点复杂,但:D有效。如果有一些彗星需要改进,我会很高兴。

4赞 z.g.y 11/20/2022 #2

你有没有考虑过有一个结构,你有一个和实际分开的,像这样?ScreenContent

// data class
data class AccountData(val accountInfo: Any?)

// composable "Screen", where you define contexts, viewModels, hoisted states, etc
@Composable
fun AccountScreen(viewModel: AccountViewModel = hiltViewModel()) {

    val accountData = viewModel.accountDataState.collectAsState()

    AccountContent(accountData = accountData) {
        // click callback
    }
}

//your actual composable that hosts your child composable widget/components
@Composable
fun AccountContent(
    accountData: AccountData,
    clickCallback: () ->
) {
   ...
}

在哪里可以预览这样的内容?Content

@Preview
@Composable
fun AccountContentPreview() {

    // create some mock AccountData
    val mockData = AccountData(…)
    AccountContent(accountData = mockData) {
         // I'm not expecting some actual ViewModel calls here, instead I'll just manipulate the mock data
    }
}

这样一来,实际内容可组合项不需要配置的所有组件都将分开,从而免去配置预览的麻烦。

只是一个附加的注释,可能偏离主题,我刚刚注意到你有一个这样的参数,

speech: SpeechHelper = SpeechHelper()

您可以考虑使用 compositionLocalProvider(如果需要),它可以清理您的参数。

评论

1赞 MeLean 11/20/2022
谢谢你的回答,在 Mobin Yardim 的评论中,建议采用类似的方法。但我认为这是一种解决方法,而不是解决问题的方法。因为 AccountScreen 的预览仍然会因为 ViewModel 注入而中断
0赞 z.g.y 11/20/2022
我刚刚阅读了他提供的链接,是的,这是一个非常相似的建议,但是如果我可以问,请查看模型注入,例如..?
0赞 MeLean 11/20/2022
我的意思是,当你有这个:AccountScreen(viewModel: AccountViewModel = hiltViewModel())时,预览不起作用。
1赞 MeLean 11/20/2022
谢谢你的评论,我明白了。谢谢你,因为你也会检查它compositionLocalProvider
1赞 MeLean 11/21/2022
感谢您的解释,我看到我们应该使用回调来处理用户交互(例如点击、手势等)。迟早:D。
1赞 Red M 8/10/2023 #3

我找到了一种解决方案,可以在开发过程中查看最佳预览,但在生产代码中则不然。

使你的 compose 中的参数可为 nullviewmodel

@Composable
fun VolumeSettingsScreen(
    viewModel: VolumeSettingsViewModel? = hiltViewModel(), ...)

然后,在预览中,只需传递一个空参数:

@Preview(showBackground = true)
@Composable
fun PreviewVolumeSettingsScreen() {
   VolumeSettingsScreen(null, ....)
}