Preview-Safe ViewModels in Jetpack Compose with Hilt
Learn how to build preview-safe ViewModels in Jetpack Compose using Hilt.

Senior Android Engineer from Bangladesh. Love to contribute in Open-Source. Indie Music Producer.
Search for a command to run...
Learn how to build preview-safe ViewModels in Jetpack Compose using Hilt.

Senior Android Engineer from Bangladesh. Love to contribute in Open-Source. Indie Music Producer.
No comments yet. Be the first to comment.
A practical guide to choosing the right coroutine primitive for your Android/Kotlin projects

This guide explains what Channels are, when to use them, how to use them correctly, and when NOT to use them — in simple, professional language. 1. What problem do Channels solve? In Kotlin coroutines, you often have multiple coroutines running at t...

HandlerInterceptor vs Filter in Spring Boot: A Practical Guide with JNDI Injection Prevention

Simple In-Memory Caching: A Tiny Trick with Massive Impact

Jetpack Compose simplifies UI development, but when combined with Hilt and Compose Preview, it exposes a subtle architectural challenge that many teams encounter in real projects.
This article explains:
the real problem
why it happens
why some “clean” solutions fail
the production-safe solution
whether this approach is industry-standard
In a typical Compose screen, it’s tempting to write:
@Composable
fun Screen(
vm: MyViewModel = hiltViewModel()
)
This works perfectly at runtime, but crashes in Compose Preview with errors like:
java.lang.NoSuchMethodException: MyViewModel.<init>()

Compose Preview:
does not run inside a real Activity
does not initialize Hilt
does not create a dependency graph
When hiltViewModel() is invoked in Preview, Compose attempts to instantiate the ViewModel using a no-arg constructor, which Hilt ViewModels intentionally do not have.
This is expected behavior.
Some common (but incorrect) workarounds:
adding a no-arg constructor ❌
disabling Preview ❌
duplicating composables just for Preview ❌
These approaches:
break dependency-injection guarantees
introduce technical debt
do not scale in large codebases
Compose Preview is not runtime.
Therefore:
Hilt must never be invoked in Preview
UI must not depend on concrete ViewModel implementations
ViewModel creation must be explicit, not inferred
The solution is abstraction + explicit wiring.
Instead of exposing a concrete ViewModel, define what the UI actually needs.
interface Step1ViewModel {
suspend fun submit(
locale: String,
selectedState: OnboardingState
): Result<Unit>
}
UI depends on behavior, not implementation
enables fake implementations
enables testing
enables Preview
@HiltViewModel
class Step1ViewModelImpl @Inject constructor(
private val repository: Repository
) : ViewModel(), Step1ViewModel {
override suspend fun submit(
locale: String,
selectedState: OnboardingState
): Result<Unit> = withContext(Dispatchers.IO) {
val response = repository.submit(locale, selectedState)
if (response.isSuccessful) {
Result.success(Unit)
} else {
Result.failure(response.error)
}
}
}
Resultclear success / failure semantics
no callback nesting
structured concurrency
test-friendly API
class FakeStep1ViewModel : ViewModel(), Step1ViewModel {
override suspend fun submit(
locale: String,
selectedState: OnboardingState
): Result<Unit> {
return Result.success(Unit)
}
}
The fake ViewModel:
has no dependencies
returns deterministic results
allows UI rendering and interaction in Preview

A common next step is to introduce a helper that tries to “automatically” choose between a fake ViewModel and a Hilt ViewModel based on Preview detection.
While this looks clean, it introduces a serious problem:
Preview detection relies on tooling signals
tooling signals are not runtime guarantees
fake ViewModels can leak into real app execution
behavior becomes non-deterministic and hard to debug
This approach hides a critical architectural decision behind a helper function.
Preview is a build-time concern, not a runtime concern.
Runtime code should never guess whether it is running in Preview.
Once this is accepted, the correct solution becomes obvious.
A composable must not decide how its ViewModel is constructed.
That responsibility belongs to the caller.
@Composable
fun Step1Screen(
vm: Step1ViewModel
) {
// UI logic
}
composable("step1") {
Step1Screen(
vm = hiltViewModel<Step1ViewModelImpl>()
)
}
@Preview(showBackground = true)
@Composable
fun Step1ScreenPreview() {
AppTheme {
Step1Screen(
vm = FakeStep1ViewModel()
)
}
}
No guessing.
No inspection flags.
No runtime ambiguity.
Yes.
This pattern aligns with:
Clean Architecture
Google’s Compose samples
test-driven UI development
large-scale Android apps
✔ Separation of concerns
UI depends on interfaces, not DI frameworks
✔ Testability
Fake ViewModels work in unit and UI tests
✔ Stability
No reflection hacks, no no-arg constructor abuse
✔ Maintainability
Clear boundaries and explicit ownership

Compose Preview is not runtime
Hilt ViewModels must not be invoked implicitly
UI should depend on interfaces
Fake ViewModels belong to Preview and tests
ViewModel creation must be explicit and controlled
If a composable needs a ViewModel, it should never decide how that ViewModel is constructed — only what it can do.
That’s it for today. Happy coding…