Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cache and Eviction Strategy: Offline First #41

Open
wants to merge 47 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
7756904
Added Dao, Weather Entity and Database
CalebKL May 14, 2023
6b81d5b
updated gradle dependency for room and Workmanager
CalebKL May 14, 2023
1039d32
DI for Local module and updated Manifest file to include notification…
CalebKL May 14, 2023
58916aa
Added Notification Util to update weather info to the user after 15 Min
CalebKL May 14, 2023
95d5e42
Updated mappers to include the new entity created
CalebKL May 14, 2023
f113abe
updated fetchWeather function to read data from cache and update afte…
CalebKL May 14, 2023
6f50a41
updated WeatherApp to getWorkManagerConfigs and included Timber for D…
CalebKL May 14, 2023
3d761b5
Added gson converter gradle dependency and created Converter class to…
CalebKL May 16, 2023
248820e
Added typeconverters to DI, Database and updated WeatherEntity Class
CalebKL May 16, 2023
b47104f
Updated mappers and DefaultWeatherRepository with fetching the correc…
CalebKL May 16, 2023
84a4bd1
Added testing dependencies
CalebKL May 17, 2023
545579a
Updated Fakes with the corrected mapped responses
CalebKL May 17, 2023
92499fd
Updated HomeViewModelIntegrationTest
CalebKL May 17, 2023
b6d13ce
Updated WeatherRepositoryUnitTest
CalebKL May 17, 2023
20e7cd9
Resolved Conflicts with develop branch
CalebKL May 17, 2023
50f9bb5
Resolved Conflicts with develop branch
CalebKL May 26, 2023
6e7b82d
Removed Gson and updated type converters
CalebKL May 26, 2023
67110cc
Refactored DAO to use relationships and updated entity and db
CalebKL May 26, 2023
7f65822
updated mappers with the populated weather
CalebKL May 26, 2023
7c5bd6d
Created usecase to sync updated weather and modified default weather …
CalebKL May 26, 2023
1faa9b5
removed raw strings and updated strings.xml
CalebKL May 26, 2023
078cfeb
removed timber config
CalebKL May 26, 2023
715ca3f
Added DI for the WeatherUpdateScheduler
CalebKL May 30, 2023
d63ad19
Fixed failing tests and updated Fakes
CalebKL May 30, 2023
738bda6
Merge branch 'develop' into offlineCache
CalebKL May 30, 2023
64e6c37
Merge branch 'develop' into offlineCache
CalebKL May 30, 2023
101ddb3
Adds Logger to DefaultWeatherRepository, HomeViewModelIntegrationTest…
CalebKL May 30, 2023
8f4f983
Adds Logger toHomeViewModelIntegrationTest and WeatherRepositoryUnitTest
CalebKL May 30, 2023
8d8a38b
Created RefreshWeatherUseCase Interface
CalebKL Jun 8, 2023
2fdf3c3
Refactored Usecase to implement the interface created and updated DI
CalebKL Jun 8, 2023
97f4dfb
Merge develop
CalebKL Jun 8, 2023
f928492
Merge develop
CalebKL Jun 8, 2023
bf60f85
Merge branch 'develop' into offlineCache
odaridavid Jun 10, 2023
521a1b8
created CustomWorkerFactory
CalebKL Jun 19, 2023
98335c6
Updated daos, database and entity
CalebKL Jun 19, 2023
7d8555e
Merge remote-tracking branch 'origin/offlineCache' into offlineCache
CalebKL Jun 19, 2023
ae05432
updated viewmodel component with singleton component
CalebKL Jun 19, 2023
710c0ad
Added latitude and longitude into the weather core model and updated …
CalebKL Jun 19, 2023
09a8f59
refactored weather update scheduler
CalebKL Jun 19, 2023
06314f4
deleted converters and replaced with room relations
CalebKL Jun 19, 2023
8b77bb8
refactored usecase
CalebKL Jun 19, 2023
e283bb2
removed type converters from the local module
CalebKL Jun 19, 2023
12e09e0
refactored repository to read daily and hourly lists
CalebKL Jun 19, 2023
e32d492
updated weather app with custom worker instead of hiltworker
CalebKL Jun 19, 2023
81f4d4b
Maoppers update
CalebKL Jun 19, 2023
c868b82
Updated Fakes with the Populated Weather
CalebKL Jun 19, 2023
15c0363
Fixed failing tests
CalebKL Jun 19, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,21 @@ dependencies {
testImplementation(libs.coroutines.test)
debugImplementation(libs.compose.ui.tooling)
debugImplementation(libs.compose.ui.test.manifest)
implementation(libs.androidx.work.testing)
implementation(libs.core.ktx)
testImplementation(libs.roboelectric)
testImplementation(libs.androidx.arch.core)
implementation(libs.core.ktx.test)

//Room
implementation(libs.room.ktx)
implementation(libs.room.runtime)
kapt(libs.room.compiler)

//WorkManager
implementation(libs.androidx.work.ktx)
implementation(libs.hilt.ext.work)
kapt(libs.hilt.compiler)

// chucker
debugImplementation(libs.chucker.debug)
Expand Down
2 changes: 1 addition & 1 deletion app/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
#-renamesourcefileattribute SourceFile
7 changes: 6 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>

<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<application
android:name=".WeatherApp"
android:allowBackup="true"
Expand All @@ -30,6 +30,11 @@
android:name="android.app.lib_name"
android:value="" />
</activity>
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
tools:node="remove">
</provider>
</application>

</manifest>
24 changes: 23 additions & 1 deletion app/src/main/java/com/github/odaridavid/weatherapp/WeatherApp.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,29 @@
package com.github.odaridavid.weatherapp

import android.app.Application
import androidx.hilt.work.HiltWorkerFactory
import androidx.work.Configuration
import com.github.odaridavid.weatherapp.worker.WeatherUpdateScheduler
import dagger.hilt.android.HiltAndroidApp
import javax.inject.Inject

@HiltAndroidApp
class WeatherApp : Application()
class WeatherApp : Application(), Configuration.Provider{
@Inject
lateinit var workerFactory: HiltWorkerFactory

@Inject
lateinit var weatherUpdateScheduler: WeatherUpdateScheduler

override fun onCreate() {
super.onCreate()
schedulePeriodicWeatherUpdates()
}

override fun getWorkManagerConfiguration(): Configuration =
Configuration.Builder().setWorkerFactory(workerFactory).build()

private fun schedulePeriodicWeatherUpdates() {
weatherUpdateScheduler.schedulePeriodicWeatherUpdates()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.github.odaridavid.weatherapp.core.api

import com.github.odaridavid.weatherapp.core.Result
import com.github.odaridavid.weatherapp.core.model.DefaultLocation
import com.github.odaridavid.weatherapp.core.model.Weather
import kotlinx.coroutines.flow.Flow

interface RefreshWeatherUseCase {
operator fun invoke(
defaultLocation: DefaultLocation,
language: String,
units: String
): Flow<Result<Weather>>
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package com.github.odaridavid.weatherapp.core.api

import com.github.odaridavid.weatherapp.core.Result
import com.github.odaridavid.weatherapp.core.model.DefaultLocation
import com.github.odaridavid.weatherapp.core.model.Weather
import com.github.odaridavid.weatherapp.core.Result
import kotlinx.coroutines.flow.Flow

interface WeatherRepository {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.github.odaridavid.weatherapp.data.weather

import android.content.Context
import com.github.odaridavid.weatherapp.R
import com.github.odaridavid.weatherapp.core.Result
import com.github.odaridavid.weatherapp.core.api.RefreshWeatherUseCase
import com.github.odaridavid.weatherapp.core.api.WeatherRepository
import com.github.odaridavid.weatherapp.core.model.DefaultLocation
import com.github.odaridavid.weatherapp.core.model.Weather
import com.github.odaridavid.weatherapp.util.NotificationUtil
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import javax.inject.Inject

class DefaultRefreshWeatherUseCase @Inject constructor(
private val weatherRepository: WeatherRepository,
private val notificationUtil: NotificationUtil,
private val context: Context
) :RefreshWeatherUseCase {
override operator fun invoke(
defaultLocation: DefaultLocation,
language: String,
units: String
): Flow<Result<Weather>> = flow{
weatherRepository.fetchWeatherData(
defaultLocation = defaultLocation,
language = language,
units =units
).collect{ result->
when (result) {
is Result.Success -> {
notificationUtil.makeNotification(context.getString(R.string.weather_updates))
}
is Result.Error -> {
R.string.error_generic
}
}
}

}
}






Original file line number Diff line number Diff line change
@@ -1,46 +1,61 @@
package com.github.odaridavid.weatherapp.data.weather

import com.github.odaridavid.weatherapp.BuildConfig
import com.github.odaridavid.weatherapp.core.ErrorType
import com.github.odaridavid.weatherapp.R
import com.github.odaridavid.weatherapp.core.Result
import com.github.odaridavid.weatherapp.core.api.Logger
import com.github.odaridavid.weatherapp.core.api.WeatherRepository
import com.github.odaridavid.weatherapp.core.model.DefaultLocation
import com.github.odaridavid.weatherapp.core.model.ExcludedData
import com.github.odaridavid.weatherapp.core.model.SupportedLanguage
import com.github.odaridavid.weatherapp.core.model.Weather
import com.github.odaridavid.weatherapp.data.weather.local.dao.WeatherDao
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flow
import java.io.IOException
import java.net.HttpURLConnection.HTTP_UNAUTHORIZED
import javax.inject.Inject

class DefaultWeatherRepository @Inject constructor(
private val openWeatherService: OpenWeatherService,
private val weatherDao: WeatherDao,
private val logger: Logger
) : WeatherRepository {

override fun fetchWeatherData(
defaultLocation: DefaultLocation,
language: String,
units: String
): Flow<Result<Weather>> = flow {

val cachedWeather = weatherDao.getWeather()
if (cachedWeather != null && cachedWeather.isDataValid()) {
val weatherData = cachedWeather.toCoreEntity(unit = units)
emit(Result.Success(data = weatherData))
return@flow
}
val excludedData = "${ExcludedData.MINUTELY.value},${ExcludedData.ALERTS.value}"

val response = openWeatherService.getWeatherData(
val apiResponse = openWeatherService.getWeatherData(
longitude = defaultLocation.longitude,
latitude = defaultLocation.latitude,
excludedInfo = excludedData,
units = units,
language = getLanguageValue(language),
appid = BuildConfig.OPEN_WEATHER_API_KEY
)

if (response.isSuccessful && response.body() != null) {
val weatherData = response.body()!!.toCoreModel(unit = units)
emit(Result.Success(data = weatherData))
val response = apiResponse.body()
if (apiResponse.isSuccessful && response != null) {
val currentWeather = apiResponse.body()!!.toCurrentWeatherEntity()
val hourlyEntity = apiResponse.body()!!.toHourlyEntity()
val dailyEntity = apiResponse.body()!!.toDailyEntity()
weatherDao.insertCurrentWeather(currentWeather)
weatherDao.insertHourlyWeather(hourlyEntity)
weatherDao.insertDailyWeather(dailyEntity)
val getWeather = weatherDao.getWeather()
val data = getWeather!!.toCoreEntity(unit = units)
emit(Result.Success(data =data))
} else {
val errorType = mapResponseCodeToErrorType(response.code())
val errorType = mapResponseCodeToErrorType(apiResponse.code())
emit(Result.Error(errorType = errorType))
}
}.catch { throwable: Throwable ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,19 @@ import com.github.odaridavid.weatherapp.core.model.Temperature
import com.github.odaridavid.weatherapp.core.model.Units
import com.github.odaridavid.weatherapp.core.model.Weather
import com.github.odaridavid.weatherapp.core.model.WeatherInfo
import com.github.odaridavid.weatherapp.data.weather.local.entity.DailyWeatherEntity
import com.github.odaridavid.weatherapp.data.weather.local.entity.HourlyWeatherEntity
import com.github.odaridavid.weatherapp.data.weather.local.entity.PopulatedWeather
import com.github.odaridavid.weatherapp.data.weather.local.entity.TemperatureEntity
import com.github.odaridavid.weatherapp.data.weather.local.entity.WeatherEntity
import com.github.odaridavid.weatherapp.data.weather.local.entity.WeatherInfoResponseEntity
import java.io.IOException
import java.net.HttpURLConnection
import java.text.SimpleDateFormat
import java.util.Date
import kotlin.math.roundToInt


fun WeatherResponse.toCoreModel(unit: String): Weather = Weather(
current = current.toCoreModel(unit = unit),
daily = daily.map { it.toCoreModel(unit = unit) },
Expand Down Expand Up @@ -55,6 +62,57 @@ fun TemperatureResponse.toCoreModel(unit: String): Temperature =
min = formatTemperatureValue(min, unit),
max = formatTemperatureValue(max, unit)
)
fun PopulatedWeather.toCoreEntity(unit: String): Weather =
Weather(
current = toCurrentWeather(unit = unit),
hourly = hourly.map{it.asCoreModel(unit = unit)},
daily = daily.map {
it.asCoreModel(unit = unit) }
)

private fun PopulatedWeather.toCurrentWeather(unit: String): CurrentWeather =
CurrentWeather(
temperature = formatTemperatureValue(current.temp, unit),
feelsLike = formatTemperatureValue(current.feels_like, unit),
weather = listOf(
WeatherInfo(
id = current.id,
main = current.main,
description = current.description,
icon = current.icon
)
)
)
fun DailyWeatherEntity.asCoreModel(unit: String): DailyWeather =
DailyWeather(
forecastedTime = getDate(dt,"EEEE dd/M"),
temperature = temperature.asCoreModel(unit = unit),
weather = weather.map { it.asCoreModel() }
)

fun HourlyWeatherEntity.asCoreModel(unit: String): HourlyWeather =
HourlyWeather(
forecastedTime = getDate(dt,"HH:SS"),
temperature = formatTemperatureValue(temperature, unit),
weather = weather.map { it.asCoreModel() }
)

fun WeatherInfoResponseEntity.asCoreModel(): WeatherInfo =
WeatherInfo(
id = id,
main = main,
description = description,
icon = "${BuildConfig.OPEN_WEATHER_ICONS_URL}[email protected]"
)
fun TemperatureEntity.asCoreModel(unit: String): Temperature =
Temperature(
min = formatTemperatureValue(min, unit),
max = formatTemperatureValue(max, unit)
)

fun WeatherInfoResponseEntity.toWeatherInfo(): WeatherInfo {
return WeatherInfo(id, main, description, icon)
}

private fun formatTemperatureValue(temperature: Float, unit: String): String =
"${temperature.roundToInt()}${getUnitSymbols(unit = unit)}"
Expand All @@ -72,6 +130,62 @@ private fun getDate(utcInMillis: Long, formatPattern: String): String {
return sdf.format(dateFormat)
}

fun WeatherResponse.toHourlyEntity():List<HourlyWeatherEntity> {
val hourlyWeatherEntities = hourly.map { hourlyResponse ->
HourlyWeatherEntity(
dt = hourlyResponse.forecastedTime,
temperature = hourlyResponse.temperature,
weather = hourlyResponse.weather.map { it.toWeatherInfoResponse() }
)
}
return hourlyWeatherEntities
}
fun WeatherResponse.toDailyEntity():List<DailyWeatherEntity> {
val dailyWeatherEntities = daily.map { dailyResponse ->
DailyWeatherEntity(
dt = dailyResponse.forecastedTime,
temperature = dailyResponse.temperature.toTemperatureEntity(),
weather = dailyResponse.weather.map { it.toWeatherInfoResponse() }
)
}
return dailyWeatherEntities
}

fun WeatherResponse.toCurrentWeatherEntity(): WeatherEntity {
val currentTime = System.currentTimeMillis()
val currentWeatherInfo = current.weather.first()

return WeatherEntity(
dt = currentTime,
id = 0,
feels_like = current.feelsLike,
temp = current.temperature,
temp_max = current.temperature,
temp_min = current.temperature,
description = currentWeatherInfo.description,
icon = currentWeatherInfo.icon,
main = currentWeatherInfo.main,
lastRefreshed = currentTime,
isValid = true,
)
}

private fun WeatherInfoResponse.toWeatherInfoResponse(): WeatherInfoResponseEntity {
return WeatherInfoResponseEntity(
id = id,
main = main,
description = description,
icon = icon
)
}

fun TemperatureResponse.toTemperatureEntity(): TemperatureEntity {
return TemperatureEntity(
min = min,
max = max
)
}

fun mapThrowableToErrorType(throwable: Throwable): ErrorType {
val errorType = when (throwable) {
is IOException -> ErrorType.IO_CONNECTION
Expand All @@ -85,4 +199,4 @@ fun mapResponseCodeToErrorType(code: Int): ErrorType = when (code) {
in 400..499 -> ErrorType.CLIENT
in 500..600 -> ErrorType.SERVER
else -> ErrorType.GENERIC
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.github.odaridavid.weatherapp.data.weather.local

import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import com.github.odaridavid.weatherapp.data.weather.local.converters.Converters
import com.github.odaridavid.weatherapp.data.weather.local.dao.WeatherDao
import com.github.odaridavid.weatherapp.data.weather.local.entity.DailyWeatherEntity
import com.github.odaridavid.weatherapp.data.weather.local.entity.HourlyWeatherEntity
import com.github.odaridavid.weatherapp.data.weather.local.entity.WeatherEntity

@Database(
entities = [
WeatherEntity::class,
HourlyWeatherEntity::class,
DailyWeatherEntity::class],
version = 5,
exportSchema = false
)
@TypeConverters(Converters::class)
abstract class WeatherDatabase: RoomDatabase(){
abstract fun weatherDao():WeatherDao
}
Loading