In modern Android development, Jetpack Compose has simplified UI development, and Hilt has made dependency injection more streamlined. However, when working with Jetpack Compose Previews, you may encounter a common issue: Hilt dependency injection does not work in the context of Compose Previews.
In this detailed blog, we’ll break down the problem, explore why PreviewActivity
doesn’t support Hilt, and show the best practices for managing dependencies during Compose Previews. We’ll also explore alternatives to using Hilt in previews while maintaining a smooth development experience.
What Are Compose Previews and Why Do We Use Them?
Before diving into the problem and solution, let’s first clarify what Compose Previews are and why they are useful:
- Compose Previews allow developers to see their UI components directly in Android Studio without needing to run the entire app on a device or emulator.
- Previews are a design-time tool for visualizing how your Composables will look under different states, layouts, and conditions.
- The goal is to quickly iterate on your UI, test multiple configurations (like different themes, device sizes), and make changes to the UI without running the full app.
Compose Preview Limitations
While Compose Previews are powerful, they have some limitations:
- Hilt Injection is Not Supported: By design, Hilt requires a dependency graph that is only created during runtime. Since Compose Previews are rendered before the app starts, there is no running application context where Hilt can inject dependencies.
- No ViewModel Injection: Since Previews don’t have the full Android lifecycle, they also don’t support
@HiltViewModel
or other lifecycle-dependent mechanisms.
The Problem: PreviewActivity
and Hilt
What is PreviewActivity
?
PreviewActivity
is a special activity used by Android Studio’s tooling to render Compose Previews.- It is part of the Compose Tooling and does not run as part of your actual application.
- Since Hilt dependency injection relies on the app’s runtime environment to manage dependency graphs, and
PreviewActivity
does not have access to that runtime context, it cannot inject dependencies using Hilt.
Why the Error Occurs:
When you try to use Hilt in a preview and attempt to inject dependencies (such as ViewModels or services), Hilt encounters the following issue:
“Given component holder class
androidx.compose.ui.tooling.PreviewActivity
does not implement interfacedagger.hilt.internal.GeneratedComponent
“
This error arises because PreviewActivity
is not part of the app’s dependency graph, and Hilt cannot find any components to inject during preview rendering.
How to Handle Dependency Injection in Compose Previews
Now that we understand the problem, let’s look at the best practices for working around this limitation. Since Hilt cannot be used directly in Compose Previews, we will focus on methods that allow you to test your UI components effectively without Hilt.
Best Practice 1: Use Mock Dependencies
The most effective way to handle dependencies in Compose Previews is to use mock data or mock dependencies instead of relying on real Hilt dependencies. Since Compose Previews are for UI visualization and not real runtime behavior, mocking allows you to bypass the need for Hilt.
Mocking Dependencies in Previews
1. Create Mock Dependencies: For each service, ViewModel, or data source you need in your Composables, create a mock or simplified version of it.
class MockViewModel : ViewModel() {
val sampleData = "Mock data for preview"
}
2. Use the Mock in Your Composable: When writing your Composables, pass the mocked data or services to the composable function.
@Composable
fun MyComposable(viewModel: MockViewModel) {
Text(text = viewModel.sampleData)
}
3. Use @Preview
with Mock Data: In the Preview function, instantiate the mock data directly.
@Preview(showBackground = true)
@Composable
fun MyComposablePreview() {
MyComposable(viewModel = MockViewModel()) // Use mock data
}
Here,
- Previews are meant for UI design, not for running real business logic or testing interactions.
- By passing mock data, you can visualize the UI without needing real data or services.
- This approach keeps previews lightweight and fast.
Best Practice 2: Use Default Arguments for Dependencies
Another approach is to use default arguments for dependencies in your Composables. This way, you can make sure that your Composables work both in the preview environment (with mock data) and in the app’s runtime (with Hilt-injected dependencies).
Default Arguments for Dependencies
1. Update Your Composables: Modify your Composables to use default arguments where appropriate.
@Composable
fun MyComposable(viewModel: MyViewModel = MockViewModel()) {
Text(text = viewModel.sampleData)
}
2. Use @Preview
with the Default Argument: In the Preview function, you don’t need to provide any dependencies explicitly because the MockViewModel
will be used by default
@Preview(showBackground = true)
@Composable
fun MyComposablePreview() {
MyComposable() // Use the default (mock) ViewModel
}
Here,
- You can keep the same Composable for both Preview and Runtime by passing mock dependencies for previews.
- In runtime, Hilt will inject the real
ViewModel
.
Best Practice 3: Use a Conditional DI Approach
If you are working with dependencies that are required for runtime but should be mocked in the preview, you can use a conditional DI approach where you check if you’re in the preview mode and inject mock data accordingly.
@Composable
fun MyComposable(viewModel: MyViewModel = if (BuildConfig.DEBUG) MockViewModel() else viewModel()) {
Text(text = viewModel.sampleData)
}
@Preview
@Composable
fun MyComposablePreview() {
MyComposable() // Will use MockViewModel in Preview
}
Best Practice 4: Avoid Hilt in Previews Entirely
Another strategy is to decouple your ViewModels or services from Hilt for the purposes of previews. This can be done by using interfaces or abstract classes for dependencies, which can then be mocked for preview.
interface DataProvider {
fun getData(): String
}
class RealDataProvider : DataProvider {
override fun getData(): String {
return "Real Data"
}
}
class MockDataProvider : DataProvider {
override fun getData(): String {
return "Mock Data for Preview"
}
}
@Composable
fun MyComposable(dataProvider: DataProvider) {
Text(text = dataProvider.getData())
}
@Preview(showBackground = true)
@Composable
fun MyComposablePreview() {
MyComposable(dataProvider = MockDataProvider()) // Use mock provider for preview and for actual real provider
}
The last two approaches are quite self-explanatory, so I’ll skip further explanation and insights. Let’s directly jump to the final conclusion.
Conclusion
Hilt cannot be used in Jetpack Compose Previews because Previews don’t have access to the runtime dependency graph that Hilt creates. To work around this limitation, you can:
- Use Mock Dependencies: Simplify your Composables to accept mock data or services for previews.
- Use Default Arguments: Make your dependencies optional, allowing mock data to be injected in previews.
- Conditional Dependency Injection: Use a flag to determine whether to use mock data or real dependencies.
- Decouple Hilt Dependencies: Abstract dependencies behind interfaces so they can be easily mocked during previews.
By following these best practices, you can effectively handle dependencies in Compose Previews without running into issues with Hilt.
happy UI composeing..!