diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 697d37c..f48e9d6 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -182,6 +182,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)
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index 2f9dc5a..ff59496 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -18,4 +18,4 @@
# If you keep the line number information, uncomment this to
# hide the original source file name.
-#-renamesourcefileattribute SourceFile
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 75570e1..dd75ec3 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -4,7 +4,7 @@
-
+
+
+
diff --git a/app/src/main/java/com/github/odaridavid/weatherapp/WeatherApp.kt b/app/src/main/java/com/github/odaridavid/weatherapp/WeatherApp.kt
index f9653be..17a70a6 100644
--- a/app/src/main/java/com/github/odaridavid/weatherapp/WeatherApp.kt
+++ b/app/src/main/java/com/github/odaridavid/weatherapp/WeatherApp.kt
@@ -1,7 +1,30 @@
package com.github.odaridavid.weatherapp
import android.app.Application
+import androidx.hilt.work.HiltWorkerFactory
+import androidx.work.Configuration
+import com.github.odaridavid.weatherapp.worker.CustomWorkerFactory
+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: CustomWorkerFactory
+
+ @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()
+ }
+}
diff --git a/app/src/main/java/com/github/odaridavid/weatherapp/core/api/RefreshWeatherUseCase.kt b/app/src/main/java/com/github/odaridavid/weatherapp/core/api/RefreshWeatherUseCase.kt
new file mode 100644
index 0000000..576b2bc
--- /dev/null
+++ b/app/src/main/java/com/github/odaridavid/weatherapp/core/api/RefreshWeatherUseCase.kt
@@ -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>
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/odaridavid/weatherapp/core/api/WeatherRepository.kt b/app/src/main/java/com/github/odaridavid/weatherapp/core/api/WeatherRepository.kt
index 31bd2f5..2a79f81 100644
--- a/app/src/main/java/com/github/odaridavid/weatherapp/core/api/WeatherRepository.kt
+++ b/app/src/main/java/com/github/odaridavid/weatherapp/core/api/WeatherRepository.kt
@@ -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 {
diff --git a/app/src/main/java/com/github/odaridavid/weatherapp/core/model/Weather.kt b/app/src/main/java/com/github/odaridavid/weatherapp/core/model/Weather.kt
index c5ad5b0..517bba4 100644
--- a/app/src/main/java/com/github/odaridavid/weatherapp/core/model/Weather.kt
+++ b/app/src/main/java/com/github/odaridavid/weatherapp/core/model/Weather.kt
@@ -1,6 +1,8 @@
package com.github.odaridavid.weatherapp.core.model
data class Weather(
+ val lat: Double,
+ val long: Double,
val current: CurrentWeather,
val hourly: List,
val daily: List
diff --git a/app/src/main/java/com/github/odaridavid/weatherapp/data/weather/DefaultRefreshWeatherUseCase.kt b/app/src/main/java/com/github/odaridavid/weatherapp/data/weather/DefaultRefreshWeatherUseCase.kt
new file mode 100644
index 0000000..657bf0c
--- /dev/null
+++ b/app/src/main/java/com/github/odaridavid/weatherapp/data/weather/DefaultRefreshWeatherUseCase.kt
@@ -0,0 +1,48 @@
+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 dagger.hilt.android.qualifiers.ApplicationContext
+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,
+ @ApplicationContext private val context: Context
+) :RefreshWeatherUseCase {
+ override operator fun invoke(
+ defaultLocation: DefaultLocation,
+ language: String,
+ units: String
+ ): Flow> = 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
+ }
+ }
+ }
+
+ }
+}
+
+
+
+
+
+
diff --git a/app/src/main/java/com/github/odaridavid/weatherapp/data/weather/DefaultWeatherRepository.kt b/app/src/main/java/com/github/odaridavid/weatherapp/data/weather/DefaultWeatherRepository.kt
index e7e4f7b..a5a6060 100644
--- a/app/src/main/java/com/github/odaridavid/weatherapp/data/weather/DefaultWeatherRepository.kt
+++ b/app/src/main/java/com/github/odaridavid/weatherapp/data/weather/DefaultWeatherRepository.kt
@@ -1,7 +1,6 @@
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.core.Result
import com.github.odaridavid.weatherapp.core.api.Logger
import com.github.odaridavid.weatherapp.core.api.WeatherRepository
@@ -9,6 +8,7 @@ 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
@@ -16,18 +16,26 @@ 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> = flow {
-
+ val cachedWeather = weatherDao.getWeather(
+ latitude = defaultLocation.latitude,
+ longitude = defaultLocation.longitude
+ )
+ 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,
@@ -35,12 +43,21 @@ class DefaultWeatherRepository @Inject constructor(
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()!!.toCurrentWeather()
+ val hourlyWeather = apiResponse.body()!!.toHourlyEntity()
+ val dailyWeather = apiResponse.body()!!.toDailyEntity()
+ val weatherInfo = apiResponse.body()!!.toWeatherInfoResponse()
+ val weatherEntity = apiResponse.body()!!.asCoreEntity()
+ weatherDao.insertCurrentWeather(currentWeather)
+ weatherDao.insertHourlyWeather(hourlyWeather)
+ weatherDao.insertDailyWeather(dailyWeather)
+ weatherDao.insertWeatherInfo(weatherInfo)
+ val result = weatherEntity.toCoreEntity(unit = units)
+ emit(Result.Success(data = result))
} else {
- val errorType = mapResponseCodeToErrorType(response.code())
+ val errorType = mapResponseCodeToErrorType(apiResponse.code())
emit(Result.Error(errorType = errorType))
}
}.catch { throwable: Throwable ->
diff --git a/app/src/main/java/com/github/odaridavid/weatherapp/data/weather/Mappers.kt b/app/src/main/java/com/github/odaridavid/weatherapp/data/weather/Mappers.kt
index 2d86966..47635fd 100644
--- a/app/src/main/java/com/github/odaridavid/weatherapp/data/weather/Mappers.kt
+++ b/app/src/main/java/com/github/odaridavid/weatherapp/data/weather/Mappers.kt
@@ -4,18 +4,32 @@ import com.github.odaridavid.weatherapp.BuildConfig
import com.github.odaridavid.weatherapp.core.ErrorType
import com.github.odaridavid.weatherapp.core.model.CurrentWeather
import com.github.odaridavid.weatherapp.core.model.DailyWeather
+import com.github.odaridavid.weatherapp.core.model.DefaultLocation
import com.github.odaridavid.weatherapp.core.model.HourlyWeather
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.CurrentWeatherEntity
+import com.github.odaridavid.weatherapp.data.weather.local.entity.CurrentWithWeatherInfo
+import com.github.odaridavid.weatherapp.data.weather.local.entity.DailyWeatherEntity
+import com.github.odaridavid.weatherapp.data.weather.local.entity.DailyWithWeatherInfo
+import com.github.odaridavid.weatherapp.data.weather.local.entity.HourlyWeatherEntity
+import com.github.odaridavid.weatherapp.data.weather.local.entity.HourlyWithWeatherInfo
+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(
+ lat = lat,
+ long = long,
current = current.toCoreModel(unit = unit),
daily = daily.map { it.toCoreModel(unit = unit) },
hourly = hourly.map { it.toCoreModel(unit = unit) }
@@ -49,12 +63,61 @@ fun WeatherInfoResponse.toCoreModel(): WeatherInfo =
description = description,
icon = "${BuildConfig.OPEN_WEATHER_ICONS_URL}$icon@2x.png"
)
+fun WeatherInfoResponse.toCoreEntity(): WeatherInfoResponseEntity =
+ WeatherInfoResponseEntity(
+ id = id,
+ main = main,
+ description = description,
+ icon = "${BuildConfig.OPEN_WEATHER_ICONS_URL}$icon@2x.png"
+ )
fun TemperatureResponse.toCoreModel(unit: String): Temperature =
Temperature(
min = formatTemperatureValue(min, unit),
max = formatTemperatureValue(max, unit)
)
+fun PopulatedWeather.toCoreEntity(unit: String): Weather =
+ Weather(
+ lat = weather.lat,
+ long =weather.lon,
+ current =current.asCoreEntity(unit = unit),
+ hourly = hourly.map{it.asCoreModel(unit = unit)},
+ daily = daily.map {
+ it.asCoreModel(unit = unit) }
+ )
+
+fun CurrentWithWeatherInfo.asCoreEntity(unit: String): CurrentWeather =
+ CurrentWeather(
+ temperature = formatTemperatureValue(currentWeather.temp, unit),
+ feelsLike = formatTemperatureValue(currentWeather.feelsLike, unit),
+ weather = weather.map { it.asCoreModel() }
+ )
+fun DailyWithWeatherInfo.asCoreModel(unit: String): DailyWeather =
+ DailyWeather(
+ forecastedTime = getDate(dailyWeatherEntity.dt,"EEEE dd/M"),
+ temperature = dailyWeatherEntity.temperature.asCoreModel(unit = unit),
+ weather = weather.map { it.asCoreModel() }
+ )
+
+fun HourlyWithWeatherInfo.asCoreModel(unit: String): HourlyWeather =
+ HourlyWeather(
+ forecastedTime = getDate(hourlyWeatherEntity.dt,"HH:SS"),
+ temperature = formatTemperatureValue(hourlyWeatherEntity.temperature, unit),
+ weather = weather.map { it.asCoreModel() }
+ )
+
+fun WeatherInfoResponseEntity.asCoreModel(): WeatherInfo =
+ WeatherInfo(
+ id = id,
+ main = main,
+ description = description,
+ icon = BuildConfig.OPEN_WEATHER_ICONS_URL
+ )
+fun TemperatureEntity.asCoreModel(unit: String): Temperature =
+ Temperature(
+ min = formatTemperatureValue(min, unit),
+ max = formatTemperatureValue(max, unit)
+ )
private fun formatTemperatureValue(temperature: Float, unit: String): String =
"${temperature.roundToInt()}${getUnitSymbols(unit = unit)}"
@@ -72,6 +135,110 @@ private fun getDate(utcInMillis: Long, formatPattern: String): String {
return sdf.format(dateFormat)
}
+fun WeatherResponse.asCoreEntity():PopulatedWeather =
+ PopulatedWeather(
+ weather = WeatherEntity(
+ lat = lat,
+ lon = long,
+ ),
+ current = current.asCoreEntity(),
+ daily = daily.map { it.asCoreEntity() },
+ hourly = hourly.map { it.asCoreEntity() }
+ )
+fun CurrentWeatherResponse.asCoreEntity(): CurrentWithWeatherInfo =
+ CurrentWithWeatherInfo(
+ currentWeather = CurrentWeatherEntity(
+ feelsLike = feelsLike,
+ temp = temperature
+ ),
+ weather = weather.map { it.toCoreEntity() }
+ )
+
+fun WeatherResponse.toCurrentWeather(): CurrentWeatherEntity =
+ CurrentWeatherEntity(
+ feelsLike = current.feelsLike,
+ temp = current.temperature
+ )
+fun WeatherResponse.toHourlyEntity():List {
+ val hourlyWeatherEntities = hourly.map { hourlyResponse ->
+ HourlyWeatherEntity(
+ dt = hourlyResponse.forecastedTime,
+ temperature = hourlyResponse.temperature,
+ )
+ }
+ return hourlyWeatherEntities
+}
+
+fun WeatherResponse.toDailyEntity():List {
+ val dailyWeatherEntities = daily.map { dailyResponse ->
+ DailyWeatherEntity(
+ dt = dailyResponse.forecastedTime,
+ temperature = dailyResponse.temperature.toTemperatureEntity(),
+ )
+ }
+ return dailyWeatherEntities
+}
+
+fun WeatherResponse.toWeatherInfoResponse(): List {
+ val currentWeatherInfoList = current.weather.map { weatherInfoResponse ->
+ WeatherInfoResponseEntity(
+ id = weatherInfoResponse.id,
+ main = weatherInfoResponse.main,
+ description = weatherInfoResponse.description,
+ icon = weatherInfoResponse.icon
+ )
+ }
+
+ val hourlyWeatherInfoList = hourly.flatMap { hourlyWeatherResponse ->
+ hourlyWeatherResponse.weather.map { weatherInfoResponse ->
+ WeatherInfoResponseEntity(
+ id = weatherInfoResponse.id,
+ main = weatherInfoResponse.main,
+ description = weatherInfoResponse.description,
+ icon = weatherInfoResponse.icon
+ )
+ }
+ }
+
+ val dailyWeatherInfoList = daily.flatMap { dailyWeatherResponse ->
+ dailyWeatherResponse.weather.map { weatherInfoResponse ->
+ WeatherInfoResponseEntity(
+ id = weatherInfoResponse.id,
+ main = weatherInfoResponse.main,
+ description = weatherInfoResponse.description,
+ icon = weatherInfoResponse.icon
+ )
+ }
+ }
+
+ return currentWeatherInfoList + hourlyWeatherInfoList + dailyWeatherInfoList
+}
+
+fun DailyWeatherResponse.asCoreEntity(): DailyWithWeatherInfo =
+ DailyWithWeatherInfo(
+ dailyWeatherEntity = DailyWeatherEntity(
+ dt = forecastedTime,
+ temperature = temperature.toTemperatureEntity()
+ ),
+ weather = weather.map { it.toCoreEntity() }
+ )
+
+fun HourlyWeatherResponse.asCoreEntity(): HourlyWithWeatherInfo =
+ HourlyWithWeatherInfo(
+ hourlyWeatherEntity = HourlyWeatherEntity(
+ dt = forecastedTime,
+ temperature =temperature,
+ ),
+ weather = weather.map { it.toCoreEntity() }
+ )
+
+fun TemperatureResponse.toTemperatureEntity(): TemperatureEntity {
+ return TemperatureEntity(
+ min = min,
+ max = max
+ )
+}
+
fun mapThrowableToErrorType(throwable: Throwable): ErrorType {
val errorType = when (throwable) {
is IOException -> ErrorType.IO_CONNECTION
@@ -85,4 +252,4 @@ fun mapResponseCodeToErrorType(code: Int): ErrorType = when (code) {
in 400..499 -> ErrorType.CLIENT
in 500..600 -> ErrorType.SERVER
else -> ErrorType.GENERIC
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/odaridavid/weatherapp/data/weather/WeatherResponse.kt b/app/src/main/java/com/github/odaridavid/weatherapp/data/weather/WeatherResponse.kt
index 7e4bbf5..2eca640 100644
--- a/app/src/main/java/com/github/odaridavid/weatherapp/data/weather/WeatherResponse.kt
+++ b/app/src/main/java/com/github/odaridavid/weatherapp/data/weather/WeatherResponse.kt
@@ -5,6 +5,8 @@ import kotlinx.serialization.Serializable
@Serializable
data class WeatherResponse(
+ @SerialName("lat") val lat: Double,
+ @SerialName("lon") val long: Double,
@SerialName("current") val current: CurrentWeatherResponse,
@SerialName("hourly") val hourly: List,
@SerialName("daily") val daily: List
diff --git a/app/src/main/java/com/github/odaridavid/weatherapp/data/weather/local/WeatherDatabase.kt b/app/src/main/java/com/github/odaridavid/weatherapp/data/weather/local/WeatherDatabase.kt
new file mode 100644
index 0000000..fb68a6a
--- /dev/null
+++ b/app/src/main/java/com/github/odaridavid/weatherapp/data/weather/local/WeatherDatabase.kt
@@ -0,0 +1,24 @@
+package com.github.odaridavid.weatherapp.data.weather.local
+
+import androidx.room.Database
+import androidx.room.RoomDatabase
+import com.github.odaridavid.weatherapp.data.weather.local.dao.WeatherDao
+import com.github.odaridavid.weatherapp.data.weather.local.entity.CurrentWeatherEntity
+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
+import com.github.odaridavid.weatherapp.data.weather.local.entity.WeatherInfoResponseEntity
+
+@Database(
+ entities = [
+ WeatherEntity::class,
+ HourlyWeatherEntity::class,
+ DailyWeatherEntity::class,
+ WeatherInfoResponseEntity::class,
+ CurrentWeatherEntity::class],
+ version = 6,
+ exportSchema = false
+)
+abstract class WeatherDatabase: RoomDatabase(){
+ abstract fun weatherDao():WeatherDao
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/odaridavid/weatherapp/data/weather/local/dao/WeatherDao.kt b/app/src/main/java/com/github/odaridavid/weatherapp/data/weather/local/dao/WeatherDao.kt
new file mode 100644
index 0000000..9e8d33e
--- /dev/null
+++ b/app/src/main/java/com/github/odaridavid/weatherapp/data/weather/local/dao/WeatherDao.kt
@@ -0,0 +1,34 @@
+package com.github.odaridavid.weatherapp.data.weather.local.dao
+
+import androidx.room.Dao
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+import androidx.room.Transaction
+import com.github.odaridavid.weatherapp.data.weather.local.entity.CurrentWeatherEntity
+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.WeatherEntity
+import com.github.odaridavid.weatherapp.data.weather.local.entity.WeatherInfoResponseEntity
+
+@Dao
+interface WeatherDao {
+
+ @Transaction
+ @Query("SELECT * FROM WeatherEntity WHERE lat = :latitude AND lon = :longitude")
+ suspend fun getWeather(latitude: Double, longitude: Double): PopulatedWeather?
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ suspend fun insertCurrentWeather(currentWeather: CurrentWeatherEntity)
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ suspend fun insertHourlyWeather(hourly: List)
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ suspend fun insertDailyWeather(daily: List)
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ suspend fun insertWeatherInfo(weatherResponse: List)
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/odaridavid/weatherapp/data/weather/local/entity/WeatherEntity.kt b/app/src/main/java/com/github/odaridavid/weatherapp/data/weather/local/entity/WeatherEntity.kt
new file mode 100644
index 0000000..821e1aa
--- /dev/null
+++ b/app/src/main/java/com/github/odaridavid/weatherapp/data/weather/local/entity/WeatherEntity.kt
@@ -0,0 +1,117 @@
+package com.github.odaridavid.weatherapp.data.weather.local.entity
+
+import androidx.room.Embedded
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import androidx.room.Relation
+
+data class PopulatedWeather(
+ @Embedded val weather: WeatherEntity,
+ @Relation(
+ entity = CurrentWeatherEntity::class,
+ parentColumn = "weatherId",
+ entityColumn = "currentId"
+ )
+ val current: CurrentWithWeatherInfo,
+ @Relation(
+ entity = HourlyWeatherEntity::class,
+ parentColumn = "weatherId",
+ entityColumn = "dt"
+ )
+ val hourly: List,
+ @Relation(
+ entity = DailyWeatherEntity::class,
+ parentColumn = "weatherId",
+ entityColumn = "dt"
+ )
+ val daily: List
+){
+ fun isDataValid(): Boolean {
+ val currentTime = System.currentTimeMillis()
+ val fifteenMinutesAgo = currentTime - 15 * 60 * 1000
+
+ val isCurrentValid = current.currentWeather.lastRefreshed >= fifteenMinutesAgo && current.currentWeather.isValid
+ val areHourlyValid = hourly.all { it.hourlyWeatherEntity.lastRefreshed >= fifteenMinutesAgo && it.hourlyWeatherEntity.isValid }
+ val areDailyValid = daily.all { it.dailyWeatherEntity.lastRefreshed >= fifteenMinutesAgo && it.dailyWeatherEntity.isValid }
+
+ return isCurrentValid && areHourlyValid && areDailyValid
+ }
+}
+
+@Entity
+data class WeatherEntity(
+ @PrimaryKey val weatherId: Int = 0,
+ val lat: Double,
+ val lon: Double,
+)
+@Entity
+data class CurrentWeatherEntity(
+ @PrimaryKey(autoGenerate = true)
+ val currentId: Long = 0L,
+ val feelsLike: Float,
+ val temp: Float,
+ val lastRefreshed: Long = 0,
+ val isValid: Boolean = false,
+)
+
+data class CurrentWithWeatherInfo(
+ @Embedded val currentWeather: CurrentWeatherEntity,
+ @Relation(
+ parentColumn = "currentId",
+ entityColumn = "id",
+ )
+ val weather: List
+)
+
+@Entity
+data class HourlyWeatherEntity(
+ @PrimaryKey(autoGenerate = true)
+ val hourlyId: Long = 0,
+ val dt: Long,
+ val temperature: Float,
+ val lastRefreshed: Long = 0,
+ val isValid: Boolean = false,
+)
+
+@Entity
+data class DailyWeatherEntity(
+ @PrimaryKey(autoGenerate = true)
+ val dailyId: Long = 0,
+ val dt: Long,
+ @Embedded
+ val temperature: TemperatureEntity,
+ val lastRefreshed: Long = 0,
+ val isValid: Boolean = false,
+)
+
+data class TemperatureEntity(
+ val min: Float,
+ val max: Float
+)
+
+data class HourlyWithWeatherInfo(
+ @Embedded val hourlyWeatherEntity: HourlyWeatherEntity,
+ @Relation(
+ parentColumn = "hourlyId",
+ entityColumn = "id",
+ )
+ val weather: List
+)
+
+data class DailyWithWeatherInfo(
+ @Embedded val dailyWeatherEntity: DailyWeatherEntity,
+ @Relation(
+ parentColumn = "dailyId",
+ entityColumn = "id",
+ )
+ val weather: List
+)
+
+@Entity
+data class WeatherInfoResponseEntity(
+ @PrimaryKey
+ val id: Int,
+ val main: String,
+ val description: String,
+ val icon: String
+)
diff --git a/app/src/main/java/com/github/odaridavid/weatherapp/di/LocalModule.kt b/app/src/main/java/com/github/odaridavid/weatherapp/di/LocalModule.kt
new file mode 100644
index 0000000..a115e43
--- /dev/null
+++ b/app/src/main/java/com/github/odaridavid/weatherapp/di/LocalModule.kt
@@ -0,0 +1,51 @@
+package com.github.odaridavid.weatherapp.di
+
+import android.content.Context
+import androidx.room.Room
+import androidx.work.WorkManager
+import com.github.odaridavid.weatherapp.data.weather.local.WeatherDatabase
+import com.github.odaridavid.weatherapp.data.weather.local.dao.WeatherDao
+import com.github.odaridavid.weatherapp.util.NotificationUtil
+import com.github.odaridavid.weatherapp.worker.WeatherUpdateScheduler
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+object LocalModule {
+ @Provides
+ @Singleton
+ fun provideWeatherDatabase(@ApplicationContext app: Context): WeatherDatabase {
+ return Room
+ .databaseBuilder(app, WeatherDatabase::class.java, "weather_database")
+ .build()
+ }
+
+ @Singleton
+ @Provides
+ fun provideNotificationUtil(@ApplicationContext context: Context): NotificationUtil {
+ return NotificationUtil(context)
+ }
+
+ @Singleton
+ @Provides
+ fun provideWeatherUpdateScheduler(workManager: WorkManager): WeatherUpdateScheduler {
+ return WeatherUpdateScheduler(workManager)
+ }
+
+ @Singleton
+ @Provides
+ fun provideWorkManager(@ApplicationContext context: Context): WorkManager {
+ return WorkManager.getInstance(context)
+ }
+
+ @Provides
+ @Singleton
+ fun providesWeatherDao(db: WeatherDatabase): WeatherDao {
+ return db.weatherDao()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/odaridavid/weatherapp/di/RepositoryModule.kt b/app/src/main/java/com/github/odaridavid/weatherapp/di/RepositoryModule.kt
index 51404e3..d024db8 100644
--- a/app/src/main/java/com/github/odaridavid/weatherapp/di/RepositoryModule.kt
+++ b/app/src/main/java/com/github/odaridavid/weatherapp/di/RepositoryModule.kt
@@ -1,18 +1,21 @@
package com.github.odaridavid.weatherapp.di
+import com.github.odaridavid.weatherapp.core.api.RefreshWeatherUseCase
import com.github.odaridavid.weatherapp.core.api.Logger
import com.github.odaridavid.weatherapp.core.api.SettingsRepository
import com.github.odaridavid.weatherapp.data.weather.DefaultWeatherRepository
import com.github.odaridavid.weatherapp.core.api.WeatherRepository
import com.github.odaridavid.weatherapp.data.settings.DefaultSettingsRepository
import com.github.odaridavid.weatherapp.data.weather.FirebaseLogger
+import com.github.odaridavid.weatherapp.data.weather.DefaultRefreshWeatherUseCase
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ViewModelComponent
+import dagger.hilt.components.SingletonComponent
@Module
-@InstallIn(ViewModelComponent::class)
+@InstallIn(SingletonComponent::class)
interface RepositoryModule {
@Binds
@@ -24,4 +27,6 @@ interface RepositoryModule {
@Binds
fun bindFirebaseLogger(logger: FirebaseLogger): Logger
+ @Binds
+ fun bindIRefreshWeatherUseCase(refreshWeatherUseCase: DefaultRefreshWeatherUseCase) : RefreshWeatherUseCase
}
diff --git a/app/src/main/java/com/github/odaridavid/weatherapp/ui/home/HomeViewModel.kt b/app/src/main/java/com/github/odaridavid/weatherapp/ui/home/HomeViewModel.kt
index 2520a8f..99e436d 100644
--- a/app/src/main/java/com/github/odaridavid/weatherapp/ui/home/HomeViewModel.kt
+++ b/app/src/main/java/com/github/odaridavid/weatherapp/ui/home/HomeViewModel.kt
@@ -3,13 +3,11 @@ package com.github.odaridavid.weatherapp.ui.home
import androidx.annotation.StringRes
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import com.github.odaridavid.weatherapp.R
-import com.github.odaridavid.weatherapp.core.ErrorType
+import com.github.odaridavid.weatherapp.core.Result
import com.github.odaridavid.weatherapp.core.model.Weather
import com.github.odaridavid.weatherapp.core.api.SettingsRepository
import com.github.odaridavid.weatherapp.core.api.WeatherRepository
import com.github.odaridavid.weatherapp.core.model.DefaultLocation
-import com.github.odaridavid.weatherapp.core.Result
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -57,7 +55,6 @@ class HomeViewModel @Inject constructor(
}
}
}
-
is HomeScreenIntent.DisplayCityName -> {
setState { copy(locationName = homeScreenIntent.cityName) }
}
@@ -93,6 +90,7 @@ class HomeViewModel @Inject constructor(
_state.emit(stateReducer(state.value))
}
}
+
}
data class HomeScreenViewState(
diff --git a/app/src/main/java/com/github/odaridavid/weatherapp/util/NotificationUtil.kt b/app/src/main/java/com/github/odaridavid/weatherapp/util/NotificationUtil.kt
new file mode 100644
index 0000000..8a586e9
--- /dev/null
+++ b/app/src/main/java/com/github/odaridavid/weatherapp/util/NotificationUtil.kt
@@ -0,0 +1,36 @@
+package com.github.odaridavid.weatherapp.util
+
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.content.Context
+import android.os.Build
+import androidx.core.app.NotificationCompat
+import com.github.odaridavid.weatherapp.R
+import javax.inject.Inject
+
+class NotificationUtil @Inject constructor(
+ private val context: Context
+) {
+ fun makeNotification(message: String) {
+ createNotificationChannel()
+
+ val builder = NotificationCompat.Builder(context, "default")
+ .setSmallIcon(R.drawable.ic_launcher_foreground)
+ .setContentText(message)
+ .setPriority(NotificationCompat.PRIORITY_DEFAULT)
+ }
+
+ private fun createNotificationChannel() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val name = context.getString(R.string.weather_notification)
+ val descriptionText = context.getString(R.string.weather_updates)
+ val importance = NotificationManager.IMPORTANCE_DEFAULT
+ val channel = NotificationChannel("default", name, importance).apply {
+ description = descriptionText
+ }
+ val notificationManager: NotificationManager =
+ context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ notificationManager.createNotificationChannel(channel)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/odaridavid/weatherapp/worker/CustomWorkerFactory.kt b/app/src/main/java/com/github/odaridavid/weatherapp/worker/CustomWorkerFactory.kt
new file mode 100644
index 0000000..0831e78
--- /dev/null
+++ b/app/src/main/java/com/github/odaridavid/weatherapp/worker/CustomWorkerFactory.kt
@@ -0,0 +1,25 @@
+package com.github.odaridavid.weatherapp.worker
+
+import android.content.Context
+import androidx.work.ListenableWorker
+import androidx.work.WorkerFactory
+import androidx.work.WorkerParameters
+import com.github.odaridavid.weatherapp.core.api.SettingsRepository
+import com.github.odaridavid.weatherapp.data.weather.DefaultRefreshWeatherUseCase
+import javax.inject.Inject
+
+class CustomWorkerFactory @Inject constructor(
+ private val refreshWeatherUseCase: DefaultRefreshWeatherUseCase,
+ private val settingsRepository: SettingsRepository
+): WorkerFactory() {
+ override fun createWorker(
+ appContext: Context,
+ workerClassName: String,
+ workerParameters: WorkerParameters
+ ): ListenableWorker = UpdateWeatherWorker(
+ context = appContext,
+ params = workerParameters,
+ refreshWeatherUseCase = refreshWeatherUseCase,
+ settingsRepository =settingsRepository
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/odaridavid/weatherapp/worker/UpdatedWeatherWorker.kt b/app/src/main/java/com/github/odaridavid/weatherapp/worker/UpdatedWeatherWorker.kt
new file mode 100644
index 0000000..1c121a0
--- /dev/null
+++ b/app/src/main/java/com/github/odaridavid/weatherapp/worker/UpdatedWeatherWorker.kt
@@ -0,0 +1,35 @@
+package com.github.odaridavid.weatherapp.worker
+
+import android.content.Context
+import androidx.hilt.work.HiltWorker
+import androidx.work.CoroutineWorker
+import androidx.work.WorkerParameters
+import com.github.odaridavid.weatherapp.core.api.SettingsRepository
+import com.github.odaridavid.weatherapp.data.weather.DefaultRefreshWeatherUseCase
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.flow.first
+
+@HiltWorker
+class UpdateWeatherWorker @AssistedInject constructor(
+ @Assisted context: Context,
+ @Assisted params: WorkerParameters,
+ private val refreshWeatherUseCase: DefaultRefreshWeatherUseCase,
+ private val settingsRepository: SettingsRepository
+): CoroutineWorker(context, params){
+
+ override suspend fun doWork(): Result {
+ return try {
+ val defaultLocation = settingsRepository.getDefaultLocation().first()
+ val language = settingsRepository.getLanguage().first()
+ val units = settingsRepository.getUnits().first()
+ refreshWeatherUseCase.invoke(
+ defaultLocation = defaultLocation,
+ language = language,
+ units = units)
+ Result.success()
+ } catch(e: Error) {
+ Result.retry()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/odaridavid/weatherapp/worker/WeatherUpdateScheduler.kt b/app/src/main/java/com/github/odaridavid/weatherapp/worker/WeatherUpdateScheduler.kt
new file mode 100644
index 0000000..d0757d3
--- /dev/null
+++ b/app/src/main/java/com/github/odaridavid/weatherapp/worker/WeatherUpdateScheduler.kt
@@ -0,0 +1,26 @@
+package com.github.odaridavid.weatherapp.worker
+
+import androidx.work.Constraints
+import androidx.work.NetworkType
+import androidx.work.PeriodicWorkRequestBuilder
+import androidx.work.WorkManager
+import java.util.concurrent.TimeUnit
+import javax.inject.Inject
+
+class WeatherUpdateScheduler @Inject constructor(
+ private val workManager: WorkManager
+) {
+ private val constraints = Constraints.Builder()
+ .setRequiredNetworkType(NetworkType.CONNECTED)
+ .build()
+
+ fun schedulePeriodicWeatherUpdates() {
+ val refreshWeatherRequest = PeriodicWorkRequestBuilder(
+ repeatInterval = 15,
+ repeatIntervalTimeUnit = TimeUnit.MINUTES)
+ .setConstraints(constraints)
+ .build()
+
+ workManager.enqueue(refreshWeatherRequest)
+ }
+}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index fd9eb90..df581ad 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -30,4 +30,6 @@
Oops! Something is wrong on our end :(
Something is happening that\'s disturbing the force :(
Check your internet connection and try again
+ Weather Notification
+ Weather Updates
diff --git a/app/src/test/java/com/github/odaridavid/weatherapp/Fakes.kt b/app/src/test/java/com/github/odaridavid/weatherapp/Fakes.kt
index d837b42..3886bbe 100644
--- a/app/src/test/java/com/github/odaridavid/weatherapp/Fakes.kt
+++ b/app/src/test/java/com/github/odaridavid/weatherapp/Fakes.kt
@@ -2,25 +2,93 @@ package com.github.odaridavid.weatherapp
import com.github.odaridavid.weatherapp.core.model.CurrentWeather
import com.github.odaridavid.weatherapp.core.model.Weather
+import com.github.odaridavid.weatherapp.core.model.WeatherInfo
import com.github.odaridavid.weatherapp.data.weather.CurrentWeatherResponse
+import com.github.odaridavid.weatherapp.data.weather.WeatherInfoResponse
import com.github.odaridavid.weatherapp.data.weather.WeatherResponse
+import com.github.odaridavid.weatherapp.data.weather.local.entity.CurrentWeatherEntity
+import com.github.odaridavid.weatherapp.data.weather.local.entity.CurrentWithWeatherInfo
+import com.github.odaridavid.weatherapp.data.weather.local.entity.PopulatedWeather
+import com.github.odaridavid.weatherapp.data.weather.local.entity.WeatherEntity
+import com.github.odaridavid.weatherapp.data.weather.local.entity.WeatherInfoResponseEntity
val fakeSuccessWeatherResponse = WeatherResponse(
current = CurrentWeatherResponse(
temperature = 3.0f,
feelsLike = 2.0f,
- weather = listOf()
+ weather = listOf(
+ WeatherInfoResponse(
+ id = 1,
+ main = "main",
+ description = "desc",
+ icon = "icon"
+ )
+ )
),
hourly = listOf(),
- daily = listOf()
+ daily = listOf(),
+ lat = 0.00,
+ long = 0.00
)
val fakeSuccessMappedWeatherResponse = Weather(
current = CurrentWeather(
temperature = "3°C",
feelsLike = "2°C",
- weather = listOf()
+ weather = listOf(
+ WeatherInfo(
+ id = 1,
+ main = "main",
+ description = "desc",
+ icon = BuildConfig.OPEN_WEATHER_ICONS_URL
+ )
+ )
),
hourly = listOf(),
- daily = listOf()
+ daily = listOf(),
+ lat = 0.00,
+ long = 0.00
)
+val fakeSuccessResponse = Weather(
+ current = CurrentWeather(
+ temperature = "3°C",
+ feelsLike = "2°C",
+ weather = listOf(
+ WeatherInfo(
+ id = 1,
+ main = "main",
+ description = "desc",
+ icon = BuildConfig.OPEN_WEATHER_ICONS_URL
+ )
+ )
+ ),
+ hourly = listOf(),
+ daily = listOf(),
+ lat = 0.00,
+ long = 0.00
+)
+
+val fakePopulatedResponse = PopulatedWeather(
+ weather = WeatherEntity(
+ weatherId = 0,
+ lat = 0.00,
+ lon = 0.00
+ ),
+ current = CurrentWithWeatherInfo(
+ currentWeather = CurrentWeatherEntity(
+ currentId = 0,
+ feelsLike = 1f,
+ temp = 1f,
+ ),
+ weather = listOf(
+ WeatherInfoResponseEntity(
+ id = 1,
+ main = "main",
+ description = "desc",
+ icon = "icon"
+ )
+ )
+ ),
+ hourly = listOf(),
+ daily = listOf()
+)
\ No newline at end of file
diff --git a/app/src/test/java/com/github/odaridavid/weatherapp/HomeViewModelIntegrationTest.kt b/app/src/test/java/com/github/odaridavid/weatherapp/HomeViewModelIntegrationTest.kt
index dcd1369..37a639d 100644
--- a/app/src/test/java/com/github/odaridavid/weatherapp/HomeViewModelIntegrationTest.kt
+++ b/app/src/test/java/com/github/odaridavid/weatherapp/HomeViewModelIntegrationTest.kt
@@ -1,5 +1,6 @@
package com.github.odaridavid.weatherapp
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import app.cash.turbine.test
import com.github.odaridavid.weatherapp.core.api.Logger
import com.github.odaridavid.weatherapp.core.api.WeatherRepository
@@ -7,6 +8,7 @@ import com.github.odaridavid.weatherapp.core.model.DefaultLocation
import com.github.odaridavid.weatherapp.data.weather.DefaultWeatherRepository
import com.github.odaridavid.weatherapp.data.weather.OpenWeatherService
import com.github.odaridavid.weatherapp.data.weather.WeatherResponse
+import com.github.odaridavid.weatherapp.data.weather.local.dao.WeatherDao
import com.github.odaridavid.weatherapp.ui.home.HomeScreenIntent
import com.github.odaridavid.weatherapp.ui.home.HomeScreenViewState
import com.github.odaridavid.weatherapp.ui.home.HomeViewModel
@@ -31,11 +33,17 @@ class HomeViewModelIntegrationTest {
val mockOpenWeatherService = mockk(relaxed = true)
@MockK
- val mockLogger = mockk(relaxed = true)
+ val mockWeatherDao = mockk(relaxed = true)
@get:Rule
val coroutineRule = MainCoroutineRule()
+ @MockK
+ val mockLogger = mockk(relaxed = true)
+
+ @get:Rule
+ val instantExecutorRule = InstantTaskExecutorRule()
+
@Test
fun `when fetching weather data is successful, then display correct data`() = runBlocking {
coEvery {
@@ -50,8 +58,14 @@ class HomeViewModelIntegrationTest {
} returns Response.success(
fakeSuccessWeatherResponse
)
+ coEvery { mockWeatherDao.getWeather(any(), any()) } returns fakePopulatedResponse
- val weatherRepository = createWeatherRepository()
+ val weatherRepository =
+ DefaultWeatherRepository(
+ openWeatherService = mockOpenWeatherService,
+ weatherDao = mockWeatherDao,
+ logger = mockLogger
+ )
val viewModel = createViewModel(weatherRepository = weatherRepository)
@@ -65,7 +79,7 @@ class HomeViewModelIntegrationTest {
),
locationName = "-",
language = "English",
- weather = fakeSuccessMappedWeatherResponse,
+ weather = fakeSuccessResponse,
isLoading = false,
errorMessageId = null
)
@@ -94,7 +108,13 @@ class HomeViewModelIntegrationTest {
"{}".toResponseBody()
)
- val weatherRepository = createWeatherRepository()
+ val weatherRepository =
+ DefaultWeatherRepository(
+ openWeatherService = mockOpenWeatherService,
+ weatherDao = mockWeatherDao,
+ logger = mockLogger
+
+ )
val viewModel = createViewModel(weatherRepository = weatherRepository)
@@ -123,7 +143,11 @@ class HomeViewModelIntegrationTest {
@Test
fun `when we init the screen, then update the state`() = runBlocking {
val weatherRepository =
- createWeatherRepository()
+ DefaultWeatherRepository(
+ openWeatherService = mockOpenWeatherService,
+ weatherDao = mockWeatherDao,
+ logger = mockLogger
+ )
val viewModel = createViewModel(weatherRepository = weatherRepository)
@@ -180,8 +204,4 @@ class HomeViewModelIntegrationTest {
weatherRepository = weatherRepository,
settingsRepository = settingsRepository
)
-
-
- private fun createWeatherRepository() =
- DefaultWeatherRepository(openWeatherService = mockOpenWeatherService, logger = mockLogger)
}
diff --git a/app/src/test/java/com/github/odaridavid/weatherapp/WeatherRepositoryUnitTest.kt b/app/src/test/java/com/github/odaridavid/weatherapp/WeatherRepositoryUnitTest.kt
index 2327bd2..a8115b3 100644
--- a/app/src/test/java/com/github/odaridavid/weatherapp/WeatherRepositoryUnitTest.kt
+++ b/app/src/test/java/com/github/odaridavid/weatherapp/WeatherRepositoryUnitTest.kt
@@ -1,5 +1,8 @@
package com.github.odaridavid.weatherapp
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import androidx.work.testing.WorkManagerTestInitHelper
import app.cash.turbine.test
import com.github.odaridavid.weatherapp.core.ErrorType
import com.github.odaridavid.weatherapp.core.api.Logger
@@ -9,24 +12,36 @@ import com.github.odaridavid.weatherapp.core.Result
import com.github.odaridavid.weatherapp.data.weather.DefaultWeatherRepository
import com.github.odaridavid.weatherapp.data.weather.OpenWeatherService
import com.github.odaridavid.weatherapp.data.weather.WeatherResponse
+import com.github.odaridavid.weatherapp.data.weather.local.dao.WeatherDao
import com.google.common.truth.Truth
import io.mockk.coEvery
import io.mockk.impl.annotations.MockK
import io.mockk.mockk
import kotlinx.coroutines.runBlocking
import okhttp3.ResponseBody.Companion.toResponseBody
+import org.junit.Before
import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
import retrofit2.Response
import java.io.IOException
+@RunWith(RobolectricTestRunner::class)
class WeatherRepositoryUnitTest {
@MockK
val mockOpenWeatherService = mockk(relaxed = true)
+ @MockK
+ val mockWeatherDao = mockk(relaxed = true)
@MockK
val mockLogger = mockk(relaxed = true)
+ @Before
+ fun setUp() {
+ val context = ApplicationProvider.getApplicationContext()
+ WorkManagerTestInitHelper.initializeTestWorkManager(context)
+ }
@Test
fun `when we fetch weather data successfully, then a successfully mapped result is emitted`() = runBlocking {
coEvery {
@@ -41,6 +56,7 @@ class WeatherRepositoryUnitTest {
} returns Response.success(
fakeSuccessWeatherResponse
)
+ coEvery { mockWeatherDao.getWeather(any(), any()) } returns fakePopulatedResponse
val weatherRepository = createWeatherRepository()
@@ -262,6 +278,7 @@ class WeatherRepositoryUnitTest {
private fun createWeatherRepository(logger: Logger = mockLogger): WeatherRepository = DefaultWeatherRepository(
openWeatherService = mockOpenWeatherService,
+ weatherDao = mockWeatherDao,
logger = logger
)
}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 3993426..3f1cf17 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -13,6 +13,7 @@ compose-bom="2023.05.01"
compose-navigation = "2.5.3"
datastore = "1.0.0"
lifecycle-viewmodel-compose = "2.6.1"
+androidxWork = "2.8.1"
# Play Services
play-services-location = "21.0.1"
# DI
@@ -30,10 +31,19 @@ turbine = "0.12.1"
mockk = "1.13.3"
truth = "1.1.3"
coroutines-test = "1.6.4"
+core-testing = "2.2.0"
+robolectric = "4.9.2"
+core-ktx = "1.5.0"
# Firebase
com-google-services = "4.3.15"
firebase-bom = "32.1.0"
crashlytics-plugin = "2.9.5"
+# Room
+room = "2.5.1"
+hiltExt = "1.0.0"
+
+# Chucker
+chucker = "3.5.2"
[libraries]
#AndroidX
@@ -44,13 +54,27 @@ compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref =
compose-ui = { group = "androidx.compose.ui", name = "ui" }
compose-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
compose-material = { group = "androidx.compose.material", name = "material" }
+compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
+compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
+compose-viewmodel-lifecycle = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle-viewmodel-compose" }
compose-navigation = { group = "androidx.navigation", name = "navigation-compose", version.ref = "compose-navigation" }
datastore = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" }
+androidx-work-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "androidxWork" }
+androidx-work-testing = { group = "androidx.work", name = "work-testing", version.ref = "androidxWork" }
+room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
+room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
+room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
+
#DI
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" }
-compose-viewmodel-lifecycle = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle-viewmodel-compose" }
+hilt-ext-work = { group = "androidx.hilt", name = "hilt-work", version.ref = "hiltExt" }
+hilt-ext-compiler = { group = "androidx.hilt", name = "hilt-compiler", version.ref = "hiltExt" }
+
+
playservices-location = { group = "com.google.android.gms", name = "play-services-location", version.ref = "play-services-location" }
+
+
# Async
retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
coil = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil-compose" }
@@ -60,6 +84,7 @@ kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-
okhttp-bom = { group = "com.squareup.okhttp3", name = "okhttp-bom", version.ref = "okhttp" }
okhttp = { group = "com.squareup.okhttp3", name = "okhttp" }
okhttp-logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor" }
+
# Testing
junit = { group = "junit", name = "junit", version.ref = "junit" }
turbine = { group = "app.cash.turbine", name = "turbine", version.ref = "turbine" }
@@ -67,8 +92,10 @@ mock-android = { group = "io.mockk", name = "mockk-android", version.ref = "mock
mock-agent = { group = "io.mockk", name = "mockk-agent", version.ref = "mockk" }
truth = { group = "com.google.truth", name = "truth", version.ref = "truth" }
coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "coroutines-test" }
-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
+androidx-arch-core= { group = "androidx.arch.core", name = "core-testing", version.ref = "core-testing" }
+roboelectric= { group = "org.robolectric", name = "robolectric", version.ref = "robolectric" }
+core-ktx-test = { group = "androidx.test", name = "core-ktx", version.ref = "core-ktx" }
+
#Firebase
com-google-services = { group = "com.google.gms", name = "google-services", version.ref = "com-google-services" }
@@ -78,8 +105,9 @@ firebase-analytics = { group = "com.google.firebase", name = "firebase-analytics
firebase-crashlytics = { group = "com.google.firebase", name = "firebase-crashlytics-ktx" }
#chucker
-chucker-debug = "com.github.chuckerteam.chucker:library:3.5.2"
-chucker-release = "com.github.chuckerteam.chucker:library-no-op:3.5.2"
+chucker-debug = { group = "com.github.chuckerteam.chucker", name = "library", version.ref = "chucker" }
+chucker-release = { group = "com.github.chuckerteam.chucker", name = "library-no-op", version.ref = "chucker" }
+
[plugins]
com-android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" }