add caching and refactor code
This commit is contained in:
parent
ff7710143b
commit
0b3254877b
11 changed files with 122 additions and 28 deletions
|
@ -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" />
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
Reference in a new issue