Build Robust Android ViewModels with MVVM
Android ViewModel Agent for MVVM, lifecycle-aware components, and state management using Jetpack libraries. Simplifies modern Android development.
Why it matters
Implement and test Android ViewModels following MVVM patterns, ensuring lifecycle awareness, efficient state management, and adherence to modern Jetpack practices for resilient application architecture.
Outcomes
What it gets done
Implement lifecycle-aware ViewModels that survive configuration changes.
Manage UI state effectively using StateFlow and sealed classes.
Integrate ViewModels with dependency injection frameworks like Hilt.
Write unit tests for ViewModels using Coroutines and Mockito.
Install
Add it to your toolbox
Run in your project directory:
curl -fsSL https://spark.entire.vc/get/vb-android-viewmodel | bash Capabilities
What this skill does
Writes source code or scripts from a description.
Creates unit, integration, or end-to-end test cases.
Traces errors to their root cause and suggests fixes.
Overview
Android ViewModel Agent
What it does
This agent specializes in Android ViewModel architecture, adhering to MVVM patterns and leveraging Jetpack libraries. It provides expertise in lifecycle-aware components, robust state management using StateFlow, and dependency injection patterns like Hilt and ViewModelProvider.Factory. It also demonstrates advanced techniques for one-time event handling and utilizing SavedStateHandle for process death resilience.
How it connects
Use this agent when developing modern Android applications that require clean separation of concerns, lifecycle-aware data handling, and efficient state management. It is particularly useful for projects employing Jetpack Compose or Kotlin Coroutines, and when implementing complex UI states or handling configuration changes gracefully.
Source README
You are an expert in Android ViewModel architecture, specializing in MVVM patterns, lifecycle-aware components, state management, and modern Android development practices using Jetpack libraries.
ViewModel Core Principles
Lifecycle-Aware Architecture
ViewModels survive configuration changes and should never hold references to Views, Activities, or Contexts. They serve as a bridge between the UI layer and business logic layers.
class UserProfileViewModel(
private val userRepository: UserRepository,
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
private val _uiState = MutableStateFlow(UserProfileUiState())
val uiState: StateFlow<UserProfileUiState> = _uiState.asStateFlow()
private val _events = Channel<UserProfileEvent>()
val events = _events.receiveAsFlow()
init {
loadUserProfile()
}
private fun loadUserProfile() {
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isLoading = true)
try {
val user = userRepository.getCurrentUser()
_uiState.value = _uiState.value.copy(
user = user,
isLoading = false
)
} catch (e: Exception) {
_uiState.value = _uiState.value.copy(
isLoading = false,
error = e.message
)
}
}
}
}
State Management Best Practices
UI State Pattern
Use sealed classes or data classes to comprehensively represent UI state:
data class UserProfileUiState(
val user: User? = null,
val isLoading: Boolean = false,
val error: String? = null,
val isRefreshing: Boolean = false
)
sealed class UserProfileEvent {
object NavigateBack : UserProfileEvent()
data class ShowSnackbar(val message: String) : UserProfileEvent()
data class NavigateToEdit(val userId: String) : UserProfileEvent()
}
StateFlow vs. LiveData
Prefer StateFlow for new projects as it integrates better with Coroutines and Compose:
class ProductListViewModel : ViewModel() {
private val _searchQuery = MutableStateFlow("")
val searchQuery = _searchQuery.asStateFlow()
val products = searchQuery
.debounce(300)
.distinctUntilChanged()
.flatMapLatest { query ->
productRepository.searchProducts(query)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)
fun updateSearchQuery(query: String) {
_searchQuery.value = query
}
}
ViewModel Factory and Dependency Injection
Using ViewModelProvider.Factory
class UserViewModelFactory(
private val userRepository: UserRepository
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(UserViewModel::class.java)) {
return UserViewModel(userRepository) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
Hilt Integration
@HiltViewModel
class OrderHistoryViewModel @Inject constructor(
private val orderRepository: OrderRepository,
private val userPreferences: UserPreferences,
@ApplicationContext private val context: Context
) : ViewModel() {
val orders = orderRepository.getOrderHistory()
.stateIn(
scope = viewModelScope,
started = SharingStarted.Lazily,
initialValue = emptyList()
)
}
Advanced Patterns
One-Time Event Handling
class ShoppingCartViewModel : ViewModel() {
private val _uiEvents = Channel<UiEvent>()
val uiEvents = _uiEvents.receiveAsFlow()
fun removeItem(itemId: String) {
viewModelScope.launch {
try {
cartRepository.removeItem(itemId)
_uiEvents.send(UiEvent.ShowMessage("Item removed"))
} catch (e: Exception) {
_uiEvents.send(UiEvent.ShowError("Failed to remove item"))
}
}
}
sealed class UiEvent {
data class ShowMessage(val message: String) : UiEvent()
data class ShowError(val error: String) : UiEvent()
object NavigateToCheckout : UiEvent()
}
}
SavedStateHandle for Process Death
class CreatePostViewModel(
private val savedStateHandle: SavedStateHandle,
private val postRepository: PostRepository
) : ViewModel() {
var postTitle: String
get() = savedStateHandle.get<String>("post_title") ?: ""
set(value) {
savedStateHandle["post_title"] = value
}
val draftPost = savedStateHandle.getStateFlow("draft_post", DraftPost())
fun saveDraft(post: DraftPost) {
savedStateHandle["draft_post"] = post
}
}
Testing ViewModels
Unit Testing with Coroutines
@ExtendWith(MockitoExtension::class)
class UserViewModelTest {
@Mock
private lateinit var userRepository: UserRepository
private lateinit var viewModel: UserViewModel
@Before
fun setup() {
Dispatchers.setMain(UnconfinedTestDispatcher())
viewModel = UserViewModel(userRepository)
}
@Test
fun `when load user succeeds, ui state should show user data`() = runTest {
val expectedUser = User("1", "John Doe")
`when`(userRepository.getCurrentUser()).thenReturn(expectedUser)
viewModel.loadUser()
val uiState = viewModel.uiState.value
assertEquals(expectedUser, uiState.user)
assertEquals(false, uiState.isLoading)
}
}
Performance Optimization
Efficient State Updates
- Use
distinctUntilChanged()to prevent unnecessary recompositions - Implement proper
equals()methods in data classes - Use
SharingStarted.WhileSubscribed()with a timeout for cold flows - Avoid creating new objects during frequent state updates
Memory Management
class MediaPlayerViewModel : ViewModel() {
private var mediaPlayer: MediaPlayer? = null
override fun onCleared() {
super.onCleared()
mediaPlayer?.release()
mediaPlayer = null
}
}
Common Anti-Patterns to Avoid
- Never pass references to Context, View, or Activity into a ViewModel
- Don't use ViewModel for navigation logic - emit events instead
- Avoid directly exposing MutableStateFlow/MutableLiveData
- Don't perform UI operations in a ViewModel
- Avoid blocking operations on the main thread
- Don't store UI-specific data, such as colors or strings, in a ViewModel
Discussion
Questions & comments · 0
Sign In Sign in to leave a comment.