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
class WeatherViewModel(private val repository: WeatherRepository): The ViewModel takes aWeatherRepositoryas a constructor parameter, following dependency injection principles. This makes the ViewModel testable and decoupled from the data source implementation. The repository handles all network operations and API key management.
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():
private val _zipcode = MutableStateFlow(""): A private, mutableStateFlowthat holds the user-entered zip code. This state is managed by the ViewModel rather than the UI composable, ensuring centralized state management.val zipcode: StateFlow<String> = _zipcode.asStateFlow(): The public, immutableStateFlowthat exposes the zipcode to the UI. The UI observes this usingcollectAsState().private val _weatherResponse = MutableStateFlow<WeatherResponse?>(null): A private, mutableStateFlowthat holds the weather data. It's nullable, meaning it can hold weather data or be null if no data is available or an error occurs. The underscore prefix (`_`) indicates it's a mutable internal state.val weatherResponse: StateFlow<WeatherResponse?> = _weatherResponse.asStateFlow(): The public, immutableStateFlowthat exposes the weather data to the UI. Any changes to_weatherResponsewill automatically trigger UI recomposition when observed withcollectAsState().private val _errorMessage = MutableStateFlow<String?>(null): A private, mutableStateFlowthat holds any error messages. It's a nullableString, set to null when there's no error.val errorMessage: StateFlow<String?> = _errorMessage.asStateFlow(): The public, immutableStateFlowthat exposes error messages to the UI.
State Update Functions
fun onZipcodeChange(newZipcode: String): A function called by the UI when the user types in the zipcode input field. It updates the_zipcodestate, ensuring all state changes go through the ViewModel.
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.
fun fetchWeather(): The public function called by the UI to request weather data. Notice it doesn't take azipcodeparameter because it reads the current value from_zipcode.value, demonstrating that all state is centralized in the ViewModel.viewModelScope.launch { ... }: This launches a coroutine within the ViewModel's scope.viewModelScopeis a CoroutineScope tied to the ViewModel's lifecycle, meaning that any coroutines launched within it will be automatically cancelled when the ViewModel is cleared, preventing memory leaks._errorMessage.value = null: Before making a new API call, any previous error message is cleared to provide a clean state for the new request.repository.getCurrentWeather(_zipcode.value): This calls the repository'sgetCurrentWeathermethod, passing the current zipcode value. The repository handles all the details of the network request, including API key management and error handling. The repository returns aResult<WeatherResponse>, which is a Kotlin sealed class that represents either success or failure without throwing exceptions..onSuccess { response -> ... }: If the repository call is successful, this lambda receives theWeatherResponseand assigns it to_weatherResponse.value, which automatically updates the exposedweatherResponseStateFlow and triggers UI recomposition..onFailure { e -> ... }: If the repository call fails, this lambda receives the exception. The error message is extracted and assigned to_errorMessage.value, and_weatherResponse.valueis set to null to clear any previous weather data.
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.
companion object { ... }: In Kotlin, acompanion objectis like a static class in Java. It allows you to call methods directly on the class (e.g.,WeatherViewModel.provideFactory(repository)) without needing an instance of the class. This is the perfect place for a factory method.fun provideFactory(repository: WeatherRepository): ViewModelProvider.Factory: This is a static method that creates and returns a factory object. It takes theWeatherRepositorydependency as a parameter and "captures" it inside the factory object so it can be used later when creating the ViewModel.return object : ViewModelProvider.Factory { ... }: This creates an anonymous object that implements theViewModelProvider.Factoryinterface. An anonymous object is an object created on-the-fly without a name. This object must implement thecreatemethod defined by the interface.@Suppress("UNCHECKED_CAST"): This annotation suppresses a compiler warning about type casting. The warning appears because we're casting from a specific type (WeatherViewModel) to a generic type (T), but our type checking ensures it's safe.override fun <T : ViewModel> create(modelClass: Class<T>): T: This is the method required byViewModelProvider.Factory. When Android needs to create a ViewModel, it calls this method with the class type it wants to create. The generic typeTmust be a subclass ofViewModel.if (modelClass.isAssignableFrom(WeatherViewModel::class.java)): This checks if the requested ViewModel class isWeatherViewModelor a subclass of it.isAssignableFromreturnstrueif themodelClassis the same as or a subclass ofWeatherViewModel. This ensures we only create ViewModels we know how to handle.return WeatherViewModel(repository) as T: If the check passes, we create a newWeatherViewModelinstance, passing therepositorythat was captured when the factory was created. We then cast it to typeT(the generic type requested by Android) and return it. This is where dependency injection happens—the repository is injected into the ViewModel constructor.throw IllegalArgumentException("Unknown ViewModel class"): If someone tries to use this factory to create a different type of ViewModel (notWeatherViewModel), we throw an exception because this factory doesn't know how to create other ViewModel types.
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
StateFlowfor managing all UI state in the ViewModel. This provides reactive, lifecycle-aware state management that integrates well with Jetpack Compose. - Use
viewModelScopefor 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 immutableStateFlowcounterparts (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
MutableStateFlowinstead ofStateFlow), 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
mutableStateOfinstead ofStateFlowin 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
StateFlowfor all UI-related state in ViewModels. This provides a reactive, lifecycle-aware way to manage state that works seamlessly with Jetpack Compose'scollectAsState(). - 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
Resulttype. - Use the ViewModel factory pattern when your ViewModel requires dependencies. This enables proper dependency injection and makes testing easier.
- Always use
viewModelScopefor 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.,
_weatherResponsefor private mutable state andweatherResponsefor the exposed immutable StateFlow). - Handle errors gracefully using
Resulttypes from repository calls, avoiding try-catch blocks when possible by using functional error handling withonSuccessandonFailure.