Building The WeatherRepository
The WeatherRepository is responsible for fetching weather data from the network and caching it locally. It is also responsible for returning the cached data if the user is offline, that is not modeled in this example. In a real world application, you would likely have a more complex caching strategy, with different rules for when to fetch from the network and when to use the cached data.
class WeatherRepository(private val weatherApiService: WeatherApiService) {
// You would typically get the API key from a more secure location
// or through dependency injection in a real app.
private val API_KEY = "80d537a4b4cd7a3b10a3c65a70316965"
suspend fun getCurrentWeather(zipcode: String): Result {
return try {
val response = weatherApiService.getCurrentWeather(
zip = "$zipcode,us",
appId = API_KEY
)
Result.success(response)
} catch (e: Exception) {
Result.failure(e)
}
}
}
Code Explanation
The WeatherRepository is a crucial component in the clean architecture pattern. It acts as an abstraction layer between the data source (in this case, the network API) and the ViewModel. The Repository pattern ensures that the ViewModel doesn't need to know the details of how data is fetched—whether it comes from a network API, a local database, or a cache.
Dependency Injection
class WeatherRepository(private val weatherApiService: WeatherApiService): The Repository takes aWeatherApiService(a Retrofit interface) as a constructor parameter. This follows dependency injection principles, making the Repository testable and allowing you to swap out the actual implementation with a mock service during testing. TheWeatherApiServiceis provided by the Retrofit instance created inRetrofitClient.
API Key Management
private val API_KEY = "80d537a4b4cd7a3b10a3c65a70316965": The API key is stored as a private constant within the Repository. While this works for educational purposes, in a production application, you would typically:- Store the API key in a secure location (like
local.propertiesfor local builds or environment variables for CI/CD) - Use Android's
BuildConfigto inject API keys during the build process - Implement API key rotation and management strategies
- Never commit API keys to version control systems
- The API key is encapsulated within the Repository, meaning the ViewModel doesn't need to know or manage the API key—it's purely a data source concern.
getCurrentWeather Function
The getCurrentWeather function is the main entry point for fetching weather data. It encapsulates all the network request logic and error handling:
suspend fun getCurrentWeather(zipcode: String): Result<WeatherResponse>: This is a suspend function (meaning it can be called from a coroutine) that takes a zipcode as a parameter and returns aResult<WeatherResponse>. TheResulttype is a Kotlin sealed class that represents either success (with the data) or failure (with an exception), allowing for functional error handling without throwing exceptions.return try { ... } catch (e: Exception) { ... }: The function uses a try-catch block to handle any exceptions that might occur during the network request. This ensures that exceptions are converted intoResult.failure()rather than propagating up and potentially crashing the app.val response = weatherApiService.getCurrentWeather(zip = "$zipcode,us", appId = API_KEY): This line makes the actual network request using the injectedWeatherApiService. The zipcode is appended with",us"to specify the country code (United States), and the API key is provided for authentication. Since this is a suspend function call, it will suspend the coroutine until the network request completes.Result.success(response): If the network request succeeds without throwing an exception, the response is wrapped in aResult.success()and returned. This allows the ViewModel to handle success cases using.onSuccess { ... }.Result.failure(e): If an exception occurs (network error, timeout, etc.), it's caught and wrapped in aResult.failure(). This allows the ViewModel to handle errors using.onFailure { ... }without needing try-catch blocks.
Benefits of the Repository Pattern
- Abstraction: The Repository abstracts the data source implementation. If you later need to add caching or switch to a different API, only the Repository needs to change—the ViewModel remains unchanged.
- Testability: You can easily create a mock or fake repository for testing the ViewModel without making actual network calls.
- Single Responsibility: The Repository has one clear responsibility: fetching weather data. This makes the code easier to understand and maintain.
- Error Handling: The Repository centralizes error handling logic, ensuring consistent error handling throughout the application.
- Future Extensibility: The Repository is designed to be easily extended. For example, you could add caching logic that checks a local database before making a network request, all without changing the ViewModel code.
Learning Aids
Tips for Success
- Keep your Repository focused on data operations—fetching, caching, and transforming data. Avoid UI-related logic or business logic.
- Always use suspend functions for network operations in Repositories. This ensures proper integration with coroutines.
- Return
Resulttypes from Repository methods rather than throwing exceptions. This provides a functional approach to error handling that integrates well with coroutines. - Inject dependencies (like API services) through the constructor. This makes the Repository testable and follows dependency injection principles.
- Keep API keys and other sensitive data encapsulated within the Repository. Never expose them to the ViewModel or UI layers.
- Consider making the Repository an interface if you need multiple implementations (e.g., a real repository and a mock repository for testing).
Common Mistakes to Avoid
- Hardcoding API keys directly in the code. In production, use secure storage methods like
local.propertiesor environment variables. - Throwing exceptions from Repository methods instead of returning
Resulttypes. This can make error handling more complex and error-prone. - Mixing business logic or UI logic into the Repository. The Repository should only handle data operations.
- Making the Repository depend on Android-specific components (like
Context) unless absolutely necessary. Keep it platform-agnostic for better testability. - Not handling exceptions properly. Always catch exceptions in Repository methods and convert them to
Result.failure(). - Not making Repository functions suspend functions when they perform network operations. This can block threads and cause performance issues.
- Exposing mutable state or complex data structures from the Repository. Keep the Repository interface simple and focused on data fetching.
Best Practices
- Use the Repository pattern to abstract data sources. This provides a clean separation between the data layer and the business logic layer.
- Always return
Resulttypes from Repository methods that can fail. This enables functional error handling and makes error states explicit. - Use suspend functions for all asynchronous operations in Repositories. This ensures proper integration with coroutines and
viewModelScope. - Keep the Repository interface simple and focused. A Repository method should do one thing: fetch data, cache data, or transform data.
- Encapsulate sensitive information (like API keys) within the Repository. Never expose them to other layers of the application.
- Make the Repository easily testable by injecting dependencies through the constructor. This allows you to provide mock dependencies during testing.
- Consider implementing multiple data sources within a single Repository (e.g., network and local database) to create a single source of truth for your data.
- Handle all exceptions within the Repository and convert them to appropriate
Result.failure()responses. This prevents exceptions from propagating to the ViewModel layer. - In production applications, implement caching strategies within the Repository to improve performance and enable offline functionality.