'Jetpack Compose - NavHost preview problem
I've started learning today the jetpack compose and I have a render preview problem when I use the 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)
My project code:
@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) {
...
}
}
}
}
}
Is there any way to fix the preview on Android Studio when I use this NavHost? I'm using the dependency:
implementation "androidx.navigation:navigation-compose:2.4.0-alpha06"
Solution 1:[1]
A technique you can use is to have a wrapper for your screen, your wrapper would get the state from your viewmodel and pass it to the actual screen. You can then preview the screen that takes the state, not the viewmodel. Something like this
@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
}
Solution 2:[2]
I found a workaround that might help someone. It's pretty clear that something internally in NavHost(...) is calling some view model functions that are verboten in the preview simulator. So instead we only provide the NavHost when instantiated during runtime, and substitute one of the navigation destinations during preview.
fun TheComponent(viewModel: YourViewModel, preview: Boolean = false) {
...
if (preview) {
AFragment(...)
} else {
NavHost(navController, "destination") {
composable("destination") { AFragment(...) }
...
}
}
...
}
You can inject a mock viewmodel to your preview if you want. What's important is that NavHost(...) is not invoked when the preview is rendered. Just pass a flag to your component, so that it substitutes the preview
@Preview()
@Composable
fun ThePreview() {
val viewModel = AViewModel(mocks...)
TheComponent(viewModel, true)
}
Ideally NavHost() would invoke the composition function for the startDestination component when run in preview instead of building the the nav graph, and then this boiler plate could be removed.
Solution 3:[3]
You can actually do this with a workaround by replacing the nav host controller value. You will be able to preview the top bar, bottom bar, and middle content however you like as long as it's filled in the preview code, and not coming from the nav host.
The top layout (Not previewed!!):
/**
* 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
)
}
The previewed layout:
/**
* 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)
)
}
And your preview will look like this:
/**
* 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(),
// ...
)
}
}
Happy previewing. If there are any more questions about this, please let me know!
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | Francesc |
| Solution 2 | |
| Solution 3 |
