add caching and refactor code

This commit is contained in:
Henry Hiles 2023-04-07 11:13:19 -04:00
parent ff7710143b
commit 0b3254877b
11 changed files with 122 additions and 28 deletions

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

View file

@ -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
)
}
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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<HourlyWeatherInfo> {
suspend fun getHourlyWeatherData(
lat: Double,
long: Double,
cache: Boolean = true
): Resource<HourlyWeatherInfo> {
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<List<DailyWeatherData>> {
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()

View file

@ -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,

View file

@ -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,

View file

@ -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)

View file

@ -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)