This is a simple weather app that uses the OpenWeatherMap API to get the weather data for a given place.
- Clone the repository
- Open the project in Android Studio
- Run
The app sends lands in a "Place Search" screen.
Place searching can be done in one of two ways:
- By beginning to type the place name. Google geocoding API will provide suggestions
- By entering the exact place name and clicking the send button
The weather screen will show the current weather data for the place that was searched for.
In case of an error - it will display an error message at the bottom of the screen. The error will disappear when refreshed and the error is resolved.
Note that a different background will appear in case of an error.
Clicking the refresh button will refresh the weather data for the current place.
Clicking the edit location button will take you back to the Place Search screen.
Clicking the temperature line will toggle between Fahrenheit and Celsius.
- Dependency injection: Koin
- Networking: Ktor
- JSON parsing: kotlinx.serialization
- Image loading: Coil
- UI: Jetpack Compose
- Navigation: Jetpack Navigation
Coroutines are used for asynchronous operations. The app is built around the ViewModel and StateFlow/SharedFlow objects.
Screens are governed by a single activity, with the navigation being handled by Jetpack Navigation.
Data is passed between screens via shared rememberSavable object. In this case, it it just the LocationModel object.
ViewModel - View coupling is implemented using MVI (Model-View-Intent) pattern.
MVI defines 3 pipes of communication between ViewModel and View:
- State - Represents the screen with data. Prepared by the ViewModel and consumed by the View via StateFlow. Typically, ViewModels would prepare States as close as possible to what the View requires, to avoid complex logic in the view. That is essential especially in Jetpack Compose, where recompositions are expensive.
- Effect/Action - Represents a one-time event that the View should act upon. Prepared by the ViewModel and consumed by the View via SharedFlow.
- Event - Reposted by the view to the viewModel. E.g. Button pressed.
PlaceSearchScreen
|
PlaceSearchViewModel
|
GeolocationRepo
|
GoogleGeocodingAPI
The screen is based on a chain of responsibility. The View posts events to the ViewModel, which issues requests to the GeolocationRepo, which in turn issues requests to the GoogleGeocodingAPI.
Responses return in the reverse order.
Project definitions require that the user should be able to search for a place for to display the weather data. The simplest, fastest and most reliable way to do that is via Google Geocoding API. The API provides a list of suggestions based on the user input.
Moreover, should the user enter an exact place name, the app should present it to OpenWeatherMap API, directly.
Therefore GeolocationRepo searches for the coordinates of a place given a string. It is piped via StateFlow to the PlaceSearchViewModel, which updates the state with the search results.
Should the user click a suggestion, the PlaceSearchViewModel will compose a location record based on the suggestion and the coordinates.
If the user clicks the send button, the PlaceSearchViewModel will compose a location record based on name only.
When a place is available, the PlaceSearchViewModel will post an effect to navigate to the Weather screen.
The Weather screen receives the location record via the shared rememberSavable object.
The WeatherScreen signals the WeatherViewModel. The view model fetches the weather data from the WeatherRepo.
WeatherViewModel subscribes to the WeatherRepo for the weather data.
The function WeatherRepo.getWeather:
- It is
suspendfunction - so it "returns" an error upon failure. - Updates the state flow with the weather data, such that the ViewModel can subscribe and update upon success, and will always have a last successful readout, if any.
WeatherRepo requests the data from the WeatherAPI - which, in turn, issues an HTTP GET request via KTOR. Since KTOR functions are suspend,
it does not block the UI thread. Eventually, WeatherAPI converts the response to a WeatherModel.
Note that the API key is stored in the manifest. The Secrets repo is used to fetch the key.
A more secure way could be to use the local.properties file, but for the sake of simplicity, the key is stored in the manifest.
The WeatherModel is posted to the WeatherViewModel, which prepares the state and posts it to the WeatherScreen.
WeatherAPI is injected to WeatherRepo, which is injected to WeatherViewModel.
WeatherViewModel prepares the state and posts it to the view.
The project is built - feature-first.
As such, the project is divided into screens. Each has its unique View, ViewModel and models.
The repositories are shared between the screens (though, practically for this project they could have been separate).
The following diagram shows the project hierarchy:
├── MainActivity.kt
├── WeatherApplication.kt
├── di Dependency injection
│ └── AppModule.kt
├── models Data models
│ ├── LocationModel.kt
│ └── WeatherModel.kt
├── repositories Shared repositories
│ ├── GeolocationRepo.kt
│ ├── Secrets.kt
│ ├── WeatherRepo.kt
│ ├── api API (Network) interfaces
│ │ ├── CallResult.kt
│ │ ├── NetworkingEngine.kt
│ │ ├── WeatherApi.kt
│ │ ├── impl - Implementations
│ │ │ ├── KtorNetworkingEngine.kt - KTOR engine as a NetworkingEngine
│ │ │ └── OpenWeatherApiImpl.kt - OpenWeatherMap API implementation
│ │ └── responses
│ │ └── WeatherApiResponse.kt - OpenWeatherMap API response
│ └── impl - Implementations
│ ├── GeolocationRepoImpl.kt
│ ├── SecretsImpl.kt
│ └── WeatherRepoImpl.kt
├── screens
│ ├── MainDestinations.kt
│ ├── MainScreen.kt - Main/Nav screen
│ ├── placesearch
│ │ ├── PlaceSearchScreen.kt
│ │ ├── SearchMviModels.kt - State, Effect, Event
│ │ └── SearchViewModel.kt
│ └── weather
│ ├── WeatherFormatters.kt - Formatters for the weather data
│ ├── WeatherMviModels.kt - State, Effect, Event
│ ├── WeatherScreen.kt
│ ├── WeatherViewModel.kt
│ └── components
│ ├── LabeledText.kt - See temperature box
│ └── WeatherScreenComponents.kt - Misc UI components
├── ui
│ └── theme
│ ├── Color.kt
│ ├── Theme.kt
│ └── Type.kt
└── utils
└── NumberUtils.kt