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