From 0b3254877bb3c8ff15c5d95a7f2a709ebfb6dcb6 Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Fri, 7 Apr 2023 11:13:19 -0400 Subject: [PATCH] add caching and refactor code --- app/src/main/AndroidManifest.xml | 1 + .../java/com/henryhiles/qweather/QWeather.kt | 6 +- .../com/henryhiles/qweather/di/AppModule.kt | 64 +++++++++++++++---- .../henryhiles/qweather/di/ManagerModule.kt | 9 +++ .../qweather/di/ScreenModelModule.kt | 13 ++++ .../qweather/domain/remote/WeatherApi.kt | 11 +++- .../domain/repository/WeatherRepository.kt | 26 ++++++-- .../screenmodel/DailyWeatherScreenModel.kt | 8 ++- .../screenmodel/HourlyWeatherScreenModel.kt | 8 ++- .../qweather/presentation/tabs/TodayTab.kt | 2 +- .../qweather/presentation/tabs/WeekTab.kt | 2 +- 11 files changed, 122 insertions(+), 28 deletions(-) create mode 100644 app/src/main/java/com/henryhiles/qweather/di/ManagerModule.kt create mode 100644 app/src/main/java/com/henryhiles/qweather/di/ScreenModelModule.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 07c419e..95dd647 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,7 @@ + diff --git a/app/src/main/java/com/henryhiles/qweather/QWeather.kt b/app/src/main/java/com/henryhiles/qweather/QWeather.kt index 34e1a5e..65a9430 100644 --- a/app/src/main/java/com/henryhiles/qweather/QWeather.kt +++ b/app/src/main/java/com/henryhiles/qweather/QWeather.kt @@ -1,9 +1,7 @@ package com.henryhiles.qweather import android.app.Application -import com.henryhiles.qweather.di.appModule -import com.henryhiles.qweather.di.locationModule -import com.henryhiles.qweather.di.repositoryModule +import com.henryhiles.qweather.di.* import org.koin.android.ext.koin.androidContext import org.koin.core.context.startKoin @@ -13,7 +11,7 @@ class QWeather : Application() { startKoin { androidContext(this@QWeather) modules( - appModule, locationModule, repositoryModule + appModule, locationModule, repositoryModule, screenModelModule, managerModule ) } } diff --git a/app/src/main/java/com/henryhiles/qweather/di/AppModule.kt b/app/src/main/java/com/henryhiles/qweather/di/AppModule.kt index 7df3cac..addceb0 100644 --- a/app/src/main/java/com/henryhiles/qweather/di/AppModule.kt +++ b/app/src/main/java/com/henryhiles/qweather/di/AppModule.kt @@ -1,27 +1,67 @@ package com.henryhiles.qweather.di +import android.content.Context +import android.net.ConnectivityManager +import android.net.NetworkCapabilities +import android.os.Build import com.henryhiles.qweather.domain.remote.WeatherApi -import com.henryhiles.qweather.presentation.screenmodel.AppearancePreferenceManager -import com.henryhiles.qweather.presentation.screenmodel.AppearancePreferencesScreenModel -import com.henryhiles.qweather.presentation.screenmodel.DailyWeatherScreenModel -import com.henryhiles.qweather.presentation.screenmodel.HourlyWeatherScreenModel -import org.koin.core.module.dsl.factoryOf +import okhttp3.Cache +import okhttp3.Interceptor +import okhttp3.OkHttpClient.Builder import org.koin.core.module.dsl.singleOf import org.koin.dsl.module import retrofit2.Retrofit import retrofit2.converter.moshi.MoshiConverterFactory import retrofit2.create +private fun isNetworkAvailable(context: Context): Boolean { + val connectivityManager = + context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val networkCapabilities = connectivityManager.activeNetwork ?: return false + val activeNetwork = + connectivityManager.getNetworkCapabilities(networkCapabilities) ?: return false + return when { + activeNetwork.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true + activeNetwork.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true + activeNetwork.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true + else -> false + } + } else { + val activeNetworkInfo = connectivityManager.activeNetworkInfo ?: return false + return activeNetworkInfo.isConnected + } +} + val appModule = module { - fun provideWeatherApi(): WeatherApi { - return Retrofit.Builder().baseUrl("https://api.open-meteo.com") + fun provideWeatherApi(context: Context): WeatherApi { + + val cacheControlInterceptor = Interceptor { chain -> + val originalResponse = chain.proceed(chain.request()) + if (isNetworkAvailable(context)) { + val maxAge = 60 * 60 * 1 + originalResponse.newBuilder() + .header("Cache-Control", "public, max-age=$maxAge") + .build() + } else { + val maxStale = 60 * 60 * 24 * 14 + originalResponse.newBuilder() + .header("Cache-Control", "public, only-if-cached, max-stale=$maxStale") + .build() + } + } + + val cacheSize = 10 * 1024 * 1024 + val cache = Cache(context.cacheDir, cacheSize.toLong()) + val builder = Builder() + .cache(cache) + builder.networkInterceptors() + .add(cacheControlInterceptor) + val okHttpClient = builder.build() + + return Retrofit.Builder().baseUrl("https://api.open-meteo.com").client(okHttpClient) .addConverterFactory(MoshiConverterFactory.create()).build().create() } singleOf(::provideWeatherApi) - singleOf(::AppearancePreferenceManager) - - factoryOf(::AppearancePreferencesScreenModel) - factoryOf(::HourlyWeatherScreenModel) - factoryOf(::DailyWeatherScreenModel) } \ No newline at end of file diff --git a/app/src/main/java/com/henryhiles/qweather/di/ManagerModule.kt b/app/src/main/java/com/henryhiles/qweather/di/ManagerModule.kt new file mode 100644 index 0000000..64ab3b5 --- /dev/null +++ b/app/src/main/java/com/henryhiles/qweather/di/ManagerModule.kt @@ -0,0 +1,9 @@ +package com.henryhiles.qweather.di + +import com.henryhiles.qweather.presentation.screenmodel.AppearancePreferenceManager +import org.koin.core.module.dsl.singleOf +import org.koin.dsl.module + +val managerModule = module { + singleOf(::AppearancePreferenceManager) +} \ No newline at end of file diff --git a/app/src/main/java/com/henryhiles/qweather/di/ScreenModelModule.kt b/app/src/main/java/com/henryhiles/qweather/di/ScreenModelModule.kt new file mode 100644 index 0000000..80daadb --- /dev/null +++ b/app/src/main/java/com/henryhiles/qweather/di/ScreenModelModule.kt @@ -0,0 +1,13 @@ +package com.henryhiles.qweather.di + +import com.henryhiles.qweather.presentation.screenmodel.AppearancePreferencesScreenModel +import com.henryhiles.qweather.presentation.screenmodel.DailyWeatherScreenModel +import com.henryhiles.qweather.presentation.screenmodel.HourlyWeatherScreenModel +import org.koin.core.module.dsl.factoryOf +import org.koin.dsl.module + +val screenModelModule = module { + factoryOf(::AppearancePreferencesScreenModel) + factoryOf(::HourlyWeatherScreenModel) + factoryOf(::DailyWeatherScreenModel) +} \ No newline at end of file diff --git a/app/src/main/java/com/henryhiles/qweather/domain/remote/WeatherApi.kt b/app/src/main/java/com/henryhiles/qweather/domain/remote/WeatherApi.kt index a6c8876..88c32e5 100644 --- a/app/src/main/java/com/henryhiles/qweather/domain/remote/WeatherApi.kt +++ b/app/src/main/java/com/henryhiles/qweather/domain/remote/WeatherApi.kt @@ -1,6 +1,7 @@ package com.henryhiles.qweather.domain.remote import retrofit2.http.GET +import retrofit2.http.Headers import retrofit2.http.Query const val DAILY = @@ -9,11 +10,19 @@ const val HOURLY = "hourly=temperature_2m,apparent_temperature,precipitation_probability,weathercode,windspeed_10m" const val TIMEZONE = "timezone=auto" const val FORECAST_DAYS = "forecast_days=14" +const val URL = "v1/forecast?$HOURLY&$DAILY&$TIMEZONE&$FORECAST_DAYS" interface WeatherApi { - @GET("v1/forecast?$HOURLY&$DAILY&$TIMEZONE&$FORECAST_DAYS") + @GET(URL) suspend fun getWeatherData( @Query("latitude") lat: Double, @Query("longitude") long: Double, ): WeatherDto + + @Headers("Cache-Control: no-cache") + @GET(URL) + suspend fun getWeatherDataWithoutCache( + @Query("latitude") lat: Double, + @Query("longitude") long: Double, + ): WeatherDto } \ No newline at end of file diff --git a/app/src/main/java/com/henryhiles/qweather/domain/repository/WeatherRepository.kt b/app/src/main/java/com/henryhiles/qweather/domain/repository/WeatherRepository.kt index c2fe0fa..94c5a70 100644 --- a/app/src/main/java/com/henryhiles/qweather/domain/repository/WeatherRepository.kt +++ b/app/src/main/java/com/henryhiles/qweather/domain/repository/WeatherRepository.kt @@ -8,10 +8,21 @@ import com.henryhiles.qweather.domain.weather.DailyWeatherData import com.henryhiles.qweather.domain.weather.HourlyWeatherInfo class WeatherRepository constructor(private val api: WeatherApi) { - suspend fun getHourlyWeatherData(lat: Double, long: Double): Resource { + suspend fun getHourlyWeatherData( + lat: Double, + long: Double, + cache: Boolean = true + ): Resource { return try { Resource.Success( - data = api.getWeatherData(lat = lat, long = long).toHourlyWeatherInfo() + data = ( + if (cache) api.getWeatherData( + lat = lat, + long = long + ) else api.getWeatherDataWithoutCache( + lat = lat, + long = long + )).toHourlyWeatherInfo() ) } catch (e: Exception) { e.printStackTrace() @@ -21,14 +32,19 @@ class WeatherRepository constructor(private val api: WeatherApi) { suspend fun getDailyWeatherData( lat: Double, - long: Double + long: Double, + cache: Boolean = true ): Resource> { return try { Resource.Success( - data = api.getWeatherData( + (if (cache) api.getWeatherData( lat = lat, long = long - ).dailyWeatherData.toDailyWeatherDataMap() + ) else api.getWeatherDataWithoutCache( + lat = lat, + long = long + )) + .dailyWeatherData.toDailyWeatherDataMap() ) } catch (e: Exception) { e.printStackTrace() diff --git a/app/src/main/java/com/henryhiles/qweather/presentation/screenmodel/DailyWeatherScreenModel.kt b/app/src/main/java/com/henryhiles/qweather/presentation/screenmodel/DailyWeatherScreenModel.kt index 2c25c45..62b04ad 100644 --- a/app/src/main/java/com/henryhiles/qweather/presentation/screenmodel/DailyWeatherScreenModel.kt +++ b/app/src/main/java/com/henryhiles/qweather/presentation/screenmodel/DailyWeatherScreenModel.kt @@ -26,13 +26,17 @@ class DailyWeatherScreenModel constructor( private set private var currentLocation: Location? = null - fun loadWeatherInfo() { + fun loadWeatherInfo(cache: Boolean = true) { coroutineScope.launch { state = state.copy(isLoading = true, error = null) currentLocation = locationTracker.getCurrentLocation() currentLocation?.let { location -> state = when (val result = - repository.getDailyWeatherData(location.latitude, location.longitude)) { + repository.getDailyWeatherData( + lat = location.latitude, + long = location.longitude, + cache = cache + )) { is Resource.Success -> { state.copy( dailyWeatherData = result.data, diff --git a/app/src/main/java/com/henryhiles/qweather/presentation/screenmodel/HourlyWeatherScreenModel.kt b/app/src/main/java/com/henryhiles/qweather/presentation/screenmodel/HourlyWeatherScreenModel.kt index 19d18f4..8e72888 100644 --- a/app/src/main/java/com/henryhiles/qweather/presentation/screenmodel/HourlyWeatherScreenModel.kt +++ b/app/src/main/java/com/henryhiles/qweather/presentation/screenmodel/HourlyWeatherScreenModel.kt @@ -24,13 +24,17 @@ class HourlyWeatherScreenModel constructor( var state by mutableStateOf(HourlyWeatherState()) private set - fun loadWeatherInfo() { + fun loadWeatherInfo(cache: Boolean = true) { coroutineScope.launch { state = state.copy(isLoading = true, error = null) val currentLocation = locationTracker.getCurrentLocation() currentLocation?.let { location -> state = when (val result = - repository.getHourlyWeatherData(location.latitude, location.longitude)) { + repository.getHourlyWeatherData( + lat = location.latitude, + long = location.longitude, + cache = cache + )) { is Resource.Success -> { state.copy( hourlyWeatherInfo = result.data, diff --git a/app/src/main/java/com/henryhiles/qweather/presentation/tabs/TodayTab.kt b/app/src/main/java/com/henryhiles/qweather/presentation/tabs/TodayTab.kt index eb80f3a..6105498 100644 --- a/app/src/main/java/com/henryhiles/qweather/presentation/tabs/TodayTab.kt +++ b/app/src/main/java/com/henryhiles/qweather/presentation/tabs/TodayTab.kt @@ -95,7 +95,7 @@ object TodayTab : NavigationTab { override fun Actions() { val viewModel: HourlyWeatherScreenModel = getScreenModel() - IconButton(onClick = { viewModel.loadWeatherInfo() }) { + IconButton(onClick = { viewModel.loadWeatherInfo(cache = false) }) { Icon( imageVector = Icons.Filled.Refresh, contentDescription = stringResource(R.string.action_reload) diff --git a/app/src/main/java/com/henryhiles/qweather/presentation/tabs/WeekTab.kt b/app/src/main/java/com/henryhiles/qweather/presentation/tabs/WeekTab.kt index 16ba379..1ab9100 100644 --- a/app/src/main/java/com/henryhiles/qweather/presentation/tabs/WeekTab.kt +++ b/app/src/main/java/com/henryhiles/qweather/presentation/tabs/WeekTab.kt @@ -98,7 +98,7 @@ object WeekTab : NavigationTab { override fun Actions() { val viewModel: DailyWeatherScreenModel = getScreenModel() - IconButton(onClick = { viewModel.loadWeatherInfo() }) { + IconButton(onClick = { viewModel.loadWeatherInfo(cache = false) }) { Icon( imageVector = Icons.Filled.Refresh, contentDescription = stringResource(R.string.action_reload)