WeatherApp Screen
The MainActivity will call the WeatherAppScreen which will be our UI.
WeatherAppScreen.kt
@Composable
fun WeatherAppScreen() {
val weatherApiService = RetrofitClient.weatherApiService
val weatherRepository = remember { WeatherRepository(weatherApiService) }
val weatherViewModel: WeatherViewModel = viewModel(
factory = WeatherViewModel.provideFactory(weatherRepository)
)
// Collect states from the ViewModel
val zipcode by weatherViewModel.zipcode.collectAsState()
val weatherResponse by weatherViewModel.weatherResponse.collectAsState()
val errorMessage by weatherViewModel.errorMessage.collectAsState()
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
OutlinedTextField(
value = zipcode,
onValueChange = { weatherViewModel.onZipcodeChange(it) },
label = { Text("Enter Zip Code") },
singleLine = true,
modifier = Modifier.padding(bottom = 8.dp)
)
Button(onClick = { weatherViewModel.fetchWeather() }) {
Text("Get Weather")
}
when {
weatherResponse != null -> {
Column(modifier = Modifier.fillMaxWidth().padding(top = 16.dp), horizontalAlignment = Alignment.Start) {
Text("City: ${weatherResponse?.name}")
Text("Temperature: ${weatherResponse?.main?.temp}°C")
Text("Description: ${weatherResponse?.weather?.firstOrNull()?.description}")
Spacer(modifier = Modifier.height(16.dp))
Text("Other Info:")
Text("Temp Min: ${weatherResponse?.main?.temp_min}")
Text("Temp Max: ${weatherResponse?.main?.temp_max}")
Text("Pressure: ${weatherResponse?.main?.pressure}")
Text("Humidity: ${weatherResponse?.main?.humidity}")
Text("Wind Speed: ${weatherResponse?.wind?.speed}")
Text("Wind Direction: ${weatherResponse?.wind?.deg}")
Text("Clouds: ${weatherResponse?.clouds?.all}")
Text("Sunrise: ${weatherResponse?.sys?.sunrise}")
Text("Sunset: ${weatherResponse?.sys?.sunset}")
}
}
errorMessage != null -> {
Column(modifier = Modifier.fillMaxWidth().padding(top = 16.dp), horizontalAlignment = Alignment.Start) {
Text("No weather found for that zip code")
}
}
else -> {
Column(modifier = Modifier.fillMaxWidth().padding(top = 16.dp), horizontalAlignment = Alignment.Start) {
Text("Enter a zip code and press 'Get Weather'")
}
}
}
}
}
Code Explanation
The WeatherAppScreen composable function is the main UI component for our weather application. It observes UI state from a WeatherViewModel and handles user interactions, while all data fetching logic is delegated to the ViewModel and Repository layers.
State Management
The WeatherAppScreen composable function utilizes a WeatherViewModel to manage and expose all UI state, including the zipcode input, weather data, and error states. This promotes better separation of concerns and testability.
val weatherApiService = RetrofitClient.weatherApiService
val weatherRepository = remember { WeatherRepository(weatherApiService) }
val weatherViewModel: WeatherViewModel = viewModel(
factory = WeatherViewModel.provideFactory(weatherRepository)
)
// Collect states from the ViewModel
val zipcode by weatherViewModel.zipcode.collectAsState()
val weatherResponse by weatherViewModel.weatherResponse.collectAsState()
val errorMessage by weatherViewModel.errorMessage.collectAsState()
weatherApiService: The Retrofit API service interface for making network requests.weatherRepository: A repository instance created usingrememberto persist it across recompositions. The repository handles data fetching logic.weatherViewModel: An instance of the ViewModel created using theviewModelcomposable with a custom factory. The factory injects theweatherRepositorydependency into the ViewModel. The factory's primary role is to provide the WeatherRepository as a dependency to the WeatherViewModel. This means when the WeatherViewModel is created, the factory ensures it receives the necessary WeatherRepository instance to perform its data fetching operations.zipcode: AStateFlow<String>observed from the ViewModel usingcollectAsState(), storing the user-entered zip code. The zipcode state is fully managed by the ViewModel.weatherResponse: AStateFlow<WeatherResponse?>observed from the ViewModel, holding the fetched weather data. It will be null if no weather data is available or an error occurred.errorMessage: AStateFlow<String?>observed from the ViewModel, holding any error messages to be displayed to the user. It will be null if there is no current error.
Dependencies and Architecture
The application follows a clean architecture pattern with clear separation of concerns:
- Repository Pattern: The
WeatherRepositoryis created in the composable usingrememberto persist it across recompositions. It takes theweatherApiServiceas a dependency and handles all data fetching logic, including API key management and error handling. - ViewModel Factory: The
WeatherViewModel.provideFactory()method creates a factory that injects theweatherRepositoryinto the ViewModel. This allows for proper dependency injection and makes the ViewModel testable. - StateFlow: All state is managed using Kotlin's
StateFlow, which provides a reactive, lifecycle-aware way to observe state changes. The UI collects these states usingcollectAsState(), which automatically recomposes the UI when state changes. - Coroutine Scope: The ViewModel uses
viewModelScopefor launching coroutines, ensuring that any background work is automatically cancelled when the ViewModel is cleared, preventing memory leaks.
UI Layout and Components
The UI is structured using Jetpack Compose's Column composable.
// ... existing code ...
Column(
modifier = Modifier.align(Alignment.Center),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
// UI elements go here
}
}
// ... existing code ...
Column: Arranges its children vertically in the center of the screenboth vertically and horizontally.
Input Field
OutlinedTextField(
value = zipcode,
onValueChange = { weatherViewModel.onZipcodeChange(it) },
label = { Text("Enter Zip Code") },
singleLine = true,
modifier = Modifier.padding(bottom = 8.dp)
)
An OutlinedTextField allows the user to input a zip code. The value is bound to the zipcode state collected from the ViewModel, and onValueChange calls the ViewModel's onZipcodeChange() method to update the state in the ViewModel. This ensures all state management is centralized in the ViewModel.
Get Weather Button
Button(onClick = { weatherViewModel.fetchWeather() }) {
Text("Get Weather")
}
The "Get Weather" button triggers the data fetching process when clicked. The fetchWeather() method in the ViewModel doesn't require a parameter because it reads the current zipcode value from the ViewModel's internal state. This further demonstrates the separation of concerns—the UI simply triggers an action, and the ViewModel handles the rest.
Weather Data Fetching Logic
The core logic for fetching weather data follows this flow:
- The UI calls
weatherViewModel.fetchWeather()when the button is clicked. - The ViewModel's
fetchWeather()method reads the currentzipcodefrom its internal state and launches a coroutine usingviewModelScope. - The ViewModel calls
repository.getCurrentWeather(zipcode), which handles the actual network request. - The repository uses the
WeatherApiServiceto make the API call and returns aResult<WeatherResponse>. - The ViewModel updates its
_weatherResponseor_errorMessageStateFlow based on the result. - The UI automatically recomposes when the StateFlow values change, thanks to
collectAsState().
This architecture ensures that the UI composable is not directly involved in data fetching, network operations, or error handling. Instead, it simply triggers the ViewModel to perform these operations and observes the results via reactive StateFlows.
Displaying Errors, Loading, and Weather
The UI conditionally displays information based on the state observed from the WeatherViewModel. A when statement is used to handle different scenarios:
// ... existing code ...
when {
weatherResponse != null -> {
Column(modifier = Modifier.fillMaxWidth().padding(top = 16.dp), horizontalAlignment = Alignment.Start) {
Text("City: ${weatherResponse?.name}")
Text("Temperature: ${weatherResponse?.main?.temp}°C")
Text("Description: ${weatherResponse?.weather?.firstOrNull()?.description}")
Spacer(modifier = Modifier.height(16.dp))
Text("Other Info:")
Text("Temp Min: ${weatherResponse?.main?.temp_min}")
Text("Temp Max: ${weatherResponse?.main?.temp_max}")
Text("Pressure: ${weatherResponse?.main?.pressure}")
Text("Humidity: ${weatherResponse?.main?.humidity}")
Text("Wind Speed: ${weatherResponse?.wind?.speed}")
Text("Wind Direction: ${weatherResponse?.wind?.deg}")
Text("Clouds: ${weatherResponse?.clouds?.all}")
Text("Sunrise: ${weatherResponse?.sys?.sunrise}")
Text("Sunset: ${weatherResponse?.sys?.sunset}")
}
}
errorMessage != null -> {
Column(modifier = Modifier.fillMaxWidth().padding(top = 16.dp), horizontalAlignment = Alignment.Start) {
Text("No weather found for that zip code")
}
}
else -> {
Column(modifier = Modifier.fillMaxWidth().padding(top = 16.dp), horizontalAlignment = Alignment.Start) {
Text("Enter a zip code and press 'Get Weather'")
}
}
}
// ... existing code ...
-
Weather Data Display: If
weatherResponseis not null, aColumndisplays various weather details such as city, temperature, description, wind speed, etc. directly from the `weatherResponse` object. -
Error Message: If
errorMessageis not null, aTextcomposable displays the error message, indicating that no weather was found. - Initial State/Guidance: If neither `weatherResponse` nor `errorMessage` is present, a message prompts the user to enter a zip code and press the 'Get Weather' button.
Learning Aids
Tips for Success
- Keep your composables focused on UI and observe state from the ViewModel using
collectAsState(). - Use
StateFlowin your ViewModel for reactive state management. All UI-related state should be managed by the ViewModel, not stored locally in composables. - Use
rememberfor dependencies like repositories that should persist across recompositions but don't need to be part of the ViewModel's state. - Structure your UI using appropriate Jetpack Compose layout composables like
Column,Row, andBoxfor better organization and responsiveness. - Use a ViewModel factory pattern when your ViewModel requires dependencies, as shown in this example.
Common Mistakes to Avoid
- Performing complex business logic or directly initiating network requests within composables; this should be handled by the ViewModel and Repository.
- Storing UI state (like input field values) locally in composables using
mutableStateOfinstead of managing it in the ViewModel. The ViewModel should own all UI-related state. - Forgetting to use
collectAsState()when observing StateFlow values from the ViewModel in composables. - Not using a ViewModel factory when the ViewModel requires dependencies, leading to difficulties in testing and dependency injection.
- Creating deeply nested composable hierarchies, which can negatively impact readability, maintainability, and rendering performance.
- Not using
viewModelScopein the ViewModel for coroutines, which can lead to memory leaks if coroutines aren't properly cancelled.
Best Practices
- Follow the Repository pattern to abstract data sources. The Repository should handle API calls, caching, and data transformation, while the ViewModel coordinates between the Repository and UI.
- Use
StateFlowin ViewModels for all UI-related state. This provides a reactive, lifecycle-aware way to manage state that integrates seamlessly with Jetpack Compose'scollectAsState(). - Delegate all data fetching, error handling, and business logic to the ViewModel and Repository, keeping your composables lean and focused solely on UI.
- Use a ViewModel factory pattern when your ViewModel requires dependencies. This makes dependency injection clean and enables easier testing.
- Always use
viewModelScopefor launching coroutines in ViewModels to ensure proper lifecycle management and automatic cancellation. - Keep UI state management centralized in the ViewModel rather than scattering it across composables. This makes state predictable and easier to test.
- Break down large composables into smaller, focused, and reusable composables for better modularity and easier maintenance.