Using Compose Preview with HiltViewModels

Search for a command to run...

No comments yet. Be the first to comment.
I've been a professional software developer in the mobile space for around 15 years now. I started in the Android 2.1 / Eclipse / Ant days and iOS Objective-C days pre-ARC on Xcode 4 (if memory servic

Intro I recently learned about friendPaths as part of the Kotlin Gradle Plugin while browsing an Android Slack group. From the documentation on friendPaths: "Paths to the output directories of the friend modules whose internal declarations should be ...

If you've ever tried to compare the behavior of Row in Google's Jetpack Compose, HStack in Apple's SwiftUi, and something like display: flex in the web world, you'll see some interesting results. First, let's take a look at how HStack renders two lon...

Said another way, how to supercharge your development workflow with a debug drawer. As Android developers, we're constantly looking for ways to streamline our workflow. One often-overlooked tool that

As Jetpack Compose becomes more widely used across Android (and Multiplatform projects!), detecting regressions via tooling becomes more important to shift left and detect regressions earlier in the d

Jetpack Compose is a modern UI toolkit for Android development that allows developers to build composable user interfaces using Kotlin. One of the great features of Jetpack Compose is the ability to preview composable functions directly in the Android Studio IDE. This makes it easy for developers to iterate on UI designs and catch issues early in the development process.
When building complex UIs, it's often necessary to access data from a ViewModel. The ViewModel is a component of the Android Architecture Components library that is responsible for managing UI-related data in a lifecycle-aware way. In this blog post, we'll explore a way to render a composable under the Jetpack Compose UI framework in a preview when accessing a ViewModel.
To get started, let's create a simple ViewModel that fetches the button text:
@HiltViewModel
class MainViewModel @Inject constructor(
val buttonService: ButtonService,
) : ViewModel() {
fun fetchButtonTextFromServer() : StateFlow<String> {
return buttonService.fetchButtonTextFromServer()
}
}
class ButtonService @Inject constructor() {
fun fetchButtonTextFromServer() : StateFlow<String> {
return MutableStateFlow("Hello from server")
}
}
Next, we'll create a composable that utilizes a flow of data from the ViewModel to set the button of our text. In this example, no actual network call is being made, but in a realistic example, you can imagine code in the ViewModel that would instead fetch the text for our button:
@Composable
fun CustomButton(
onClick : () -> Unit,
) {
val viewModel : MainViewModel = hiltViewModel()
val buttonText = viewModel.fetchButtonTextFromServer().collectAsStateWithLifecycle().value
MyButton(
onClick = onClick,
text = buttonText,
)
}
@Composable
private fun MyButton(
text: String,
onClick : () -> Unit,
) {
Button(onClick = onClick) {
MyButtonText(text = text)
}
}
@Composable
private fun MyButtonText(
text: String,
) {
Text(text = text)
}
If we try to create a @Preview for our CustomButton composable, and render it in the IDE / device, we will get this error:
@Preview
@Composable
fun Preview_CustomButton() {
CustomButton {
// no-op
}
}
Caused by: java.lang.RuntimeException: Cannot create an instance of class com.joetr.themepreviewviewmodel.MainViewModel
Now we want to preview this composable function in Android Studio. By default, Jetpack Compose previews are generated using sample data that is provided by the developer. To preview the composable function using data from our ViewModel, we can create a CompositionLocal when we want to force it in preview mode.
From the docs - "CompositionLocal is a tool for passing data down through the Composition implicitly." This can be thought of as a good solution using two metrics (also from the docs):
A CompositionLocal should have a good default value. If there's no default value, you must guarantee that it is exceedingly difficult for a developer to get into a situation where a value for the CompositionLocal isn't provided. Not providing a default value can cause problems and frustration when creating tests or previewing a composable that uses that CompositionLocal will always require it to be explicitly provided.
Avoid CompositionLocal for concepts that aren't thought as tree-scoped or sub-hierarchy scoped.
val LocalMyButtonPreviewMode: ProvidableCompositionLocal<Boolean> = compositionLocalOf { false }
@Composable
fun CustomButton(
onClick : () -> Unit,
) {
if(LocalMyButtonPreviewMode.current) {
MyButton(
text = "Hello from preview",
onClick = {
// no-op
}
)
} else {
val viewModel : MainViewModel = hiltViewModel()
val buttonText = viewModel.fetchButtonTextFromServer().collectAsStateWithLifecycle().value
MyButton(
onClick = onClick,
text = buttonText,
)
}
}
Now, when we want to create a preview for MyButton, we can utilize this CompostionLocal to get a preview to display as expected.
@Preview
@Composable
fun Preview_MyButton() {
CompositionLocalProvider(LocalMyButtonPreviewMode provides true) {
CustomButton {
// no-op
}
}
}
In summary, by creating a custom CompostionLocal which allows us to use a Preview instance of our Composable instead of relying on a HiltViewModel, we can render our composable under a preview using fake data. This allows us to iterate on UI designs and catch issues early in the development process.