Understanding the Problem
Let's understand the problem we face while creating previews of Viewmodel-dependent Composables.
Previews are lightweight and don't use the whole Android framework to render. Thus, previews have the following limitations
- No network access
- No file access
- Some Context APIs may not be fully available
So the ViewModel that depends on the classes and features that are not available in the preview environment will fail to render with some exceptions.
Real-world scenario where we have a ViewModel-dependent Composable
Let's take an example of a UserProfile screen that fetches user details from a remote server.
ViewModel
The following is a simple ViewModel that fetches user details from a remote server.
class UserViewModel(
private val userRepository: UserRepository
) : ViewModel() {
private val _userState = MutableStateFlow<UserState>(UserState.Loading)
val userState: StateFlow<UserState> = _userState.asStateFlow()
fun fetchUserDetails(userId: String) {
viewModelScope.launch {
try {
val user = userRepository.getUser(userId)
_userState.value = UserState.Success(user)
} catch (e: Exception) {
_userState.value = UserState.Error(e.message ?: "Unknown error")
}
}
}
}
Composabe that depends on the ViewModel
The following is a simple Composable that displays user details.
@Composable
fun UserProfile(
viewModel: UserViewModel,
userId: String
) {
val userState by viewModel.userState.collectAsState()
LaunchedEffect(userId) {
viewModel.fetchUserDetails(userId)
}
when (userState) {
is UserState.Loading -> {
CircularProgressIndicator()
}
is UserState.Success -> {
val user = (userState as UserState.Success).user
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text(
text = user.name,
style = MaterialTheme.typography.h5
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = user.email,
style = MaterialTheme.typography.body1
)
}
}
is UserState.Error -> {
Text(
text = (userState as UserState.Error).message,
color = MaterialTheme.colors.error
)
}
}
}
Preview
The following shows the problem we would have while creating a preview for the above composable.
@Preview
@Composable
private fun UserProfilePreview() {
UserProfile(
viewModel = UserViewModel(
userRepository = UserRepository()
),
userId = "123"
)
}
UserRepository uses local and remote sources to fetch user details.
Neither the local nor remote source dependencies are available in the preview environment, so the preview will fail to render.
Solution
To Solve this issue we can mock the ViewModel or its dependencies with the kotlin Mockk
library.
Kotlin mockk is a mocking library for kotlin which is used in unit testing. However, we could leverage the Mockk library to mock the Composable dependencies while creating the preview.
Even if ViewModel is a complex class with multiple dependencies, we can mock it easily by using the Mockk library.
Add Mockk to your project
Add the following dependency to your app's build.gradle file:
note
: we use the debugImplementation scope to avoid adding the mockk
library to the release build.
dependencies {
debugImplementation("io.mockk:mockk:1.13.13")
}
For more details about Mockk library, check out the official documentation.
ProfileScreen Preview with Mocked ViewModel
@Preview
@Composable
private fun UserProfilePreview() {
val mockViewModel = mockk<UserViewModel>()
// Define what should be returned when the userState property is accessed
val userState = remember { MutableStateFlow<UserState>(UserState.Loading) }
every { mockViewModel.userState } returns userState
// Define what should happen when the fetchUserDetails function is called
every { mockViewModel.fetchUserDetails(any()) } answers {
// give some delay to simulate a real API call
delay(1000)
// Simulate a successful API call
userState.value = UserState.Success(User("John Doe", "john.doe@example.com"))
}
UserProfile(
viewModel = mockViewModel,
userId = "123"
)
}
That's it! Now, when you run the preview, the UserProfile composable will use the mocked UserViewModel. We mock the behaviour of the ViewModel to return a specific state and the behaviour of the fetchUserDetails function to simulate a successful API call.
Conclusion
In this post, we learned:
- How to handle Jetpack Compose previews when dependencies are not available in the preview environment
- Using MockK library to mock ViewModel and its dependencies in Compose previews
- Adding MockK dependency specifically for debug builds using debugImplementation
- Creating mock behaviours for ViewModel properties and functions
- Simulating API calls and state changes in the preview environment
This approach allows us to create realistic previews of our Composables even when they depend on complex ViewModels or repositories, making the development process more efficient and reliable.
Top comments (0)