One Off the Slack: @Preview and a ViewModel, Again

Jeffrey Nyauke asked:

Noticed that previews do not work when I use a viewModel in a composable. How do you get around this?

This sounds like the topic of a previous “One Off the Slack” post. And, as that one is only three months old, the advice from there is sound, and some of it is linked to in this new Slack thread.

However, we also got some new advice from different advisers.

Bryan Herbst offered:

I’m generally creating two composables- one that uses the view model (either as a parameter or via the viewModels() extension function) and one that only takes in the state I need. E.g.:

@Composable
fun AccountScreen(
  viewModel: AccountViewModel
) {
  AccountScreen(viewModel.screenState)
}

@Composable
private fun AccountScreen(
  state: AccountScreenState
) {
  // ...
}

// Now you can actually write a preview
@Preview
@Composable
private fun LoggedInAccountPreview() {
  AccountScreen(AccountScreenState.LoggedIn)
}

This works nicely, in that the previewable content is in a dedicated function, with the ViewModel being used in a wrapper.

Jeffrey then asked:

What if the composable takes in a viewmodel to make a call like viewModel.login()

Brandon McAnsh replied:

hoist that up back to the parent composable via a lambda

actioner: (Event) -> Unit

The gist of both Bryan and Brandon’s responses is that the previewable composables should be dealing with basic types, core Compose constructs (e.g., State), and lambdas… and little else.

Jeffrey persisted:

So practically there is no way to preview a screen with many composables if I’m using the viewModel?

At this point, Google’s Ian Lake pointed to a previous discussion, illustrating yet another approach: have the @Preview function accept a ViewModel as a parameter, but provide a default value for that ViewModel, such as one that you get from your DI framework (Koin, Dagger/Hilt, or whatever).

Jeffrey pointed out that this may not be as easy as described:

When I pass it in as a parameter, and include a default value for the ViewModel that pulls from Koin, the composable does not display. I get the message Koin cannot be initialized.

Ian responded:

The whole point of passing it in is so that your preview code can override the default value and provide a proper test/fake for your preview to use

Discussions then continued on how to actually provide a fake… but at that point, I really start to question the sanity of the approach. Going back to the earlier “One Off the Slack” post, I’ll recommend this comment from Google’s Jim Sproch:

When your composable does not behave well in Preview, that is almost always an indication your composable is not sufficiently isolated from the rest of the platform/application code.

…and this one from Google’s Sean McQuillan:

I’d extend this to say any reference to a stateful final class in a composable is something to add carefully as it makes your composable inherently stateful, which can make testing and preview harder.


Read the original thread in the kotlinlang Slack workspace. Not a member? Join that Slack workspace here!