CPS251 Android Development by Scott Shaper

Building The WeatherViewModel

The WeatherViewModel is responsible for managing UI-related state and coordinating with the WeatherRepository to fetch weather data from the network. It exposes state using StateFlow for reactive UI updates and handles all business logic related to weather data.

class WeatherViewModel(private val repository: WeatherRepository) : ViewModel() {
    private val _zipcode = MutableStateFlow("")
    val zipcode: StateFlow = _zipcode.asStateFlow()

    private val _weatherResponse = MutableStateFlow(null)
    val weatherResponse: StateFlow = _weatherResponse.asStateFlow()

    private val _errorMessage = MutableStateFlow(null)
    val errorMessage: StateFlow = _errorMessage.asStateFlow()

    fun onZipcodeChange(newZipcode: String) {
        _zipcode.value = newZipcode
    }

    fun fetchWeather() {
        viewModelScope.launch {
            _errorMessage.value = null // Clear any previous error
            repository.getCurrentWeather(_zipcode.value)
                .onSuccess { response ->
                    _weatherResponse.value = response
                }
                .onFailure { e ->
                    _errorMessage.value = e.message
                    _weatherResponse.value = null
                }
        }
    }

    // Factory for ViewModel injection
    companion object {
        fun provideFactory(repository: WeatherRepository): ViewModelProvider.Factory {
            return object : ViewModelProvider.Factory {
                @Suppress("UNCHECKED_CAST")
                override fun <T : ViewModel> create(modelClass: Class<T>): T {
                    if (modelClass.isAssignableFrom(WeatherViewModel::class.java)) {
                        return WeatherViewModel(repository) as T
                    }
                    throw IllegalArgumentException("Unknown ViewModel class")
                }
            }
        }
    }
}

Code Explanation

The WeatherViewModel is a central component for managing and providing weather data to the UI. It extends ViewModel from Android Architecture Components, which means its lifecycle is independent of UI components (like Activities or Composables) and it survives configuration changes.

Dependency Injection

State Variables

All UI state is managed using Kotlin's StateFlow, which provides a reactive, lifecycle-aware way to manage state that integrates seamlessly with Jetpack Compose's collectAsState():

State Update Functions

fetchWeather Function

The fetchWeather function is responsible for initiating the weather data retrieval process. It reads the current zipcode from the ViewModel's internal state and delegates the actual network request to the repository.

ViewModel Factory

The companion object contains a factory method for creating instances of WeatherViewModel with the required dependencies. This is necessary because Android's ViewModel system requires a special way to create ViewModels when they have constructor parameters.

Why a Factory is Needed: Since WeatherViewModel requires a WeatherRepository in its constructor (dependency injection), we can't use the default ViewModel creation mechanism. Android's ViewModelProvider needs a factory that knows how to create our ViewModel with its dependencies.

How It's Used: In the UI composable (like WeatherAppScreen), you call viewModel(factory = WeatherViewModel.provideFactory(weatherRepository)). This tells Jetpack Compose to use our custom factory when creating the ViewModel, ensuring the repository dependency is properly injected.

Benefits: This factory pattern enables proper dependency injection, making the ViewModel testable (you can inject a mock repository during testing), maintainable (dependencies are explicit), and following clean architecture principles.

Learning Aids

Tips for Success

  • Keep your ViewModel focused on UI-related data and logic, avoiding direct Android view dependencies.
  • Use StateFlow for managing all UI state in the ViewModel. This provides reactive, lifecycle-aware state management that integrates well with Jetpack Compose.
  • Use viewModelScope for launching coroutines to ensure they are automatically cancelled when the ViewModel is no longer needed.
  • Clearly separate mutable internal states (e.g., _weatherResponse) from their publicly exposed immutable StateFlow counterparts (e.g., weatherResponse).
  • Delegate data fetching to a Repository layer. The ViewModel should coordinate between the Repository and UI, not directly make network calls.
  • Use the ViewModel factory pattern when your ViewModel requires dependencies, enabling clean dependency injection.

Common Mistakes to Avoid

  • Passing `Context` directly into the ViewModel, which can lead to memory leaks. Instead, inject application context or use AndroidViewModel if necessary.
  • Performing long-running operations directly in the ViewModel without using coroutines, which can block the UI thread.
  • Exposing mutable state directly to the UI (e.g., exposing MutableStateFlow instead of StateFlow), which can lead to unexpected UI updates and difficult-to-track bugs.
  • Making direct API calls from the ViewModel instead of using a Repository layer. This makes the code harder to test and violates separation of concerns.
  • Storing UI state (like input field values) in composables instead of the ViewModel, leading to scattered state management.
  • Not using a ViewModel factory when the ViewModel requires dependencies, making it difficult to inject mock dependencies for testing.
  • Using mutableStateOf instead of StateFlow in ViewModels when you want the state to be observable by multiple collectors or need better integration with reactive programming patterns.

Best Practices

  • Inject dependencies (like WeatherRepository) into the ViewModel constructor for testability and maintainability. This follows dependency injection principles and makes your code more modular.
  • Use StateFlow for all UI-related state in ViewModels. This provides a reactive, lifecycle-aware way to manage state that works seamlessly with Jetpack Compose's collectAsState().
  • Keep all UI state centralized in the ViewModel, including input field values like zipcode, rather than managing it in composables.
  • Delegate data fetching operations to a Repository layer. The ViewModel should coordinate between the Repository and UI, handling success/failure states using Kotlin's Result type.
  • Use the ViewModel factory pattern when your ViewModel requires dependencies. This enables proper dependency injection and makes testing easier.
  • Always use viewModelScope for launching coroutines in ViewModels to ensure proper lifecycle management and automatic cancellation.
  • Use a consistent naming convention for mutable and immutable state variables (e.g., _weatherResponse for private mutable state and weatherResponse for the exposed immutable StateFlow).
  • Handle errors gracefully using Result types from repository calls, avoiding try-catch blocks when possible by using functional error handling with onSuccess and onFailure.