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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<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.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
package com.henryhiles.qweather
|
package com.henryhiles.qweather
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import com.henryhiles.qweather.di.appModule
|
import com.henryhiles.qweather.di.*
|
||||||
import com.henryhiles.qweather.di.locationModule
|
|
||||||
import com.henryhiles.qweather.di.repositoryModule
|
|
||||||
import org.koin.android.ext.koin.androidContext
|
import org.koin.android.ext.koin.androidContext
|
||||||
import org.koin.core.context.startKoin
|
import org.koin.core.context.startKoin
|
||||||
|
|
||||||
|
@ -13,7 +11,7 @@ class QWeather : Application() {
|
||||||
startKoin {
|
startKoin {
|
||||||
androidContext(this@QWeather)
|
androidContext(this@QWeather)
|
||||||
modules(
|
modules(
|
||||||
appModule, locationModule, repositoryModule
|
appModule, locationModule, repositoryModule, screenModelModule, managerModule
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,27 +1,67 @@
|
||||||
package com.henryhiles.qweather.di
|
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.domain.remote.WeatherApi
|
||||||
import com.henryhiles.qweather.presentation.screenmodel.AppearancePreferenceManager
|
import okhttp3.Cache
|
||||||
import com.henryhiles.qweather.presentation.screenmodel.AppearancePreferencesScreenModel
|
import okhttp3.Interceptor
|
||||||
import com.henryhiles.qweather.presentation.screenmodel.DailyWeatherScreenModel
|
import okhttp3.OkHttpClient.Builder
|
||||||
import com.henryhiles.qweather.presentation.screenmodel.HourlyWeatherScreenModel
|
|
||||||
import org.koin.core.module.dsl.factoryOf
|
|
||||||
import org.koin.core.module.dsl.singleOf
|
import org.koin.core.module.dsl.singleOf
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
import retrofit2.Retrofit
|
import retrofit2.Retrofit
|
||||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||||
import retrofit2.create
|
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 {
|
val appModule = module {
|
||||||
fun provideWeatherApi(): WeatherApi {
|
fun provideWeatherApi(context: Context): WeatherApi {
|
||||||
return Retrofit.Builder().baseUrl("https://api.open-meteo.com")
|
|
||||||
|
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()
|
.addConverterFactory(MoshiConverterFactory.create()).build().create()
|
||||||
}
|
}
|
||||||
|
|
||||||
singleOf(::provideWeatherApi)
|
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
|
package com.henryhiles.qweather.domain.remote
|
||||||
|
|
||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
|
import retrofit2.http.Headers
|
||||||
import retrofit2.http.Query
|
import retrofit2.http.Query
|
||||||
|
|
||||||
const val DAILY =
|
const val DAILY =
|
||||||
|
@ -9,11 +10,19 @@ const val HOURLY =
|
||||||
"hourly=temperature_2m,apparent_temperature,precipitation_probability,weathercode,windspeed_10m"
|
"hourly=temperature_2m,apparent_temperature,precipitation_probability,weathercode,windspeed_10m"
|
||||||
const val TIMEZONE = "timezone=auto"
|
const val TIMEZONE = "timezone=auto"
|
||||||
const val FORECAST_DAYS = "forecast_days=14"
|
const val FORECAST_DAYS = "forecast_days=14"
|
||||||
|
const val URL = "v1/forecast?$HOURLY&$DAILY&$TIMEZONE&$FORECAST_DAYS"
|
||||||
|
|
||||||
interface WeatherApi {
|
interface WeatherApi {
|
||||||
@GET("v1/forecast?$HOURLY&$DAILY&$TIMEZONE&$FORECAST_DAYS")
|
@GET(URL)
|
||||||
suspend fun getWeatherData(
|
suspend fun getWeatherData(
|
||||||
@Query("latitude") lat: Double,
|
@Query("latitude") lat: Double,
|
||||||
@Query("longitude") long: Double,
|
@Query("longitude") long: Double,
|
||||||
): WeatherDto
|
): 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
|
import com.henryhiles.qweather.domain.weather.HourlyWeatherInfo
|
||||||
|
|
||||||
class WeatherRepository constructor(private val api: WeatherApi) {
|
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 {
|
return try {
|
||||||
Resource.Success(
|
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) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
|
@ -21,14 +32,19 @@ class WeatherRepository constructor(private val api: WeatherApi) {
|
||||||
|
|
||||||
suspend fun getDailyWeatherData(
|
suspend fun getDailyWeatherData(
|
||||||
lat: Double,
|
lat: Double,
|
||||||
long: Double
|
long: Double,
|
||||||
|
cache: Boolean = true
|
||||||
): Resource<List<DailyWeatherData>> {
|
): Resource<List<DailyWeatherData>> {
|
||||||
return try {
|
return try {
|
||||||
Resource.Success(
|
Resource.Success(
|
||||||
data = api.getWeatherData(
|
(if (cache) api.getWeatherData(
|
||||||
lat = lat,
|
lat = lat,
|
||||||
long = long
|
long = long
|
||||||
).dailyWeatherData.toDailyWeatherDataMap()
|
) else api.getWeatherDataWithoutCache(
|
||||||
|
lat = lat,
|
||||||
|
long = long
|
||||||
|
))
|
||||||
|
.dailyWeatherData.toDailyWeatherDataMap()
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
|
|
|
@ -26,13 +26,17 @@ class DailyWeatherScreenModel constructor(
|
||||||
private set
|
private set
|
||||||
private var currentLocation: Location? = null
|
private var currentLocation: Location? = null
|
||||||
|
|
||||||
fun loadWeatherInfo() {
|
fun loadWeatherInfo(cache: Boolean = true) {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
state = state.copy(isLoading = true, error = null)
|
state = state.copy(isLoading = true, error = null)
|
||||||
currentLocation = locationTracker.getCurrentLocation()
|
currentLocation = locationTracker.getCurrentLocation()
|
||||||
currentLocation?.let { location ->
|
currentLocation?.let { location ->
|
||||||
state = when (val result =
|
state = when (val result =
|
||||||
repository.getDailyWeatherData(location.latitude, location.longitude)) {
|
repository.getDailyWeatherData(
|
||||||
|
lat = location.latitude,
|
||||||
|
long = location.longitude,
|
||||||
|
cache = cache
|
||||||
|
)) {
|
||||||
is Resource.Success -> {
|
is Resource.Success -> {
|
||||||
state.copy(
|
state.copy(
|
||||||
dailyWeatherData = result.data,
|
dailyWeatherData = result.data,
|
||||||
|
|
|
@ -24,13 +24,17 @@ class HourlyWeatherScreenModel constructor(
|
||||||
var state by mutableStateOf(HourlyWeatherState())
|
var state by mutableStateOf(HourlyWeatherState())
|
||||||
private set
|
private set
|
||||||
|
|
||||||
fun loadWeatherInfo() {
|
fun loadWeatherInfo(cache: Boolean = true) {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
state = state.copy(isLoading = true, error = null)
|
state = state.copy(isLoading = true, error = null)
|
||||||
val currentLocation = locationTracker.getCurrentLocation()
|
val currentLocation = locationTracker.getCurrentLocation()
|
||||||
currentLocation?.let { location ->
|
currentLocation?.let { location ->
|
||||||
state = when (val result =
|
state = when (val result =
|
||||||
repository.getHourlyWeatherData(location.latitude, location.longitude)) {
|
repository.getHourlyWeatherData(
|
||||||
|
lat = location.latitude,
|
||||||
|
long = location.longitude,
|
||||||
|
cache = cache
|
||||||
|
)) {
|
||||||
is Resource.Success -> {
|
is Resource.Success -> {
|
||||||
state.copy(
|
state.copy(
|
||||||
hourlyWeatherInfo = result.data,
|
hourlyWeatherInfo = result.data,
|
||||||
|
|
|
@ -95,7 +95,7 @@ object TodayTab : NavigationTab {
|
||||||
override fun Actions() {
|
override fun Actions() {
|
||||||
val viewModel: HourlyWeatherScreenModel = getScreenModel()
|
val viewModel: HourlyWeatherScreenModel = getScreenModel()
|
||||||
|
|
||||||
IconButton(onClick = { viewModel.loadWeatherInfo() }) {
|
IconButton(onClick = { viewModel.loadWeatherInfo(cache = false) }) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.Refresh,
|
imageVector = Icons.Filled.Refresh,
|
||||||
contentDescription = stringResource(R.string.action_reload)
|
contentDescription = stringResource(R.string.action_reload)
|
||||||
|
|
|
@ -98,7 +98,7 @@ object WeekTab : NavigationTab {
|
||||||
override fun Actions() {
|
override fun Actions() {
|
||||||
val viewModel: DailyWeatherScreenModel = getScreenModel()
|
val viewModel: DailyWeatherScreenModel = getScreenModel()
|
||||||
|
|
||||||
IconButton(onClick = { viewModel.loadWeatherInfo() }) {
|
IconButton(onClick = { viewModel.loadWeatherInfo(cache = false) }) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.Refresh,
|
imageVector = Icons.Filled.Refresh,
|
||||||
contentDescription = stringResource(R.string.action_reload)
|
contentDescription = stringResource(R.string.action_reload)
|
||||||
|
|
Reference in a new issue