Overview
The android-viewmodel skill provides best practices for implementing Android ViewModels with proper state management. It focuses on using StateFlow for persistent UI state and SharedFlow for one-off events, ensuring proper handling of configuration changes.
When to use this skill:
Implementing ViewModels for Activities or Composables
Managing UI state that survives configuration changes
Handling one-off events like navigation or showing toasts
Setting up reactive UI updates with Flows
Ensuring lifecycle-aware state collection
Core Concept
ViewModel holds state and business logic that must outlive configuration changes (like screen rotation). It acts as the bridge between the UI layer and the business logic/data layer.
UI State with StateFlow
What is UI State?
UI State represents the persistent state of the UI, such as:
Loading states
Success data
Error messages
Form input values
Implementation Pattern
data class NewsUiState (
val isLoading: Boolean = false ,
val news: List < News > = emptyList (),
val error: String ? = null
)
@HiltViewModel
class NewsViewModel @Inject constructor (
private val getNewsUseCase: GetNewsUseCase
) : ViewModel () {
private val _uiState = MutableStateFlow ( NewsUiState (isLoading = true ))
val uiState: StateFlow < NewsUiState > = _uiState. asStateFlow ()
init {
loadNews ()
}
private fun loadNews () {
viewModelScope. launch {
_uiState. update { it. copy (isLoading = true , error = null ) }
try {
val news = getNewsUseCase ()
_uiState. update { it. copy (isLoading = false , news = news) }
} catch (e: Exception ) {
_uiState. update { it. copy (isLoading = false , error = e.message) }
}
}
}
}
Key Requirements
Type Use StateFlow<UiState> for all persistent state
Initial Value Must have an initial value (e.g., Loading state)
Exposure Expose as read-only StateFlow, backed by private MutableStateFlow
Updates Use .update { } for thread-safe state updates
Always use .update { oldState -> ... } instead of direct assignment for thread safety: // Good - Thread-safe
_uiState. update { it. copy (isLoading = false ) }
// Bad - Not thread-safe
_uiState. value = _uiState. value . copy (isLoading = false )
One-Off Events with SharedFlow
What are One-Off Events?
One-off events are transient actions that should happen once, such as:
Showing a toast message
Navigating to another screen
Showing a snackbar
Triggering haptic feedback
Implementation Pattern
sealed class UiEvent {
data class ShowToast ( val message: String ) : UiEvent ()
data class Navigate ( val route: String ) : UiEvent ()
data object ShowSnackbar : UiEvent ()
}
@HiltViewModel
class NewsViewModel @Inject constructor (
private val getNewsUseCase: GetNewsUseCase
) : ViewModel () {
private val _uiEvent = MutableSharedFlow < UiEvent >(replay = 0 )
val uiEvent: SharedFlow < UiEvent > = _uiEvent. asSharedFlow ()
fun onNewsItemClick (newsId: String ) {
viewModelScope. launch {
_uiEvent. emit (UiEvent. Navigate ( "news/ $newsId " ))
}
}
fun onShareClick () {
viewModelScope. launch {
_uiEvent. emit (UiEvent. ShowToast ( "Sharing..." ))
}
}
}
Key Requirements
Critical: Must use replay = 0 to prevent events from re-triggering on screen rotation or configuration changes.
Type Use SharedFlow<UiEvent> for transient events
Replay Must set replay = 0 to avoid re-triggering
Sending Use .emit(event) (suspend) or .tryEmit(event)
Consumption Events are consumed once and not replayed
Collecting in UI
Jetpack Compose
StateFlow Collection
SharedFlow Collection
@Composable
fun NewsScreen (
viewModel: NewsViewModel = hiltViewModel ()
) {
val state by viewModel.uiState. collectAsStateWithLifecycle ()
when {
state.isLoading -> LoadingIndicator ()
state.error != null -> ErrorMessage (state.error !! )
else -> NewsList (news = state.news)
}
}
Use collectAsStateWithLifecycle() for StateFlow in Compose - it automatically handles lifecycle awareness and prevents unnecessary recompositions.
XML Views
class NewsFragment : Fragment () {
private val viewModel: NewsViewModel by viewModels ()
override fun onViewCreated (view: View , savedInstanceState: Bundle ?) {
super . onViewCreated (view, savedInstanceState)
// Collect StateFlow
viewLifecycleOwner.lifecycleScope. launch {
viewLifecycleOwner. repeatOnLifecycle (Lifecycle.State.STARTED) {
viewModel.uiState. collect { state ->
updateUI (state)
}
}
}
// Collect SharedFlow
viewLifecycleOwner.lifecycleScope. launch {
viewLifecycleOwner. repeatOnLifecycle (Lifecycle.State.STARTED) {
viewModel.uiEvent. collect { event ->
handleEvent (event)
}
}
}
}
}
Always use repeatOnLifecycle(Lifecycle.State.STARTED) in Views to ensure collection stops when the UI is not visible, preventing memory leaks and wasted resources.
ViewModel Scope
Using viewModelScope
@HiltViewModel
class NewsViewModel @Inject constructor (
private val repository: NewsRepository
) : ViewModel () {
fun refreshNews () {
viewModelScope. launch {
// This coroutine is automatically cancelled when ViewModel is cleared
repository. refreshNews ()
}
}
}
Use viewModelScope for all coroutines started by the ViewModel
Coroutines are automatically cancelled when the ViewModel is cleared
Delegate specific operations to UseCases or Repositories
Don’t perform heavy operations directly in the ViewModel
Best Practices
StateFlow for State Use StateFlow for persistent UI state that survives configuration changes
SharedFlow for Events Use SharedFlow with replay = 0 for one-off events
Thread-Safe Updates Always use .update { } for modifying StateFlow values
Lifecycle Awareness Collect flows with lifecycle awareness to prevent leaks
Common Patterns
Loading, Success, Error Pattern
sealed interface UiState < out T > {
data object Loading : UiState < Nothing >
data class Success < T >( val data : T ) : UiState < T >
data class Error ( val message: String ) : UiState < Nothing >
}
@HiltViewModel
class NewsViewModel @Inject constructor (
private val getNewsUseCase: GetNewsUseCase
) : ViewModel () {
private val _uiState = MutableStateFlow < UiState < List < News >>>(UiState.Loading)
val uiState: StateFlow < UiState < List < News >>> = _uiState. asStateFlow ()
init {
loadNews ()
}
private fun loadNews () {
viewModelScope. launch {
_uiState. value = UiState.Loading
try {
val news = getNewsUseCase ()
_uiState. value = UiState. Success (news)
} catch (e: Exception ) {
_uiState. value = UiState. Error (e.message ?: "Unknown error" )
}
}
}
}
Architecture Learn about the overall Android app architecture
Data Layer Implement repositories and data sources for your ViewModels