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.
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
The problem
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>()

Why this happens
Compose Preview:
does not run inside a real
Activitydoes 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.
Why common fixes are wrong
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
The correct mental model
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.
Step 1: define a ViewModel contract (interface)
Instead of exposing a concrete ViewModel, define what the UI actually needs.
interface Step1ViewModel {
suspend fun submit(
locale: String,
selectedState: OnboardingState
): Result<Unit>
}
Why this matters
UI depends on behavior, not implementation
enables fake implementations
enables testing
enables Preview
Step 2: implement the real Hilt ViewModel
@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)
}
}
}
Why return Result
clear success / failure semantics
no callback nesting
structured concurrency
test-friendly API
Step 3: create a fake ViewModel for Preview
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

The tempting abstraction — and why it failed
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.
The key realization
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.
The production-safe architecture
The rule
A composable must not decide how its ViewModel is constructed.
That responsibility belongs to the caller.
Pure UI composable (no DI knowledge)
@Composable
fun Step1Screen(
vm: Step1ViewModel
) {
// UI logic
}
Runtime wiring (NavHost / Activity)
composable("step1") {
Step1Screen(
vm = hiltViewModel<Step1ViewModelImpl>()
)
}
Preview wiring (explicit fake)
@Preview(showBackground = true)
@Composable
fun Step1ScreenPreview() {
AppTheme {
Step1Screen(
vm = FakeStep1ViewModel()
)
}
}
No guessing.
No inspection flags.
No runtime ambiguity.
Is this a standard production approach?
Yes.
This pattern aligns with:
Clean Architecture
Google’s Compose samples
test-driven UI development
large-scale Android apps
Why this scales in production
✔ 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

Conclusion
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…



