Skip to main content

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

@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

Build docs developers (and LLMs) love