Jetpack Compose - NavHost 预览问题。

8

我今天开始学习Jetpack Compose,当我使用NavHost时,遇到了渲染预览问题:

java.lang.IllegalStateException: ViewModels creation is not supported in Preview
at androidx.compose.ui.tooling.ComposeViewAdapter$FakeViewModelStoreOwner$1.getViewModelStore(ComposeViewAdapter.kt:709)
at androidx.navigation.compose.NavHostKt.NavHost(NavHost.kt:103)
at androidx.navigation.compose.NavHostKt.NavHost(NavHost.kt:66)
at com.example.jetpackstudy.ui.activity.BottomNavActivity$AppContentView$1$3$1.invoke(BottomNavActivity.kt:71)
at com.example.jetpackstudy.ui.activity.BottomNavActivity$AppContentView$1$3$1.invoke(BottomNavActivity.kt:70)

我的项目代码:

@Preview @Composable
fun AppContentView() {
    JetPackStudyTheme {
        val navController = rememberNavController()
        Scaffold(topBar = {...})
        }, bottomBar = {...}
        }) {
            Surface(color = MaterialTheme.colors.primary, modifier = Modifier.fillMaxSize()) {
                NavHost(navController, startDestination = BotNavItem.Home.route) {
                    ...
                }
            }
        }
    }
}

有没有方法可以在使用 NavHost 时修复 Android Studio 上的预览?我正在使用以下依赖项: implementation "androidx.navigation:navigation-compose:2.4.0-alpha06"
3个回答

1

您可以使用的一种技术是为您的屏幕创建一个包装器,该包装器将从您的视图模型中获取状态并将其传递给实际屏幕。然后,您可以预览采用状态而不是视图模型的屏幕。类似于这样:

@Composable
fun CityScreen(
    viewModel: CityViewModel,
    modifier: Modifier = Modifier,
) {
    val state = viewModel.state.collectAsState()
    CityScreen(
        state = state.value,
        modifier = modifier,
    )
}

@Composable
private fun CityScreen(
    state: CityState,
    modifier: Modifier = Modifier,
) {
    // code here
}

1
我认为预览NavHost不是可行的选项,事实上,即使是单独的屏幕也是如此。预览仅适用于产品的某些部分。如果您希望以形式查看整个屏幕,则最好实际部署它。因此,才有这个名字。 - Richard Onslow Roper
1
NavHost不应该作为参数传递,我的代码片段展示了如何传递viewmodel和state。如果需要导航,请传递lambda表达式。全屏预览非常有效,您可能希望在不必部署多个模拟器或设备的情况下同时查看它在不同形态因素中的适配情况。 - Francesc
好的,那就这样吧。谢谢你提供的信息 :) - Richard Onslow Roper
很遗憾,这个答案不起作用。我也遇到了同样的问题,即使我从组合函数中删除了所有对视图模型的引用,但是NavHost的存在仍会导致出现此错误。可能在构建导航图时,NavHost构造函数需要构建一些ViewModels助手。 - Jarrod Moldrich

1
您可以通过替换导航宿主控制器的值来实现此操作。只要在预览代码中填充,而不是来自导航宿主,则您可以随意预览顶部栏、底部栏和中间内容。
顶部布局(未预览!!):
/**
 * Top main activity layout with val's that trigger recomposition for everything.
 */
@Composable
fun MainActivityTopLayout() {
    val navController = rememberNavController()
    val viewModel: MainActivityViewModel = hiltViewModel()
    // Add your variables here

    MainActivityLayout(
        navController = navController,
        // Pass your variables here
    )
}

预览布局如下:
/**
 * Call this from a compose function that stores the viewmodel, 
 * if you have any. Since previews can't instantiate ViewModels,
 * you should add everything you need from the viewmodel as
 * function parameter.
**/
@Composable
fun MainActivityLayout(
    content: (@Composable (PaddingValues) -> Unit)? = null,
    navController: NavHostController,
    // ...
) {
    Scaffold(
        // Top and bottom bar declarations here...
        // mainNavHost() is a function that returns your NavHost.
        content = content ?: mainNavHost(navController)
    )
}

你的预览将会是这个样子:

/**
 * Previews the main activity layout.
 * Note that providing an empty block for padding values fixes
 * the compose preview.
 */
@Preview(showBackground = true)
@Composable
fun PreviewMainActivityLayout() {
    AppTheme {
        MainActivityLayout(
            content = {
                // Preview your content, ie. MyOtherView(...)
            },
            navController = rememberNavController(),
            // ...
        )
    }
}

预览愉快。如果还有关于此的任何问题,请告诉我!


1
我找到了一个解决方法,可能会对某些人有帮助。很明显,在预览模拟器中,NavHost(...) 内部调用了一些视图模型函数,这是不允许的。因此,我们只在运行时提供 NavHost,并在预览期间替换其中一个导航目标即可。
fun TheComponent(viewModel: YourViewModel, preview: Boolean = false) {
    ...
    if (preview) {
        AFragment(...)
    } else {
        NavHost(navController, "destination") {
           composable("destination") { AFragment(...) }
           ...
        }
    }
    ...
}

如果你想要,可以在预览中注入一个模拟的ViewModel。重要的是,在渲染预览时不会调用NavHost(...)。只需向组件传递标志,以便替换预览即可。
@Preview()
@Composable
fun ThePreview() {
    val viewModel = AViewModel(mocks...)
    TheComponent(viewModel, true)
}

理想情况下,当在预览中运行时,NavHost()应该调用startDestination组件的构成函数,而不是构建导航图,这样就可以删除此样板文件。请注意,保留了HTML标签。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接