From 7756904864d07ced0d4957bb48a0919d62c2ad59 Mon Sep 17 00:00:00 2001 From: Caleb Langat <95022986+Mzazi25@users.noreply.github.com> Date: Sun, 14 May 2023 13:37:51 +0300 Subject: [PATCH 01/42] Added Dao, Weather Entity and Database --- .../data/weather/local/WeatherDatabase.kt | 16 +++++++++++ .../data/weather/local/dao/WeatherDao.kt | 22 +++++++++++++++ .../weather/local/entity/WeatherEntity.kt | 27 +++++++++++++++++++ 3 files changed, 65 insertions(+) create mode 100644 app/src/main/java/com/github/odaridavid/weatherapp/data/weather/local/WeatherDatabase.kt create mode 100644 app/src/main/java/com/github/odaridavid/weatherapp/data/weather/local/dao/WeatherDao.kt create mode 100644 app/src/main/java/com/github/odaridavid/weatherapp/data/weather/local/entity/WeatherEntity.kt 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..f8081cf --- /dev/null +++ b/app/src/main/java/com/github/odaridavid/weatherapp/data/weather/local/WeatherDatabase.kt @@ -0,0 +1,16 @@ +package com.github.odaridavid.weatherapp.data.weather.local + +import androidx.room.Database +import androidx.room.RoomDatabase +import androidx.room.TypeConverters +import com.github.odaridavid.weatherapp.data.weather.local.dao.WeatherDao +import com.github.odaridavid.weatherapp.data.weather.local.entity.WeatherEntity + +@Database( + entities = [WeatherEntity::class], + version = 1, + 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..e43e2a4 --- /dev/null +++ b/app/src/main/java/com/github/odaridavid/weatherapp/data/weather/local/dao/WeatherDao.kt @@ -0,0 +1,22 @@ +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.Update +import androidx.room.Upsert +import com.github.odaridavid.weatherapp.data.weather.ApiResult +import com.github.odaridavid.weatherapp.data.weather.local.entity.WeatherEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface WeatherDao { + + @Query("SELECT * FROM weather") + suspend fun getWeather(): WeatherEntity? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertCurrentWeather(currentWeather: WeatherEntity) + +} \ 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..f4b2630 --- /dev/null +++ b/app/src/main/java/com/github/odaridavid/weatherapp/data/weather/local/entity/WeatherEntity.kt @@ -0,0 +1,27 @@ +package com.github.odaridavid.weatherapp.data.weather.local.entity + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "weather") +data class WeatherEntity( + val dt: Long, + @PrimaryKey(autoGenerate = true) + val id:Int, + val feels_like: Float, + val temp: Float, + val temp_max: Float, + val temp_min: Float, + val description: String, + val icon: String, + val main: String, + val lastRefreshed: Long = 0, // timestamp of last refresh + val isValid: Boolean = false // whether data is still valid +){ + // Check if the data is still valid + fun isDataValid(): Boolean { + val currentTime = System.currentTimeMillis() + val fifteenMinutesAgo = currentTime - 15 * 60 * 1000 + return lastRefreshed >= fifteenMinutesAgo && isValid + } +} \ No newline at end of file From 6b81d5bfb00d08dba4fc1aeb2487cf72c93a8dfe Mon Sep 17 00:00:00 2001 From: Caleb Langat <95022986+Mzazi25@users.noreply.github.com> Date: Sun, 14 May 2023 13:39:03 +0300 Subject: [PATCH 02/42] updated gradle dependency for room and Workmanager --- app/build.gradle | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/app/build.gradle b/app/build.gradle index d834cb3..02c325c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -78,10 +78,24 @@ dependencies { implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1' implementation("io.coil-kt:coil-compose:2.2.2") + // Room components + implementation "androidx.room:room-runtime:2.5.1" + kapt "androidx.room:room-compiler:2.5.1" + implementation "androidx.room:room-ktx:2.5.1" + + //Work Manager + implementation "androidx.work:work-runtime-ktx:2.8.1" + implementation 'androidx.hilt:hilt-work:1.0.0' + kapt 'androidx.hilt:hilt-compiler:1.0.0' + + // DI implementation "com.google.dagger:hilt-android:2.44" kapt "com.google.dagger:hilt-compiler:2.44" + //Timber + implementation 'com.jakewharton.timber:timber:5.0.1' + // Test testImplementation 'junit:junit:4.13.2' testImplementation 'app.cash.turbine:turbine:0.12.1' From 1039d3230cc92df61f0f26c56fc562bc3a6b73e9 Mon Sep 17 00:00:00 2001 From: Caleb Langat <95022986+Mzazi25@users.noreply.github.com> Date: Sun, 14 May 2023 13:40:14 +0300 Subject: [PATCH 03/42] DI for Local module and updated Manifest file to include notification permission --- app/src/main/AndroidManifest.xml | 7 +++- .../odaridavid/weatherapp/di/LocalModule.kt | 37 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/github/odaridavid/weatherapp/di/LocalModule.kt 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/di/LocalModule.kt b/app/src/main/java/com/github/odaridavid/weatherapp/di/LocalModule.kt new file mode 100644 index 0000000..89c6395 --- /dev/null +++ b/app/src/main/java/com/github/odaridavid/weatherapp/di/LocalModule.kt @@ -0,0 +1,37 @@ +package com.github.odaridavid.weatherapp.di + +import android.content.Context +import androidx.room.Room +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 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) + } + + @Provides + @Singleton + fun providesWeatherDao(db: WeatherDatabase): WeatherDao { + return db.weatherDao() + } +} \ No newline at end of file From 58916aa54ec06121d5c4ea4ef27bcc4330722936 Mon Sep 17 00:00:00 2001 From: Caleb Langat <95022986+Mzazi25@users.noreply.github.com> Date: Sun, 14 May 2023 13:41:16 +0300 Subject: [PATCH 04/42] Added Notification Util to update weather info to the user after 15 Min --- .../weatherapp/util/NotificationUtil.kt | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 app/src/main/java/com/github/odaridavid/weatherapp/util/NotificationUtil.kt 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..0211962 --- /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 = "notification" + val descriptionText = "description" + 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 From 95d5e42226995043cd20ee12c017d508ca9f9899 Mon Sep 17 00:00:00 2001 From: Caleb Langat <95022986+Mzazi25@users.noreply.github.com> Date: Sun, 14 May 2023 13:43:55 +0300 Subject: [PATCH 05/42] Updated mappers to include the new entity created --- .../weatherapp/data/weather/Mappers.kt | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) 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 5789bd7..07b5792 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 @@ -8,6 +8,7 @@ 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.WeatherEntity import java.text.SimpleDateFormat import java.util.Date import kotlin.math.roundToInt @@ -68,3 +69,66 @@ private fun getDate(utcInMillis: Long, formatPattern: String): String { val dateFormat = Date(utcInMillis * 1000) return sdf.format(dateFormat) } + +fun WeatherResponse.asEntity( + currentWeatherResponse: CurrentWeatherResponse, + hourlyWeatherResponse: List, + dailyWeatherResponse: List +) = WeatherEntity( + id = currentWeatherResponse.weather.first().id, + dt = hourlyWeatherResponse.first().forecastedTime, + feels_like = currentWeatherResponse.feelsLike, + temp = currentWeatherResponse.temperature, + temp_max = dailyWeatherResponse.first().temperature.max, + temp_min = dailyWeatherResponse.first().temperature.min, + description = currentWeatherResponse.weather.first().description, + icon = currentWeatherResponse.weather.first().icon, + main = currentWeatherResponse.weather.first().main, +) + +fun WeatherEntity.asExternalModel(unit: String):Weather = + Weather( + current = CurrentWeather( + temperature = formatTemperatureValue(temp, unit), + feelsLike = formatTemperatureValue(feels_like, unit), + weather = listOf( + WeatherInfo( + id = id, + main = main, + description = description, + icon = icon, + ) + ) + ) , + hourly = listOf( + HourlyWeather( + forecastedTime = getDate(dt,"HH:SS"), + temperature = formatTemperatureValue(temp, unit), + weather = listOf( + WeatherInfo( + id = id, + main = main, + description = description, + icon = "${BuildConfig.OPEN_WEATHER_ICONS_URL}$icon@2x.png" + ) + ) + ) + ), + daily = listOf( + DailyWeather( + forecastedTime = getDate(dt,"EEEE dd/M"), + temperature = Temperature( + min = formatTemperatureValue(temp_min, unit), + max = formatTemperatureValue(temp_max, unit), + ), + weather = listOf( + WeatherInfo( + id = id, + main = main, + description = description, + icon = "${BuildConfig.OPEN_WEATHER_ICONS_URL}$icon@2x.png" + ) + ) + ) + ) +) \ No newline at end of file From f113abebd611c12fe745db51be17b719d93d9487 Mon Sep 17 00:00:00 2001 From: Caleb Langat <95022986+Mzazi25@users.noreply.github.com> Date: Sun, 14 May 2023 13:44:57 +0300 Subject: [PATCH 06/42] updated fetchWeather function to read data from cache and update after 15 Min using work manager --- .../data/weather/DefaultWeatherRepository.kt | 51 +++++++++++++++-- .../weatherapp/worker/UpdatedWeatherWorker.kt | 55 +++++++++++++++++++ 2 files changed, 100 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/com/github/odaridavid/weatherapp/worker/UpdatedWeatherWorker.kt 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 886db63..99059ef 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,5 +1,10 @@ package com.github.odaridavid.weatherapp.data.weather +import android.content.Context +import androidx.work.Constraints +import androidx.work.NetworkType +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkManager import com.github.odaridavid.weatherapp.BuildConfig import com.github.odaridavid.weatherapp.R import com.github.odaridavid.weatherapp.core.api.WeatherRepository @@ -7,25 +12,38 @@ 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 com.github.odaridavid.weatherapp.worker.UpdateWeatherWorker +import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.onCompletion +import timber.log.Timber import java.io.IOException import java.net.HttpURLConnection.HTTP_UNAUTHORIZED +import java.util.concurrent.TimeUnit import javax.inject.Inject class DefaultWeatherRepository @Inject constructor( private val openWeatherService: OpenWeatherService, + private val weatherDao: WeatherDao, + @ApplicationContext private val context: Context ) : WeatherRepository { override fun fetchWeatherData( defaultLocation: DefaultLocation, language: String, units: String ): Flow> = flow { - + val cachedWeather = weatherDao.getWeather() + if (cachedWeather != null && cachedWeather.isDataValid()) { + val weatherData = cachedWeather.asExternalModel(unit = units) + emit(ApiResult.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, @@ -33,12 +51,22 @@ 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) + val response = apiResponse.body() + if (apiResponse.isSuccessful && response != null) { + val currentWeatherResponse = response.current + val hourlyWeatherResponse = response.hourly + val dailyWeatherResponse = response.daily + val weatherEntity = response.asEntity( + currentWeatherResponse = currentWeatherResponse, + hourlyWeatherResponse = hourlyWeatherResponse, + dailyWeatherResponse = dailyWeatherResponse + ) + weatherDao.insertCurrentWeather(weatherEntity) + val weatherData = weatherEntity.asExternalModel(units) emit(ApiResult.Success(data = weatherData)) } else { - val errorMessage = mapResponseCodeToErrorMessage(response.code()) + val errorMessage = mapResponseCodeToErrorMessage(apiResponse.code()) + Timber.e("Error Message $errorMessage") emit(ApiResult.Error(errorMessage)) } }.catch { throwable -> @@ -47,6 +75,17 @@ class DefaultWeatherRepository @Inject constructor( else -> R.string.error_generic } emit(ApiResult.Error(errorMessage)) + }.onCompletion { + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + + val refreshWeatherRequest = OneTimeWorkRequestBuilder() + .setConstraints(constraints) + .setInitialDelay(15, TimeUnit.MINUTES) + .build() + + WorkManager.getInstance(context).enqueue(refreshWeatherRequest) } private fun mapResponseCodeToErrorMessage(code: Int): Int = when (code) { 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..1333448 --- /dev/null +++ b/app/src/main/java/com/github/odaridavid/weatherapp/worker/UpdatedWeatherWorker.kt @@ -0,0 +1,55 @@ +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.model.DefaultLocation +import com.github.odaridavid.weatherapp.core.model.SupportedLanguage +import com.github.odaridavid.weatherapp.core.model.Units +import com.github.odaridavid.weatherapp.data.weather.DefaultWeatherRepository +import com.github.odaridavid.weatherapp.util.NotificationUtil +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject + + +@HiltWorker +class UpdateWeatherWorker @AssistedInject constructor( + @Assisted context: Context, + @Assisted params: WorkerParameters, + private val weatherRepository: DefaultWeatherRepository, + private val notificationUtil: NotificationUtil +): CoroutineWorker(context, params){ + + override suspend fun doWork(): Result { + return try { + val defaultLocation = getDefaultLocation() + val language = getDefaultLanguage() + val units = getDefaultUnits() + weatherRepository.fetchWeatherData( + defaultLocation = defaultLocation, + language = language, + units = units + ) + notificationUtil.makeNotification("Weather Updated") + Result.success() + } catch(e: Error) { + Result.retry() + } + } + + private fun getDefaultLocation(): DefaultLocation { + return DefaultLocation( + latitude = 12.11, + longitude = 98.55, + ) + } + + private fun getDefaultLanguage(): String { + return SupportedLanguage.values().toString() + } + + private fun getDefaultUnits(): String { + return Units.METRIC.value + } +} \ No newline at end of file From 6f50a418a2ba050faafaae94e773a70ff0d5061d Mon Sep 17 00:00:00 2001 From: Caleb Langat <95022986+Mzazi25@users.noreply.github.com> Date: Sun, 14 May 2023 13:45:46 +0300 Subject: [PATCH 07/42] updated WeatherApp to getWorkManagerConfigs and included Timber for Debugging --- .../github/odaridavid/weatherapp/WeatherApp.kt | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) 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..cc10020 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,22 @@ package com.github.odaridavid.weatherapp import android.app.Application +import androidx.hilt.work.HiltWorkerFactory +import androidx.work.Configuration import dagger.hilt.android.HiltAndroidApp +import timber.log.Timber +import javax.inject.Inject @HiltAndroidApp -class WeatherApp : Application() +class WeatherApp : Application(), Configuration.Provider{ + override fun onCreate() { + super.onCreate() + if (BuildConfig.DEBUG) { + Timber.plant(Timber.DebugTree()) + } + } + @Inject + lateinit var workerFactory: HiltWorkerFactory + override fun getWorkManagerConfiguration(): Configuration = + Configuration.Builder().setWorkerFactory(workerFactory).build() +} From 3d761b5f933582cb28bda61d8861fdba5ad89854 Mon Sep 17 00:00:00 2001 From: Caleb Langat <95022986+Mzazi25@users.noreply.github.com> Date: Tue, 16 May 2023 15:45:48 +0300 Subject: [PATCH 08/42] Added gson converter gradle dependency and created Converter class to transform hourly and daily weather list --- app/build.gradle | 1 + .../weather/local/converters/Converters.kt | 58 +++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 app/src/main/java/com/github/odaridavid/weatherapp/data/weather/local/converters/Converters.kt diff --git a/app/build.gradle b/app/build.gradle index 02c325c..e40b9a4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -77,6 +77,7 @@ dependencies { implementation "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0" implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1' implementation("io.coil-kt:coil-compose:2.2.2") + implementation 'com.squareup.retrofit2:converter-gson:2.9.0' // Room components implementation "androidx.room:room-runtime:2.5.1" diff --git a/app/src/main/java/com/github/odaridavid/weatherapp/data/weather/local/converters/Converters.kt b/app/src/main/java/com/github/odaridavid/weatherapp/data/weather/local/converters/Converters.kt new file mode 100644 index 0000000..8c36970 --- /dev/null +++ b/app/src/main/java/com/github/odaridavid/weatherapp/data/weather/local/converters/Converters.kt @@ -0,0 +1,58 @@ +package com.github.odaridavid.weatherapp.data.weather.local.converters + +import androidx.room.ProvidedTypeConverter +import androidx.room.TypeConverter +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.TemperatureEntity +import com.github.odaridavid.weatherapp.data.weather.local.entity.WeatherInfoResponseEntity +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken + +@ProvidedTypeConverter +class Converters { + private val gson = Gson() + + @TypeConverter + fun fromHourlyWeatherEntityList(hourlyList: List): String { + return gson.toJson(hourlyList) + } + + @TypeConverter + fun toHourlyWeatherEntityList(json: String): List { + val type = object : TypeToken>() {}.type + return gson.fromJson(json, type) + } + + @TypeConverter + fun fromDailyWeatherEntityList(dailyList: List): String { + return gson.toJson(dailyList) + } + + @TypeConverter + fun toDailyWeatherEntityList(json: String): List { + val type = object : TypeToken>() {}.type + return gson.fromJson(json, type) + } + + @TypeConverter + fun fromWeatherInfoResponseEntityList(weatherList: List): String { + return gson.toJson(weatherList) + } + + @TypeConverter + fun toWeatherInfoResponseEntityList(json: String): List { + val type = object : TypeToken>() {}.type + return gson.fromJson(json, type) + } + + @TypeConverter + fun fromTemperatureEntity(temperature: TemperatureEntity): String { + return gson.toJson(temperature) + } + + @TypeConverter + fun toTemperatureEntity(json: String): TemperatureEntity { + return gson.fromJson(json, TemperatureEntity::class.java) + } +} From 248820eb0f4fbf918da42ee98f98de8961efd426 Mon Sep 17 00:00:00 2001 From: Caleb Langat <95022986+Mzazi25@users.noreply.github.com> Date: Tue, 16 May 2023 15:48:44 +0300 Subject: [PATCH 09/42] Added typeconverters to DI, Database and updated WeatherEntity Class --- .../data/weather/local/WeatherDatabase.kt | 4 +- .../weather/local/entity/WeatherEntity.kt | 41 +++++++++++++++++-- .../odaridavid/weatherapp/di/LocalModule.kt | 2 + 3 files changed, 42 insertions(+), 5 deletions(-) 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 index f8081cf..cc58d5b 100644 --- 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 @@ -3,14 +3,16 @@ package com.github.odaridavid.weatherapp.data.weather.local import androidx.room.Database import androidx.room.RoomDatabase import androidx.room.TypeConverters +import com.github.odaridavid.weatherapp.data.weather.local.converters.Converters import com.github.odaridavid.weatherapp.data.weather.local.dao.WeatherDao import com.github.odaridavid.weatherapp.data.weather.local.entity.WeatherEntity @Database( entities = [WeatherEntity::class], - version = 1, + version = 3, exportSchema = false ) +@TypeConverters(Converters::class) 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/entity/WeatherEntity.kt b/app/src/main/java/com/github/odaridavid/weatherapp/data/weather/local/entity/WeatherEntity.kt index f4b2630..cd960a5 100644 --- 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 @@ -1,10 +1,21 @@ package com.github.odaridavid.weatherapp.data.weather.local.entity +import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey +import androidx.room.TypeConverters +import com.github.odaridavid.weatherapp.data.weather.local.converters.Converters @Entity(tableName = "weather") data class WeatherEntity( + @ColumnInfo(name = "hourly") + @TypeConverters(Converters::class) + val hourly: List = emptyList(), + + @ColumnInfo(name = "daily") + @TypeConverters(Converters::class) + val daily: List = emptyList(), + val dt: Long, @PrimaryKey(autoGenerate = true) val id:Int, @@ -15,13 +26,35 @@ data class WeatherEntity( val description: String, val icon: String, val main: String, - val lastRefreshed: Long = 0, // timestamp of last refresh - val isValid: Boolean = false // whether data is still valid + val lastRefreshed: Long = 0, + val isValid: Boolean = false ){ - // Check if the data is still valid fun isDataValid(): Boolean { val currentTime = System.currentTimeMillis() val fifteenMinutesAgo = currentTime - 15 * 60 * 1000 return lastRefreshed >= fifteenMinutesAgo && isValid } -} \ No newline at end of file +} + +data class HourlyWeatherEntity( + val dt: Long, + val temperature: Float, + val weather: List +) + +data class DailyWeatherEntity( + val dt: Long, + val temperature: TemperatureEntity, + val weather: List +) + +data class TemperatureEntity( + val min: Float, + val max: Float +) +data class WeatherInfoResponseEntity( + val id: Int, + val main: String, + val description: String, + val icon: String +) \ No newline at end of file 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 index 89c6395..dcabbd1 100644 --- a/app/src/main/java/com/github/odaridavid/weatherapp/di/LocalModule.kt +++ b/app/src/main/java/com/github/odaridavid/weatherapp/di/LocalModule.kt @@ -3,6 +3,7 @@ package com.github.odaridavid.weatherapp.di import android.content.Context import androidx.room.Room import com.github.odaridavid.weatherapp.data.weather.local.WeatherDatabase +import com.github.odaridavid.weatherapp.data.weather.local.converters.Converters import com.github.odaridavid.weatherapp.data.weather.local.dao.WeatherDao import com.github.odaridavid.weatherapp.util.NotificationUtil import dagger.Module @@ -20,6 +21,7 @@ object LocalModule { fun provideWeatherDatabase(@ApplicationContext app: Context): WeatherDatabase { return Room .databaseBuilder(app, WeatherDatabase::class.java, "weather_database") + .addTypeConverter(Converters()) .build() } From b47104faae3cbaeb4eb6f0d1ef1c1d299f8f0b6a Mon Sep 17 00:00:00 2001 From: Caleb Langat <95022986+Mzazi25@users.noreply.github.com> Date: Tue, 16 May 2023 15:50:21 +0300 Subject: [PATCH 10/42] Updated mappers and DefaultWeatherRepository with fetching the correct weatherinfo from the cached data --- .../data/weather/DefaultWeatherRepository.kt | 18 +- .../weatherapp/data/weather/Mappers.kt | 156 +++++++++++------- 2 files changed, 104 insertions(+), 70 deletions(-) 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 99059ef..2819c7d 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 @@ -37,7 +37,7 @@ class DefaultWeatherRepository @Inject constructor( ): Flow> = flow { val cachedWeather = weatherDao.getWeather() if (cachedWeather != null && cachedWeather.isDataValid()) { - val weatherData = cachedWeather.asExternalModel(unit = units) + val weatherData = cachedWeather.toCoreEntity(unit = units) emit(ApiResult.Success(data = weatherData)) return@flow } @@ -53,17 +53,11 @@ class DefaultWeatherRepository @Inject constructor( ) val response = apiResponse.body() if (apiResponse.isSuccessful && response != null) { - val currentWeatherResponse = response.current - val hourlyWeatherResponse = response.hourly - val dailyWeatherResponse = response.daily - val weatherEntity = response.asEntity( - currentWeatherResponse = currentWeatherResponse, - hourlyWeatherResponse = hourlyWeatherResponse, - dailyWeatherResponse = dailyWeatherResponse - ) - weatherDao.insertCurrentWeather(weatherEntity) - val weatherData = weatherEntity.asExternalModel(units) - emit(ApiResult.Success(data = weatherData)) + val entity = apiResponse.body()!!.toWeatherEntity() + + weatherDao.insertCurrentWeather(entity) + val weatherData = entity.toCoreEntity(unit= units) + emit(ApiResult.Success(data =weatherData)) } else { val errorMessage = mapResponseCodeToErrorMessage(apiResponse.code()) Timber.e("Error Message $errorMessage") 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 07b5792..6c92343 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 @@ -8,7 +8,11 @@ import com.github.odaridavid.weatherapp.core.model.Temperature import com.github.odaridavid.weatherapp.core.model.Units import com.github.odaridavid.weatherapp.core.model.Weather import com.github.odaridavid.weatherapp.core.model.WeatherInfo +import com.github.odaridavid.weatherapp.data.weather.local.entity.DailyWeatherEntity +import com.github.odaridavid.weatherapp.data.weather.local.entity.HourlyWeatherEntity +import com.github.odaridavid.weatherapp.data.weather.local.entity.TemperatureEntity import com.github.odaridavid.weatherapp.data.weather.local.entity.WeatherEntity +import com.github.odaridavid.weatherapp.data.weather.local.entity.WeatherInfoResponseEntity import java.text.SimpleDateFormat import java.util.Date import kotlin.math.roundToInt @@ -53,6 +57,52 @@ fun TemperatureResponse.toCoreModel(unit: String): Temperature = min = formatTemperatureValue(min, unit), max = formatTemperatureValue(max, unit) ) +fun WeatherEntity.toCoreEntity(unit: String): Weather = + Weather( + current = toCurrentWeather(unit = unit), + hourly = hourly.map { it.toCoreEntity(unit = unit) }, + daily = daily.map { it.toCoreEntity(unit = unit) } + ) +private fun WeatherEntity.toCurrentWeather(unit: String): CurrentWeather = + CurrentWeather( + temperature = formatTemperatureValue(temp, unit), + feelsLike = formatTemperatureValue(feels_like, unit), + weather = listOf(toWeatherInfo()) + ) +private fun WeatherEntity.toWeatherInfo(): WeatherInfo = + WeatherInfo( + id = id, + main = main, + description = description, + icon = "${BuildConfig.OPEN_WEATHER_ICONS_URL}$icon@2x.png" + ) +fun DailyWeatherEntity.toCoreEntity(unit: String): DailyWeather = + DailyWeather( + forecastedTime = getDate(dt,"EEEE dd/M"), + temperature = temperature.toCoreEntity(unit = unit), + weather = weather.map { it.toCoreEntity() } + ) + +fun HourlyWeatherEntity.toCoreEntity(unit: String): HourlyWeather = + HourlyWeather( + forecastedTime = getDate(dt,"HH:SS"), + temperature = formatTemperatureValue(temperature, unit), + weather = weather.map { it.toCoreEntity() } + ) + +fun WeatherInfoResponseEntity.toCoreEntity(): WeatherInfo = + WeatherInfo( + id = id, + main = main, + description = description, + icon = "${BuildConfig.OPEN_WEATHER_ICONS_URL}$icon@2x.png" + ) + +fun TemperatureEntity.toCoreEntity(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)}" @@ -70,65 +120,55 @@ private fun getDate(utcInMillis: Long, formatPattern: String): String { return sdf.format(dateFormat) } -fun WeatherResponse.asEntity( - currentWeatherResponse: CurrentWeatherResponse, - hourlyWeatherResponse: List, - dailyWeatherResponse: List -) = WeatherEntity( - id = currentWeatherResponse.weather.first().id, - dt = hourlyWeatherResponse.first().forecastedTime, - feels_like = currentWeatherResponse.feelsLike, - temp = currentWeatherResponse.temperature, - temp_max = dailyWeatherResponse.first().temperature.max, - temp_min = dailyWeatherResponse.first().temperature.min, - description = currentWeatherResponse.weather.first().description, - icon = currentWeatherResponse.weather.first().icon, - main = currentWeatherResponse.weather.first().main, -) +fun WeatherResponse.toWeatherEntity(): WeatherEntity { + val currentTime = System.currentTimeMillis() + val currentWeatherInfo = current.weather.first() -fun WeatherEntity.asExternalModel(unit: String):Weather = - Weather( - current = CurrentWeather( - temperature = formatTemperatureValue(temp, unit), - feelsLike = formatTemperatureValue(feels_like, unit), - weather = listOf( - WeatherInfo( - id = id, - main = main, - description = description, - icon = icon, - ) + val hourlyWeatherEntities = hourly.map { hourlyResponse -> + HourlyWeatherEntity( + dt = hourlyResponse.forecastedTime, + temperature = hourlyResponse.temperature, + weather = hourlyResponse.weather.map { it.toWeatherInfoResponse() } ) - ) , - hourly = listOf( - HourlyWeather( - forecastedTime = getDate(dt,"HH:SS"), - temperature = formatTemperatureValue(temp, unit), - weather = listOf( - WeatherInfo( - id = id, - main = main, - description = description, - icon = "${BuildConfig.OPEN_WEATHER_ICONS_URL}$icon@2x.png" - ) - ) - ) - ), - daily = listOf( - DailyWeather( - forecastedTime = getDate(dt,"EEEE dd/M"), - temperature = Temperature( - min = formatTemperatureValue(temp_min, unit), - max = formatTemperatureValue(temp_max, unit), - ), - weather = listOf( - WeatherInfo( - id = id, - main = main, - description = description, - icon = "${BuildConfig.OPEN_WEATHER_ICONS_URL}$icon@2x.png" - ) - ) + } + + val dailyWeatherEntities = daily.map { dailyResponse -> + DailyWeatherEntity( + dt = dailyResponse.forecastedTime, + temperature = dailyResponse.temperature.toTemperatureEntity(), + weather = dailyResponse.weather.map { it.toWeatherInfoResponse() } ) + } + + return WeatherEntity( + dt = currentTime, + id = 0, + feels_like = current.feelsLike, + temp = current.temperature, + temp_max = current.temperature, + temp_min = current.temperature, + description = currentWeatherInfo.description, + icon = currentWeatherInfo.icon, + main = currentWeatherInfo.main, + lastRefreshed = currentTime, + isValid = true, + hourly = hourlyWeatherEntities, + daily = dailyWeatherEntities + ) +} + +private fun WeatherInfoResponse.toWeatherInfoResponse(): WeatherInfoResponseEntity { + return WeatherInfoResponseEntity( + id = id, + main = main, + description = description, + icon = icon + ) +} + +fun TemperatureResponse.toTemperatureEntity(): TemperatureEntity { + return TemperatureEntity( + min = min, + max = max ) -) \ No newline at end of file +} \ No newline at end of file From 84a4bd1c569e64f893f0a93c9618dd0e532700c4 Mon Sep 17 00:00:00 2001 From: Caleb Langat <95022986+Mzazi25@users.noreply.github.com> Date: Wed, 17 May 2023 09:15:52 +0300 Subject: [PATCH 11/42] Added testing dependencies --- app/build.gradle | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/build.gradle b/app/build.gradle index e40b9a4..bbea4bf 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -106,6 +106,11 @@ dependencies { testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4' debugImplementation "androidx.compose.ui:ui-tooling:$compose_ui_version" debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_ui_version" + testImplementation "org.robolectric:robolectric:4.9.2" + testImplementation "androidx.arch.core:core-testing:2.2.0" + implementation 'androidx.work:work-testing:2.8.1' + implementation 'androidx.test:core-ktx:1.5.0' + } kapt { From 545579a05f2ca07f4b5aa0fa9c39538735acf859 Mon Sep 17 00:00:00 2001 From: Caleb Langat <95022986+Mzazi25@users.noreply.github.com> Date: Wed, 17 May 2023 09:16:51 +0300 Subject: [PATCH 12/42] Updated Fakes with the corrected mapped responses --- .../com/github/odaridavid/weatherapp/Fakes.kt | 38 ++++++++++++++++--- 1 file changed, 32 insertions(+), 6 deletions(-) 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..80d675b 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,51 @@ 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.WeatherEntity val fakeSuccessWeatherResponse = WeatherResponse( current = CurrentWeatherResponse( temperature = 3.0f, feelsLike = 2.0f, - weather = listOf() + weather = listOf(WeatherInfoResponse( + id = 1, + main = "fake", + description = "fake", + icon = "fake" + )) ), - hourly = listOf(), - daily = listOf() + hourly = emptyList(), + daily = emptyList() ) val fakeSuccessMappedWeatherResponse = Weather( current = CurrentWeather( temperature = "3°C", feelsLike = "2°C", - weather = listOf() + weather = listOf(WeatherInfo( + id = 0, + main = "fake", + description = "fake", + icon = "http://openweathermap.org/img/wn/fake@2x.png" + )) ), - hourly = listOf(), - daily = listOf() + hourly = emptyList(), + daily = emptyList() +) +val fakeSuccessMappedEntityResponse = WeatherEntity( + hourly = emptyList(), + daily = emptyList(), + dt = 10L, + id = 1, + feels_like = 2.0f, + temp = 3.0f, + temp_max = 0.0f, + temp_min = 0.0f, + description = "fake", + icon = "fake", + main = "fake", ) From 92499fdd69b1ed364a486e0ac393a88395205681 Mon Sep 17 00:00:00 2001 From: Caleb Langat <95022986+Mzazi25@users.noreply.github.com> Date: Wed, 17 May 2023 09:18:54 +0300 Subject: [PATCH 13/42] Updated HomeViewModelIntegrationTest --- .../HomeViewModelIntegrationTest.kt | 30 +++++++++++++++++-- 1 file changed, 27 insertions(+), 3 deletions(-) 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 b99b9ee..619a609 100644 --- a/app/src/test/java/com/github/odaridavid/weatherapp/HomeViewModelIntegrationTest.kt +++ b/app/src/test/java/com/github/odaridavid/weatherapp/HomeViewModelIntegrationTest.kt @@ -1,14 +1,17 @@ package com.github.odaridavid.weatherapp +import android.content.Context import app.cash.turbine.test import com.github.odaridavid.weatherapp.core.api.WeatherRepository 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 +import androidx.arch.core.executor.testing.InstantTaskExecutorRule import com.google.common.truth.Truth import io.mockk.coEvery import io.mockk.impl.annotations.MockK @@ -29,9 +32,18 @@ class HomeViewModelIntegrationTest { @MockK val mockOpenWeatherService = mockk(relaxed = true) + @MockK + val mockWeatherDao = mockk(relaxed = true) + + @MockK + val mockContext = mockk(relaxed = true) + @get:Rule val coroutineRule = MainCoroutineRule() + @get:Rule + val instantExecutorRule = InstantTaskExecutorRule() + @Test fun `when fetching weather data is successful, then display correct data`() = runBlocking { coEvery { @@ -48,7 +60,11 @@ class HomeViewModelIntegrationTest { ) val weatherRepository = - DefaultWeatherRepository(openWeatherService = mockOpenWeatherService) + DefaultWeatherRepository( + openWeatherService = mockOpenWeatherService, + weatherDao = mockWeatherDao, + context = mockContext + ) val viewModel = createViewModel(weatherRepository = weatherRepository) @@ -92,7 +108,11 @@ class HomeViewModelIntegrationTest { ) val weatherRepository = - DefaultWeatherRepository(openWeatherService = mockOpenWeatherService) + DefaultWeatherRepository( + openWeatherService = mockOpenWeatherService, + weatherDao = mockWeatherDao, + context = mockContext + ) val viewModel = createViewModel(weatherRepository = weatherRepository) @@ -121,7 +141,11 @@ class HomeViewModelIntegrationTest { @Test fun `when we init the screen, then update the state`() = runBlocking { val weatherRepository = - DefaultWeatherRepository(openWeatherService = mockOpenWeatherService) + DefaultWeatherRepository( + openWeatherService = mockOpenWeatherService, + weatherDao = mockWeatherDao, + context = mockContext + ) val viewModel = createViewModel(weatherRepository = weatherRepository) From b6d13cef0272cdc3a93a467c12e8511a3c9cc2d2 Mon Sep 17 00:00:00 2001 From: Caleb Langat <95022986+Mzazi25@users.noreply.github.com> Date: Wed, 17 May 2023 09:19:14 +0300 Subject: [PATCH 14/42] Updated WeatherRepositoryUnitTest --- .../weatherapp/WeatherRepositoryUnitTest.kt | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) 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 c9381ca..defa2fe 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.api.WeatherRepository import com.github.odaridavid.weatherapp.core.model.DefaultLocation @@ -7,20 +10,35 @@ import com.github.odaridavid.weatherapp.data.weather.ApiResult 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 mockContext = 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 { @@ -35,6 +53,7 @@ class WeatherRepositoryUnitTest { } returns Response.success( fakeSuccessWeatherResponse ) + coEvery { mockWeatherDao.getWeather() } returns fakeSuccessMappedEntityResponse val weatherRepository = createWeatherRepository() @@ -255,6 +274,8 @@ class WeatherRepositoryUnitTest { } private fun createWeatherRepository(): WeatherRepository = DefaultWeatherRepository( - openWeatherService = mockOpenWeatherService + openWeatherService = mockOpenWeatherService, + weatherDao = mockWeatherDao, + context = mockContext ) } From 50f9bb54bd023ca985bb8bbf1f593a5ffd76a4d9 Mon Sep 17 00:00:00 2001 From: Caleb Langat <95022986+Mzazi25@users.noreply.github.com> Date: Fri, 26 May 2023 08:20:15 +0300 Subject: [PATCH 15/42] Resolved Conflicts with develop branch --- .../com/github/odaridavid/weatherapp/Fakes.kt | 63 ++++++++++--------- 1 file changed, 35 insertions(+), 28 deletions(-) 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 80d675b..42dce68 100644 --- a/app/src/test/java/com/github/odaridavid/weatherapp/Fakes.kt +++ b/app/src/test/java/com/github/odaridavid/weatherapp/Fakes.kt @@ -1,52 +1,59 @@ package com.github.odaridavid.weatherapp import com.github.odaridavid.weatherapp.core.model.CurrentWeather +import com.github.odaridavid.weatherapp.core.model.DailyWeather +import com.github.odaridavid.weatherapp.core.model.HourlyWeather +import com.github.odaridavid.weatherapp.core.model.Temperature 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.DailyWeatherResponse +import com.github.odaridavid.weatherapp.data.weather.HourlyWeatherResponse +import com.github.odaridavid.weatherapp.data.weather.TemperatureResponse 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.DailyWeatherEntity +import com.github.odaridavid.weatherapp.data.weather.local.entity.HourlyWeatherEntity +import com.github.odaridavid.weatherapp.data.weather.local.entity.PopulatedWeather +import com.github.odaridavid.weatherapp.data.weather.local.entity.TemperatureEntity import com.github.odaridavid.weatherapp.data.weather.local.entity.WeatherEntity +import com.github.odaridavid.weatherapp.data.weather.local.entity.WeatherInfoResponseEntity val fakeSuccessWeatherResponse = WeatherResponse( current = CurrentWeatherResponse( temperature = 3.0f, feelsLike = 2.0f, - weather = listOf(WeatherInfoResponse( - id = 1, - main = "fake", - description = "fake", - icon = "fake" - )) + weather = listOf() ), - hourly = emptyList(), - daily = emptyList() + hourly = listOf(), + daily = listOf() ) val fakeSuccessMappedWeatherResponse = Weather( current = CurrentWeather( temperature = "3°C", feelsLike = "2°C", - weather = listOf(WeatherInfo( - id = 0, - main = "fake", - description = "fake", - icon = "http://openweathermap.org/img/wn/fake@2x.png" - )) + weather = listOf() ), - hourly = emptyList(), - daily = emptyList() + hourly = listOf(), + daily = listOf() ) -val fakeSuccessMappedEntityResponse = WeatherEntity( - hourly = emptyList(), - daily = emptyList(), - dt = 10L, - id = 1, - feels_like = 2.0f, - temp = 3.0f, - temp_max = 0.0f, - temp_min = 0.0f, - description = "fake", - icon = "fake", - main = "fake", +val fakeSuccessResponse = Weather( + current = CurrentWeather( + temperature = "0°C", + feelsLike = "0°C", + weather = listOf() + ), + hourly = listOf(), + daily = listOf() ) + +val fakePopulatedResponse = PopulatedWeather( + current = WeatherEntity( + feels_like = 2.0f, + temp = 3.0f, + weather = listOf() + ), + hourly = listOf(), + daily = listOf() +) \ No newline at end of file From 6e7b82db214008f6c45bc9b2e8dc7c37c12e62e7 Mon Sep 17 00:00:00 2001 From: Caleb Langat <95022986+Mzazi25@users.noreply.github.com> Date: Fri, 26 May 2023 10:25:12 +0300 Subject: [PATCH 16/42] Removed Gson and updated type converters --- app/build.gradle.kts | 4 -- .../weather/local/converters/Converters.kt | 49 ++----------------- gradle/libs.versions.toml | 11 +---- 3 files changed, 6 insertions(+), 58 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7d50303..48f60a1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -75,7 +75,6 @@ dependencies { implementation(libs.kotlinx.serialization.converter) implementation(libs.kotlinx.serialization) implementation(libs.coil) - implementation(libs.retrofit.converter.gson) // DI implementation(libs.hilt.android) kapt(libs.hilt.compiler) @@ -108,9 +107,6 @@ dependencies { testImplementation(libs.androidx.arch.core) implementation(libs.core.ktx.test) - //Timber - implementation(libs.timber) - } kapt { diff --git a/app/src/main/java/com/github/odaridavid/weatherapp/data/weather/local/converters/Converters.kt b/app/src/main/java/com/github/odaridavid/weatherapp/data/weather/local/converters/Converters.kt index 8c36970..f0ac9fb 100644 --- a/app/src/main/java/com/github/odaridavid/weatherapp/data/weather/local/converters/Converters.kt +++ b/app/src/main/java/com/github/odaridavid/weatherapp/data/weather/local/converters/Converters.kt @@ -2,57 +2,18 @@ package com.github.odaridavid.weatherapp.data.weather.local.converters import androidx.room.ProvidedTypeConverter import androidx.room.TypeConverter -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.TemperatureEntity import com.github.odaridavid.weatherapp.data.weather.local.entity.WeatherInfoResponseEntity -import com.google.gson.Gson -import com.google.gson.reflect.TypeToken +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json @ProvidedTypeConverter class Converters { - private val gson = Gson() @TypeConverter - fun fromHourlyWeatherEntityList(hourlyList: List): String { - return gson.toJson(hourlyList) - } + fun fromWeatherInfo(value: List?): String = Json.encodeToString(value) @TypeConverter - fun toHourlyWeatherEntityList(json: String): List { - val type = object : TypeToken>() {}.type - return gson.fromJson(json, type) - } + fun toWeatherInfo(value: String?) = value?.let { Json.decodeFromString>(it) } - @TypeConverter - fun fromDailyWeatherEntityList(dailyList: List): String { - return gson.toJson(dailyList) - } - - @TypeConverter - fun toDailyWeatherEntityList(json: String): List { - val type = object : TypeToken>() {}.type - return gson.fromJson(json, type) - } - - @TypeConverter - fun fromWeatherInfoResponseEntityList(weatherList: List): String { - return gson.toJson(weatherList) - } - - @TypeConverter - fun toWeatherInfoResponseEntityList(json: String): List { - val type = object : TypeToken>() {}.type - return gson.fromJson(json, type) - } - - @TypeConverter - fun fromTemperatureEntity(temperature: TemperatureEntity): String { - return gson.toJson(temperature) - } - - @TypeConverter - fun toTemperatureEntity(json: String): TemperatureEntity { - return gson.fromJson(json, TemperatureEntity::class.java) - } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e7a4767..0d6ba3a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -42,11 +42,6 @@ crashlytics-plugin = "2.9.5" room = "2.5.1" hiltExt = "1.0.0" -#Gson -gson = "2.9.0" - -#Timber -timber = "5.0.1" [libraries] #AndroidX @@ -72,8 +67,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" } -#Timber -timber = { group = "com.jakewharton.timber", name = "timber", version.ref = "timber" } + # Testing junit = { group = "junit", name = "junit", version.ref = "junit" } turbine = { group = "app.cash.turbine", name = "turbine", version.ref = "turbine" } @@ -103,9 +97,6 @@ firebase-crashlytics = { group = "com.google.firebase", name = "firebase-crashly 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" } -#Gson -retrofit-converter-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "gson" } - [plugins] com-android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" } From 67110cce9d34994a13aed693ca8efc1710173226 Mon Sep 17 00:00:00 2001 From: Caleb Langat <95022986+Mzazi25@users.noreply.github.com> Date: Fri, 26 May 2023 10:29:25 +0300 Subject: [PATCH 17/42] Refactored DAO to use relationships and updated entity and db --- .../data/weather/local/WeatherDatabase.kt | 9 +- .../data/weather/local/dao/WeatherDao.kt | 19 +++-- .../weather/local/entity/WeatherEntity.kt | 83 ++++++++++++++----- 3 files changed, 84 insertions(+), 27 deletions(-) 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 index cc58d5b..1360d28 100644 --- 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 @@ -5,11 +5,16 @@ import androidx.room.RoomDatabase import androidx.room.TypeConverters import com.github.odaridavid.weatherapp.data.weather.local.converters.Converters import com.github.odaridavid.weatherapp.data.weather.local.dao.WeatherDao +import com.github.odaridavid.weatherapp.data.weather.local.entity.DailyWeatherEntity +import com.github.odaridavid.weatherapp.data.weather.local.entity.HourlyWeatherEntity import com.github.odaridavid.weatherapp.data.weather.local.entity.WeatherEntity @Database( - entities = [WeatherEntity::class], - version = 3, + entities = [ + WeatherEntity::class, + HourlyWeatherEntity::class, + DailyWeatherEntity::class], + version = 5, exportSchema = false ) @TypeConverters(Converters::class) 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 index e43e2a4..2e8fbc2 100644 --- 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 @@ -4,19 +4,26 @@ import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query -import androidx.room.Update -import androidx.room.Upsert -import com.github.odaridavid.weatherapp.data.weather.ApiResult +import androidx.room.Transaction +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 kotlinx.coroutines.flow.Flow @Dao interface WeatherDao { - @Query("SELECT * FROM weather") - suspend fun getWeather(): WeatherEntity? + @Transaction + @Query("SELECT * FROM weather_entity") + suspend fun getWeather(): PopulatedWeather? @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertCurrentWeather(currentWeather: WeatherEntity) + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertHourlyWeather(hourly: List) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertDailyWeather(daily: 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 index cd960a5..be22685 100644 --- 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 @@ -1,24 +1,42 @@ package com.github.odaridavid.weatherapp.data.weather.local.entity import androidx.room.ColumnInfo +import androidx.room.Embedded import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index import androidx.room.PrimaryKey +import androidx.room.Relation import androidx.room.TypeConverters import com.github.odaridavid.weatherapp.data.weather.local.converters.Converters -@Entity(tableName = "weather") -data class WeatherEntity( - @ColumnInfo(name = "hourly") - @TypeConverters(Converters::class) - val hourly: List = emptyList(), +data class PopulatedWeather( + @Embedded + val current: WeatherEntity, - @ColumnInfo(name = "daily") - @TypeConverters(Converters::class) - val daily: List = emptyList(), + @Relation(parentColumn = "identity", entityColumn ="dt_column", entity = HourlyWeatherEntity::class) + val hourly: List, + + @Relation(parentColumn = "identity", entityColumn ="dt", entity = DailyWeatherEntity::class) + val daily: List +){ + fun isDataValid(): Boolean { + val currentTime = current.dt + val fifteenMinutesAgo = currentTime - 15 * 60 * 1000 + val isCurrentValid = current.lastRefreshed >= fifteenMinutesAgo && current.isValid + val areHourlyValid = hourly.all { it.lastRefreshed >= fifteenMinutesAgo && it.isValid } + val areDailyValid = daily.all { it.lastRefreshed >= fifteenMinutesAgo && it.isValid } + + return isCurrentValid && areHourlyValid && areDailyValid + } +} + +@Entity(tableName = "weather_entity") +data class WeatherEntity( + @PrimaryKey @ColumnInfo(name = "identity") + val id: Int, val dt: Long, - @PrimaryKey(autoGenerate = true) - val id:Int, val feels_like: Float, val temp: Float, val temp_max: Float, @@ -28,24 +46,51 @@ data class WeatherEntity( val main: String, val lastRefreshed: Long = 0, val isValid: Boolean = false -){ - fun isDataValid(): Boolean { - val currentTime = System.currentTimeMillis() - val fifteenMinutesAgo = currentTime - 15 * 60 * 1000 - return lastRefreshed >= fifteenMinutesAgo && isValid - } -} +) +@Entity( + tableName = "hourly_weather", + foreignKeys = [ + ForeignKey(entity = WeatherEntity::class, + parentColumns = ["identity"], + childColumns = ["dt_column"], + onDelete = ForeignKey.CASCADE + )], + indices = [Index("dt_column")] +) data class HourlyWeatherEntity( + @PrimaryKey + val id: Int= 0, + @ColumnInfo(name = "dt_column") val dt: Long, val temperature: Float, - val weather: List + @TypeConverters(Converters::class) + val weather: List, + val lastRefreshed: Long = 0, + val isValid: Boolean = false ) +@Entity( + tableName = "daily_weather", + foreignKeys = [ + ForeignKey(entity = WeatherEntity::class, + parentColumns = ["identity"], + childColumns = ["dt"], + onDelete = ForeignKey.CASCADE + )], + indices = [Index("dt")] +) data class DailyWeatherEntity( + @PrimaryKey + val id: Int= 0, + @ColumnInfo(name = "dt") val dt: Long, + @Embedded val temperature: TemperatureEntity, - val weather: List + @TypeConverters(Converters::class) + val weather: List, + val lastRefreshed: Long = 0, + val isValid: Boolean = false ) data class TemperatureEntity( From 7f65822e591571607f40a886489959f089bc2d0d Mon Sep 17 00:00:00 2001 From: Caleb Langat <95022986+Mzazi25@users.noreply.github.com> Date: Fri, 26 May 2023 10:30:25 +0300 Subject: [PATCH 18/42] updated mappers with the populated weather --- .../weatherapp/data/weather/Mappers.kt | 68 +++++++++++-------- 1 file changed, 39 insertions(+), 29 deletions(-) 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 6c92343..9b8515c 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 @@ -10,6 +10,7 @@ import com.github.odaridavid.weatherapp.core.model.Weather import com.github.odaridavid.weatherapp.core.model.WeatherInfo import com.github.odaridavid.weatherapp.data.weather.local.entity.DailyWeatherEntity import com.github.odaridavid.weatherapp.data.weather.local.entity.HourlyWeatherEntity +import com.github.odaridavid.weatherapp.data.weather.local.entity.PopulatedWeather import com.github.odaridavid.weatherapp.data.weather.local.entity.TemperatureEntity import com.github.odaridavid.weatherapp.data.weather.local.entity.WeatherEntity import com.github.odaridavid.weatherapp.data.weather.local.entity.WeatherInfoResponseEntity @@ -17,6 +18,7 @@ import java.text.SimpleDateFormat import java.util.Date import kotlin.math.roundToInt + fun WeatherResponse.toCoreModel(unit: String): Weather = Weather( current = current.toCoreModel(unit = unit), daily = daily.map { it.toCoreModel(unit = unit) }, @@ -57,53 +59,58 @@ fun TemperatureResponse.toCoreModel(unit: String): Temperature = min = formatTemperatureValue(min, unit), max = formatTemperatureValue(max, unit) ) -fun WeatherEntity.toCoreEntity(unit: String): Weather = +fun PopulatedWeather.toCoreEntity(unit: String): Weather = Weather( current = toCurrentWeather(unit = unit), - hourly = hourly.map { it.toCoreEntity(unit = unit) }, - daily = daily.map { it.toCoreEntity(unit = unit) } + hourly = hourly.map{it.asCoreModel(unit = unit)}, + daily = daily.map { + it.asCoreModel(unit = unit) } ) -private fun WeatherEntity.toCurrentWeather(unit: String): CurrentWeather = + +private fun PopulatedWeather.toCurrentWeather(unit: String): CurrentWeather = CurrentWeather( - temperature = formatTemperatureValue(temp, unit), - feelsLike = formatTemperatureValue(feels_like, unit), - weather = listOf(toWeatherInfo()) - ) -private fun WeatherEntity.toWeatherInfo(): WeatherInfo = - WeatherInfo( - id = id, - main = main, - description = description, - icon = "${BuildConfig.OPEN_WEATHER_ICONS_URL}$icon@2x.png" + temperature = formatTemperatureValue(current.temp, unit), + feelsLike = formatTemperatureValue(current.feels_like, unit), + weather = listOf( + WeatherInfo( + id = current.id, + main = current.main, + description = current.description, + icon = current.icon + ) + ) ) -fun DailyWeatherEntity.toCoreEntity(unit: String): DailyWeather = +fun DailyWeatherEntity.asCoreModel(unit: String): DailyWeather = DailyWeather( forecastedTime = getDate(dt,"EEEE dd/M"), - temperature = temperature.toCoreEntity(unit = unit), - weather = weather.map { it.toCoreEntity() } + temperature = temperature.asCoreModel(unit = unit), + weather = weather.map { it.asCoreModel() } ) -fun HourlyWeatherEntity.toCoreEntity(unit: String): HourlyWeather = +fun HourlyWeatherEntity.asCoreModel(unit: String): HourlyWeather = HourlyWeather( forecastedTime = getDate(dt,"HH:SS"), temperature = formatTemperatureValue(temperature, unit), - weather = weather.map { it.toCoreEntity() } + weather = weather.map { it.asCoreModel() } ) -fun WeatherInfoResponseEntity.toCoreEntity(): WeatherInfo = +fun WeatherInfoResponseEntity.asCoreModel(): WeatherInfo = WeatherInfo( id = id, main = main, description = description, icon = "${BuildConfig.OPEN_WEATHER_ICONS_URL}$icon@2x.png" ) - -fun TemperatureEntity.toCoreEntity(unit: String): Temperature = +fun TemperatureEntity.asCoreModel(unit: String): Temperature = Temperature( min = formatTemperatureValue(min, unit), max = formatTemperatureValue(max, unit) ) +fun WeatherInfoResponseEntity.toWeatherInfo(): WeatherInfo { + return WeatherInfo(id, main, description, icon) +} + private fun formatTemperatureValue(temperature: Float, unit: String): String = "${temperature.roundToInt()}${getUnitSymbols(unit = unit)}" @@ -120,10 +127,7 @@ private fun getDate(utcInMillis: Long, formatPattern: String): String { return sdf.format(dateFormat) } -fun WeatherResponse.toWeatherEntity(): WeatherEntity { - val currentTime = System.currentTimeMillis() - val currentWeatherInfo = current.weather.first() - +fun WeatherResponse.toHourlyEntity():List { val hourlyWeatherEntities = hourly.map { hourlyResponse -> HourlyWeatherEntity( dt = hourlyResponse.forecastedTime, @@ -131,7 +135,9 @@ fun WeatherResponse.toWeatherEntity(): WeatherEntity { weather = hourlyResponse.weather.map { it.toWeatherInfoResponse() } ) } - + return hourlyWeatherEntities +} +fun WeatherResponse.toDailyEntity():List { val dailyWeatherEntities = daily.map { dailyResponse -> DailyWeatherEntity( dt = dailyResponse.forecastedTime, @@ -139,6 +145,12 @@ fun WeatherResponse.toWeatherEntity(): WeatherEntity { weather = dailyResponse.weather.map { it.toWeatherInfoResponse() } ) } + return dailyWeatherEntities +} + +fun WeatherResponse.toCurrentWeatherEntity(): WeatherEntity { + val currentTime = System.currentTimeMillis() + val currentWeatherInfo = current.weather.first() return WeatherEntity( dt = currentTime, @@ -152,8 +164,6 @@ fun WeatherResponse.toWeatherEntity(): WeatherEntity { main = currentWeatherInfo.main, lastRefreshed = currentTime, isValid = true, - hourly = hourlyWeatherEntities, - daily = dailyWeatherEntities ) } From 7c5bd6d369d618d856df0f3fa093c3a54af8ca68 Mon Sep 17 00:00:00 2001 From: Caleb Langat <95022986+Mzazi25@users.noreply.github.com> Date: Fri, 26 May 2023 10:31:52 +0300 Subject: [PATCH 19/42] Created usecase to sync updated weather and modified default weather repo --- .../data/weather/DefaultWeatherRepository.kt | 38 ++++------------ .../data/weather/RefreshWeatherUseCase.kt | 45 +++++++++++++++++++ .../odaridavid/weatherapp/di/UseCaseModule.kt | 29 ++++++++++++ .../weatherapp/worker/UpdatedWeatherWorker.kt | 11 +++-- .../worker/WeatherUpdateScheduler.kt | 27 +++++++++++ 5 files changed, 115 insertions(+), 35 deletions(-) create mode 100644 app/src/main/java/com/github/odaridavid/weatherapp/data/weather/RefreshWeatherUseCase.kt create mode 100644 app/src/main/java/com/github/odaridavid/weatherapp/di/UseCaseModule.kt create mode 100644 app/src/main/java/com/github/odaridavid/weatherapp/worker/WeatherUpdateScheduler.kt 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 2819c7d..6307976 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,10 +1,5 @@ package com.github.odaridavid.weatherapp.data.weather -import android.content.Context -import androidx.work.Constraints -import androidx.work.NetworkType -import androidx.work.OneTimeWorkRequestBuilder -import androidx.work.WorkManager import com.github.odaridavid.weatherapp.BuildConfig import com.github.odaridavid.weatherapp.R import com.github.odaridavid.weatherapp.core.api.WeatherRepository @@ -13,22 +8,16 @@ 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 com.github.odaridavid.weatherapp.worker.UpdateWeatherWorker -import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.onCompletion -import timber.log.Timber import java.io.IOException import java.net.HttpURLConnection.HTTP_UNAUTHORIZED -import java.util.concurrent.TimeUnit import javax.inject.Inject class DefaultWeatherRepository @Inject constructor( private val openWeatherService: OpenWeatherService, private val weatherDao: WeatherDao, - @ApplicationContext private val context: Context ) : WeatherRepository { override fun fetchWeatherData( defaultLocation: DefaultLocation, @@ -53,14 +42,17 @@ class DefaultWeatherRepository @Inject constructor( ) val response = apiResponse.body() if (apiResponse.isSuccessful && response != null) { - val entity = apiResponse.body()!!.toWeatherEntity() - - weatherDao.insertCurrentWeather(entity) - val weatherData = entity.toCoreEntity(unit= units) - emit(ApiResult.Success(data =weatherData)) + val currentWeather = apiResponse.body()!!.toCurrentWeatherEntity() + val hourlyEntity = apiResponse.body()!!.toHourlyEntity() + val dailyEntity = apiResponse.body()!!.toDailyEntity() + weatherDao.insertCurrentWeather(currentWeather) + weatherDao.insertHourlyWeather(hourlyEntity) + weatherDao.insertDailyWeather(dailyEntity) + val getWeather = weatherDao.getWeather() + val data = getWeather!!.toCoreEntity(unit = units) + emit(ApiResult.Success(data =data)) } else { val errorMessage = mapResponseCodeToErrorMessage(apiResponse.code()) - Timber.e("Error Message $errorMessage") emit(ApiResult.Error(errorMessage)) } }.catch { throwable -> @@ -69,19 +61,7 @@ class DefaultWeatherRepository @Inject constructor( else -> R.string.error_generic } emit(ApiResult.Error(errorMessage)) - }.onCompletion { - val constraints = Constraints.Builder() - .setRequiredNetworkType(NetworkType.CONNECTED) - .build() - - val refreshWeatherRequest = OneTimeWorkRequestBuilder() - .setConstraints(constraints) - .setInitialDelay(15, TimeUnit.MINUTES) - .build() - - WorkManager.getInstance(context).enqueue(refreshWeatherRequest) } - private fun mapResponseCodeToErrorMessage(code: Int): Int = when (code) { HTTP_UNAUTHORIZED -> R.string.error_unauthorized in 400..499 -> R.string.error_client diff --git a/app/src/main/java/com/github/odaridavid/weatherapp/data/weather/RefreshWeatherUseCase.kt b/app/src/main/java/com/github/odaridavid/weatherapp/data/weather/RefreshWeatherUseCase.kt new file mode 100644 index 0000000..4fd8576 --- /dev/null +++ b/app/src/main/java/com/github/odaridavid/weatherapp/data/weather/RefreshWeatherUseCase.kt @@ -0,0 +1,45 @@ +package com.github.odaridavid.weatherapp.data.weather + +import android.content.Context +import com.github.odaridavid.weatherapp.R +import com.github.odaridavid.weatherapp.core.api.WeatherRepository +import com.github.odaridavid.weatherapp.core.model.DefaultLocation +import com.github.odaridavid.weatherapp.core.model.Weather +import com.github.odaridavid.weatherapp.util.NotificationUtil +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import javax.inject.Inject + +class RefreshWeatherUseCase @Inject constructor( + private val weatherRepository: WeatherRepository, + private val notificationUtil: NotificationUtil, + private val context: Context +) { + operator fun invoke( + defaultLocation: DefaultLocation, + language: String, + units: String + ): Flow> = flow{ + weatherRepository.fetchWeatherData( + defaultLocation = defaultLocation, + language = language, + units =units + ).collect{ result-> + when (result) { + is ApiResult.Success -> { + notificationUtil.makeNotification(context.getString(R.string.weather_updates)) + } + is ApiResult.Error -> { + R.string.error_generic + } + } + } + + } +} + + + + + + diff --git a/app/src/main/java/com/github/odaridavid/weatherapp/di/UseCaseModule.kt b/app/src/main/java/com/github/odaridavid/weatherapp/di/UseCaseModule.kt new file mode 100644 index 0000000..0a52f27 --- /dev/null +++ b/app/src/main/java/com/github/odaridavid/weatherapp/di/UseCaseModule.kt @@ -0,0 +1,29 @@ +package com.github.odaridavid.weatherapp.di + +import android.content.Context +import com.github.odaridavid.weatherapp.data.weather.DefaultWeatherRepository +import com.github.odaridavid.weatherapp.data.weather.RefreshWeatherUseCase +import com.github.odaridavid.weatherapp.util.NotificationUtil +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object UseCaseModule { + @Provides + @Singleton + fun provideRefreshWeatherUseCase( + repository: DefaultWeatherRepository, + notificationUtil: NotificationUtil, + context: Context + ): RefreshWeatherUseCase { + return RefreshWeatherUseCase( + weatherRepository = repository, + notificationUtil = notificationUtil, + context = context + ) + } +} \ 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 index 1333448..44f2425 100644 --- a/app/src/main/java/com/github/odaridavid/weatherapp/worker/UpdatedWeatherWorker.kt +++ b/app/src/main/java/com/github/odaridavid/weatherapp/worker/UpdatedWeatherWorker.kt @@ -4,10 +4,12 @@ import android.content.Context import androidx.hilt.work.HiltWorker import androidx.work.CoroutineWorker import androidx.work.WorkerParameters +import com.github.odaridavid.weatherapp.R import com.github.odaridavid.weatherapp.core.model.DefaultLocation import com.github.odaridavid.weatherapp.core.model.SupportedLanguage import com.github.odaridavid.weatherapp.core.model.Units import com.github.odaridavid.weatherapp.data.weather.DefaultWeatherRepository +import com.github.odaridavid.weatherapp.data.weather.RefreshWeatherUseCase import com.github.odaridavid.weatherapp.util.NotificationUtil import dagger.assisted.Assisted import dagger.assisted.AssistedInject @@ -17,8 +19,7 @@ import dagger.assisted.AssistedInject class UpdateWeatherWorker @AssistedInject constructor( @Assisted context: Context, @Assisted params: WorkerParameters, - private val weatherRepository: DefaultWeatherRepository, - private val notificationUtil: NotificationUtil + private val refreshWeatherUseCase: RefreshWeatherUseCase, ): CoroutineWorker(context, params){ override suspend fun doWork(): Result { @@ -26,12 +27,10 @@ class UpdateWeatherWorker @AssistedInject constructor( val defaultLocation = getDefaultLocation() val language = getDefaultLanguage() val units = getDefaultUnits() - weatherRepository.fetchWeatherData( + refreshWeatherUseCase.invoke( defaultLocation = defaultLocation, language = language, - units = units - ) - notificationUtil.makeNotification("Weather Updated") + units = units) Result.success() } catch(e: Error) { Result.retry() 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..86496b9 --- /dev/null +++ b/app/src/main/java/com/github/odaridavid/weatherapp/worker/WeatherUpdateScheduler.kt @@ -0,0 +1,27 @@ +package com.github.odaridavid.weatherapp.worker + +import android.content.Context +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 context: Context +) { + private val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + + fun schedulePeriodicWeatherUpdates() { + val refreshWeatherRequest = PeriodicWorkRequestBuilder( + repeatInterval = 15, + repeatIntervalTimeUnit = TimeUnit.MINUTES) + .setConstraints(constraints) + .build() + + WorkManager.getInstance(context).enqueue(refreshWeatherRequest) + } +} From 1faa9b52deada8402256a82c7cd48d41f59c44e7 Mon Sep 17 00:00:00 2001 From: Caleb Langat <95022986+Mzazi25@users.noreply.github.com> Date: Fri, 26 May 2023 10:33:01 +0300 Subject: [PATCH 20/42] removed raw strings and updated strings.xml --- .../com/github/odaridavid/weatherapp/util/NotificationUtil.kt | 4 ++-- app/src/main/res/values/strings.xml | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) 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 index 0211962..8a586e9 100644 --- a/app/src/main/java/com/github/odaridavid/weatherapp/util/NotificationUtil.kt +++ b/app/src/main/java/com/github/odaridavid/weatherapp/util/NotificationUtil.kt @@ -22,8 +22,8 @@ class NotificationUtil @Inject constructor( private fun createNotificationChannel() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val name = "notification" - val descriptionText = "description" + 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 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 From 078cfebee60a3618a91c5f80ed0d22b69e07f6c0 Mon Sep 17 00:00:00 2001 From: Caleb Langat <95022986+Mzazi25@users.noreply.github.com> Date: Fri, 26 May 2023 10:33:25 +0300 Subject: [PATCH 21/42] removed timber config --- .../odaridavid/weatherapp/WeatherApp.kt | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) 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 cc10020..79162b1 100644 --- a/app/src/main/java/com/github/odaridavid/weatherapp/WeatherApp.kt +++ b/app/src/main/java/com/github/odaridavid/weatherapp/WeatherApp.kt @@ -3,20 +3,27 @@ package com.github.odaridavid.weatherapp import android.app.Application import androidx.hilt.work.HiltWorkerFactory import androidx.work.Configuration +import com.github.odaridavid.weatherapp.worker.WeatherUpdateScheduler import dagger.hilt.android.HiltAndroidApp -import timber.log.Timber import javax.inject.Inject @HiltAndroidApp class WeatherApp : Application(), Configuration.Provider{ + @Inject + lateinit var workerFactory: HiltWorkerFactory + + @Inject + lateinit var weatherUpdateScheduler: WeatherUpdateScheduler + override fun onCreate() { super.onCreate() - if (BuildConfig.DEBUG) { - Timber.plant(Timber.DebugTree()) - } + schedulePeriodicWeatherUpdates() } - @Inject - lateinit var workerFactory: HiltWorkerFactory + override fun getWorkManagerConfiguration(): Configuration = Configuration.Builder().setWorkerFactory(workerFactory).build() + + private fun schedulePeriodicWeatherUpdates() { + weatherUpdateScheduler.schedulePeriodicWeatherUpdates() + } } From 715ca3f347a18d6ee614d0eaf1c8235fbaa00a6d Mon Sep 17 00:00:00 2001 From: Caleb Langat <95022986+Mzazi25@users.noreply.github.com> Date: Tue, 30 May 2023 15:50:37 +0300 Subject: [PATCH 22/42] Added DI for the WeatherUpdateScheduler --- .../com/github/odaridavid/weatherapp/di/LocalModule.kt | 7 +++++++ 1 file changed, 7 insertions(+) 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 index dcabbd1..471742c 100644 --- a/app/src/main/java/com/github/odaridavid/weatherapp/di/LocalModule.kt +++ b/app/src/main/java/com/github/odaridavid/weatherapp/di/LocalModule.kt @@ -6,6 +6,7 @@ import com.github.odaridavid.weatherapp.data.weather.local.WeatherDatabase import com.github.odaridavid.weatherapp.data.weather.local.converters.Converters import com.github.odaridavid.weatherapp.data.weather.local.dao.WeatherDao import com.github.odaridavid.weatherapp.util.NotificationUtil +import com.github.odaridavid.weatherapp.worker.WeatherUpdateScheduler import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -31,6 +32,12 @@ object LocalModule { return NotificationUtil(context) } + @Singleton + @Provides + fun provideWeatherUpdateScheduler(@ApplicationContext context: Context): WeatherUpdateScheduler { + return WeatherUpdateScheduler(context) + } + @Provides @Singleton fun providesWeatherDao(db: WeatherDatabase): WeatherDao { From d63ad19c37eef1a3ad02f3fd13e6a9cb28c2d07c Mon Sep 17 00:00:00 2001 From: Caleb Langat <95022986+Mzazi25@users.noreply.github.com> Date: Tue, 30 May 2023 15:51:25 +0300 Subject: [PATCH 23/42] Fixed failing tests and updated Fakes --- .../com/github/odaridavid/weatherapp/Fakes.kt | 41 +++++++++++++++---- .../HomeViewModelIntegrationTest.kt | 18 +++----- .../weatherapp/WeatherRepositoryUnitTest.kt | 8 +--- 3 files changed, 41 insertions(+), 26 deletions(-) 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 42dce68..8d1c148 100644 --- a/app/src/test/java/com/github/odaridavid/weatherapp/Fakes.kt +++ b/app/src/test/java/com/github/odaridavid/weatherapp/Fakes.kt @@ -23,7 +23,14 @@ 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() @@ -33,16 +40,30 @@ val fakeSuccessMappedWeatherResponse = Weather( current = CurrentWeather( temperature = "3°C", feelsLike = "2°C", - weather = listOf() + weather = listOf( + WeatherInfo( + id = 1, + main = "main", + description = "desc", + icon = "icon" + ) + ) ), hourly = listOf(), daily = listOf() ) val fakeSuccessResponse = Weather( current = CurrentWeather( - temperature = "0°C", - feelsLike = "0°C", - weather = listOf() + temperature = "3°C", + feelsLike = "2°C", + weather = listOf( + WeatherInfo( + id = 1, + main = "main", + description = "desc", + icon = "icon" + ) + ) ), hourly = listOf(), daily = listOf() @@ -50,9 +71,15 @@ val fakeSuccessResponse = Weather( val fakePopulatedResponse = PopulatedWeather( current = WeatherEntity( + dt = 1L, + main = "main", + temp = 3.0F, + temp_max = 0.0F, + temp_min = 0.0F, + description ="desc", + icon = "icon", + id = 1, feels_like = 2.0f, - temp = 3.0f, - weather = listOf() ), hourly = listOf(), daily = listOf() 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 619a609..db02163 100644 --- a/app/src/test/java/com/github/odaridavid/weatherapp/HomeViewModelIntegrationTest.kt +++ b/app/src/test/java/com/github/odaridavid/weatherapp/HomeViewModelIntegrationTest.kt @@ -1,6 +1,6 @@ package com.github.odaridavid.weatherapp -import android.content.Context +import androidx.arch.core.executor.testing.InstantTaskExecutorRule import app.cash.turbine.test import com.github.odaridavid.weatherapp.core.api.WeatherRepository import com.github.odaridavid.weatherapp.core.model.DefaultLocation @@ -11,7 +11,6 @@ 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 -import androidx.arch.core.executor.testing.InstantTaskExecutorRule import com.google.common.truth.Truth import io.mockk.coEvery import io.mockk.impl.annotations.MockK @@ -35,9 +34,6 @@ class HomeViewModelIntegrationTest { @MockK val mockWeatherDao = mockk(relaxed = true) - @MockK - val mockContext = mockk(relaxed = true) - @get:Rule val coroutineRule = MainCoroutineRule() @@ -58,12 +54,12 @@ class HomeViewModelIntegrationTest { } returns Response.success( fakeSuccessWeatherResponse ) + coEvery { mockWeatherDao.getWeather() } returns fakePopulatedResponse val weatherRepository = DefaultWeatherRepository( openWeatherService = mockOpenWeatherService, weatherDao = mockWeatherDao, - context = mockContext ) val viewModel = createViewModel(weatherRepository = weatherRepository) @@ -78,7 +74,7 @@ class HomeViewModelIntegrationTest { ), locationName = "-", language = "English", - weather = fakeSuccessMappedWeatherResponse, + weather = fakeSuccessResponse, isLoading = false, errorMessageId = null ) @@ -110,9 +106,7 @@ class HomeViewModelIntegrationTest { val weatherRepository = DefaultWeatherRepository( openWeatherService = mockOpenWeatherService, - weatherDao = mockWeatherDao, - context = mockContext - ) + weatherDao = mockWeatherDao) val viewModel = createViewModel(weatherRepository = weatherRepository) @@ -143,9 +137,7 @@ class HomeViewModelIntegrationTest { val weatherRepository = DefaultWeatherRepository( openWeatherService = mockOpenWeatherService, - weatherDao = mockWeatherDao, - context = mockContext - ) + weatherDao = mockWeatherDao) val viewModel = createViewModel(weatherRepository = weatherRepository) 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 defa2fe..14bdd2b 100644 --- a/app/src/test/java/com/github/odaridavid/weatherapp/WeatherRepositoryUnitTest.kt +++ b/app/src/test/java/com/github/odaridavid/weatherapp/WeatherRepositoryUnitTest.kt @@ -31,8 +31,6 @@ class WeatherRepositoryUnitTest { val mockOpenWeatherService = mockk(relaxed = true) @MockK val mockWeatherDao = mockk(relaxed = true) - @MockK - val mockContext = mockk(relaxed = true) @Before fun setUp() { @@ -53,7 +51,7 @@ class WeatherRepositoryUnitTest { } returns Response.success( fakeSuccessWeatherResponse ) - coEvery { mockWeatherDao.getWeather() } returns fakeSuccessMappedEntityResponse + coEvery { mockWeatherDao.getWeather() } returns fakePopulatedResponse val weatherRepository = createWeatherRepository() @@ -275,7 +273,5 @@ class WeatherRepositoryUnitTest { private fun createWeatherRepository(): WeatherRepository = DefaultWeatherRepository( openWeatherService = mockOpenWeatherService, - weatherDao = mockWeatherDao, - context = mockContext - ) + weatherDao = mockWeatherDao) } From 64e6c37c42c305ea7acc77571ba5402ed2414f19 Mon Sep 17 00:00:00 2001 From: Caleb Langat <95022986+Mzazi25@users.noreply.github.com> Date: Tue, 30 May 2023 16:51:59 +0300 Subject: [PATCH 24/42] Merge branch 'develop' into offlineCache # Conflicts: # app/build.gradle.kts # gradle/libs.versions.toml --- README.md | 2 + app/build.gradle.kts | 111 ++++++++++++++++-- .../odaridavid/weatherapp/di/ClientModule.kt | 23 +++- gradle/libs.versions.toml | 6 + 4 files changed, 132 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index f50f4b9..50480f2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ ### Weather App [![Build Status](https://app.bitrise.io/app/80f9b4627fc90757/status.svg?token=3KnRQl0WRfDT5UTzPDiRgA&branch=develop)](https://app.bitrise.io/app/80f9b4627fc90757) +[![codecov](https://codecov.io/gh/odaridavid/WeatherApp/branch/develop/graph/badge.svg?token=eZcGjGhF83)](https://codecov.io/gh/odaridavid/WeatherApp) *Summary* @@ -93,6 +94,7 @@ is written that makes use of fake,so as to mimic the real scenario as much as po *Tooling/Project setup* - [Gradle secrets plugin](https://github.com/google/secrets-gradle-plugin) - [Hilt(DI)](https://developer.android.com/training/dependency-injection/hilt-android) +- [Firebase - Crashlytics](https://firebase.google.com/docs/crashlytics) # LICENSE diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 48f60a1..ac05889 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -7,6 +7,94 @@ plugins { alias(libs.plugins.org.jetbrains.kotlin.plugin.serialization) id("com.google.gms.google-services") id("com.google.firebase.crashlytics") + jacoco +} + +jacoco { + toolVersion = "0.8.8" +} + +project.afterEvaluate { + setupAndroidReporting() +} + +fun setupAndroidReporting() { + val buildTypes = listOf("debug") + + buildTypes.forEach { buildTypeName -> + var sourceName = buildTypeName + val testTaskName = "test${sourceName.capitalize()}UnitTest" + println("Task -> $testTaskName") + + tasks.register("${testTaskName}Coverage") { + dependsOn(tasks.findByName(testTaskName)) + + group = "Reporting" + description = "Generate Jacoco coverage reports on the ${sourceName.capitalize()} build." + + reports { + xml.required.set(true) + csv.required.set(false) + html.required.set(true) + } + + val fileFilter = listOf( + // android + "**/R.class", + "**/R$*.class", + "**/BuildConfig.*", + "**/Manifest*.*", + "**/*Test*.*", + "android/**/*.*", + // kotlin + "**/*MapperImpl*.*", + "**/*\$ViewInjector*.*", + "**/*\$ViewBinder*.*", + "**/BuildConfig.*", + "**/*Component*.*", + "**/*BR*.*", + "**/Manifest*.*", + "**/*\$Lambda$*.*", + "**/*Companion*.*", + "**/*Module*.*", + "**/*Dagger*.*", + "**/*Hilt*.*", + "**/*MembersInjector*.*", + "**/*_MembersInjector.class", + "**/*_Factory*.*", + "**/*_Provide*Factory*.*", + "**/*Extensions*.*", + // sealed and data classes + "**/*\$Result.*", + "**/*\$Result$*.*", + // adapters generated by moshi + "**/*JsonAdapter.*", + "**/*Activity*", + "**/di/**", + "**/hilt*/**", + "**/entrypoint/**", + "**/theme/**", + "**/*Screen*.*" + ) + + val javaTree = fileTree("${project.buildDir}/intermediates/javac/$sourceName/classes"){ + exclude(fileFilter) + } + val kotlinTree = fileTree("${project.buildDir}/tmp/kotlin-classes/$sourceName"){ + exclude(fileFilter) + } + classDirectories.setFrom(files(javaTree, kotlinTree)) + + executionData.setFrom(files("${project.buildDir}/jacoco/${testTaskName}.exec")) + val coverageSourceDirs = listOf( + "${project.projectDir}/src/main/java", + "${project.projectDir}/src/$buildTypeName/java" + ) + + sourceDirectories.setFrom(files(coverageSourceDirs)) + additionalSourceDirs.setFrom(files(coverageSourceDirs)) + } + } } android { @@ -75,6 +163,7 @@ dependencies { implementation(libs.kotlinx.serialization.converter) implementation(libs.kotlinx.serialization) implementation(libs.coil) + // DI implementation(libs.hilt.android) kapt(libs.hilt.compiler) @@ -83,15 +172,6 @@ dependencies { implementation(platform(libs.firebase.bom)) implementation(libs.bundles.firebase) - //WorkManager - implementation(libs.androidx.work.ktx) - implementation(libs.hilt.ext.work) - kapt(libs.hilt.compiler) - - //Room - implementation(libs.room.ktx) - implementation(libs.room.runtime) - kapt(libs.room.compiler) // Test testImplementation(libs.junit) testImplementation(libs.turbine) @@ -107,6 +187,19 @@ dependencies { 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) + releaseImplementation(libs.chucker.release) } kapt { diff --git a/app/src/main/java/com/github/odaridavid/weatherapp/di/ClientModule.kt b/app/src/main/java/com/github/odaridavid/weatherapp/di/ClientModule.kt index 7921437..25938a2 100644 --- a/app/src/main/java/com/github/odaridavid/weatherapp/di/ClientModule.kt +++ b/app/src/main/java/com/github/odaridavid/weatherapp/di/ClientModule.kt @@ -1,11 +1,15 @@ package com.github.odaridavid.weatherapp.di +import android.content.Context +import com.chuckerteam.chucker.api.ChuckerCollector +import com.chuckerteam.chucker.api.ChuckerInterceptor import com.github.odaridavid.weatherapp.BuildConfig import com.github.odaridavid.weatherapp.data.weather.OpenWeatherService import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory import dagger.Module import dagger.Provides import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json @@ -38,10 +42,14 @@ object ClientModule { @Provides @Singleton - fun provideOkhttpClient(loggingInterceptor: HttpLoggingInterceptor): OkHttpClient = + fun provideOkhttpClient( + loggingInterceptor: HttpLoggingInterceptor, + chuckerInterceptor: ChuckerInterceptor, + ): OkHttpClient = OkHttpClient.Builder() .connectTimeout(60L, TimeUnit.SECONDS) .addInterceptor(loggingInterceptor) + .addInterceptor(chuckerInterceptor) .build() @Provides @@ -55,6 +63,19 @@ object ClientModule { } } + @Provides + @Singleton + fun provideChuckerInterceptor( + @ApplicationContext context:Context, + ): ChuckerInterceptor { + return ChuckerInterceptor.Builder(context = context) + .collector(ChuckerCollector(context = context)) + .maxContentLength(length = 250000L) + .redactHeaders(headerNames = emptySet()) + .alwaysReadResponseBody(enable = false) + .build() + } + @Provides @Singleton fun provideOpenWeatherService(retrofit: Retrofit): OpenWeatherService = diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0d6ba3a..d5b004c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -42,6 +42,8 @@ crashlytics-plugin = "2.9.5" room = "2.5.1" hiltExt = "1.0.0" +# Chucker +chucker = "3.5.2" [libraries] #AndroidX @@ -93,6 +95,10 @@ firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.r firebase-analytics = { group = "com.google.firebase", name = "firebase-analytics-ktx" } firebase-crashlytics = { group = "com.google.firebase", name = "firebase-crashlytics-ktx" } +#chucker +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" } + #Room room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" } room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" } From 101ddb363be4bc1b2b9d5c93be6b8bfc5d6beb81 Mon Sep 17 00:00:00 2001 From: Caleb Langat <95022986+Mzazi25@users.noreply.github.com> Date: Tue, 30 May 2023 17:07:06 +0300 Subject: [PATCH 25/42] Adds Logger to DefaultWeatherRepository, HomeViewModelIntegrationTest and Weather --- .../weatherapp/data/weather/DefaultWeatherRepository.kt | 3 +++ 1 file changed, 3 insertions(+) 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 6307976..52ae775 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 @@ -2,6 +2,7 @@ package com.github.odaridavid.weatherapp.data.weather import com.github.odaridavid.weatherapp.BuildConfig import com.github.odaridavid.weatherapp.R +import com.github.odaridavid.weatherapp.core.api.Logger import com.github.odaridavid.weatherapp.core.api.WeatherRepository import com.github.odaridavid.weatherapp.core.model.DefaultLocation import com.github.odaridavid.weatherapp.core.model.ExcludedData @@ -18,6 +19,7 @@ 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, @@ -60,6 +62,7 @@ class DefaultWeatherRepository @Inject constructor( is IOException -> R.string.error_connection else -> R.string.error_generic } + logger.logException(throwable) emit(ApiResult.Error(errorMessage)) } private fun mapResponseCodeToErrorMessage(code: Int): Int = when (code) { From 8f4f9836390716b10572c9f8abcfbc6cac2a3009 Mon Sep 17 00:00:00 2001 From: Caleb Langat <95022986+Mzazi25@users.noreply.github.com> Date: Tue, 30 May 2023 17:07:44 +0300 Subject: [PATCH 26/42] Adds Logger toHomeViewModelIntegrationTest and WeatherRepositoryUnitTest --- .../weatherapp/HomeViewModelIntegrationTest.kt | 14 ++++++++++++-- .../weatherapp/WeatherRepositoryUnitTest.kt | 8 +++++++- 2 files changed, 19 insertions(+), 3 deletions(-) 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 db02163..e4faf0f 100644 --- a/app/src/test/java/com/github/odaridavid/weatherapp/HomeViewModelIntegrationTest.kt +++ b/app/src/test/java/com/github/odaridavid/weatherapp/HomeViewModelIntegrationTest.kt @@ -2,6 +2,7 @@ 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 import com.github.odaridavid.weatherapp.core.model.DefaultLocation import com.github.odaridavid.weatherapp.data.weather.DefaultWeatherRepository @@ -37,6 +38,9 @@ class HomeViewModelIntegrationTest { @get:Rule val coroutineRule = MainCoroutineRule() + @MockK + val mockLogger = mockk(relaxed = true) + @get:Rule val instantExecutorRule = InstantTaskExecutorRule() @@ -60,6 +64,7 @@ class HomeViewModelIntegrationTest { DefaultWeatherRepository( openWeatherService = mockOpenWeatherService, weatherDao = mockWeatherDao, + logger = mockLogger ) val viewModel = createViewModel(weatherRepository = weatherRepository) @@ -106,7 +111,10 @@ class HomeViewModelIntegrationTest { val weatherRepository = DefaultWeatherRepository( openWeatherService = mockOpenWeatherService, - weatherDao = mockWeatherDao) + weatherDao = mockWeatherDao, + logger = mockLogger + + ) val viewModel = createViewModel(weatherRepository = weatherRepository) @@ -137,7 +145,9 @@ class HomeViewModelIntegrationTest { val weatherRepository = DefaultWeatherRepository( openWeatherService = mockOpenWeatherService, - weatherDao = mockWeatherDao) + weatherDao = mockWeatherDao, + logger = mockLogger + ) val viewModel = createViewModel(weatherRepository = weatherRepository) 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 14bdd2b..14f9b3b 100644 --- a/app/src/test/java/com/github/odaridavid/weatherapp/WeatherRepositoryUnitTest.kt +++ b/app/src/test/java/com/github/odaridavid/weatherapp/WeatherRepositoryUnitTest.kt @@ -4,6 +4,7 @@ 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.api.Logger import com.github.odaridavid.weatherapp.core.api.WeatherRepository import com.github.odaridavid.weatherapp.core.model.DefaultLocation import com.github.odaridavid.weatherapp.data.weather.ApiResult @@ -32,6 +33,9 @@ class WeatherRepositoryUnitTest { @MockK val mockWeatherDao = mockk(relaxed = true) + @MockK + val mockLogger = mockk(relaxed = true) + @Before fun setUp() { val context = ApplicationProvider.getApplicationContext() @@ -273,5 +277,7 @@ class WeatherRepositoryUnitTest { private fun createWeatherRepository(): WeatherRepository = DefaultWeatherRepository( openWeatherService = mockOpenWeatherService, - weatherDao = mockWeatherDao) + weatherDao = mockWeatherDao, + logger = mockLogger + ) } From 8d8a38b1505ef9a01e1abb5fe83a72b09a805d0d Mon Sep 17 00:00:00 2001 From: Caleb Langat <95022986+Mzazi25@users.noreply.github.com> Date: Thu, 8 Jun 2023 21:28:13 +0300 Subject: [PATCH 27/42] Created RefreshWeatherUseCase Interface --- .../weatherapp/core/api/RefreshWeatherUseCase.kt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 app/src/main/java/com/github/odaridavid/weatherapp/core/api/RefreshWeatherUseCase.kt 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..a0833a0 --- /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.model.DefaultLocation +import com.github.odaridavid.weatherapp.core.model.Weather +import com.github.odaridavid.weatherapp.data.weather.ApiResult +import kotlinx.coroutines.flow.Flow + +interface RefreshWeatherUseCase { + operator fun invoke( + defaultLocation: DefaultLocation, + language: String, + units: String + ): Flow> +} \ No newline at end of file From 2fdf3c3f7464b3917216c7065728f92040fd11f3 Mon Sep 17 00:00:00 2001 From: Caleb Langat <95022986+Mzazi25@users.noreply.github.com> Date: Thu, 8 Jun 2023 21:31:02 +0300 Subject: [PATCH 28/42] Refactored Usecase to implement the interface created and updated DI --- ...ase.kt => DefaultRefreshWeatherUseCase.kt} | 7 ++-- .../weatherapp/di/RepositoryModule.kt | 4 +++ .../odaridavid/weatherapp/di/UseCaseModule.kt | 29 --------------- .../weatherapp/worker/UpdatedWeatherWorker.kt | 35 +++++-------------- 4 files changed, 16 insertions(+), 59 deletions(-) rename app/src/main/java/com/github/odaridavid/weatherapp/data/weather/{RefreshWeatherUseCase.kt => DefaultRefreshWeatherUseCase.kt} (86%) delete mode 100644 app/src/main/java/com/github/odaridavid/weatherapp/di/UseCaseModule.kt diff --git a/app/src/main/java/com/github/odaridavid/weatherapp/data/weather/RefreshWeatherUseCase.kt b/app/src/main/java/com/github/odaridavid/weatherapp/data/weather/DefaultRefreshWeatherUseCase.kt similarity index 86% rename from app/src/main/java/com/github/odaridavid/weatherapp/data/weather/RefreshWeatherUseCase.kt rename to app/src/main/java/com/github/odaridavid/weatherapp/data/weather/DefaultRefreshWeatherUseCase.kt index 4fd8576..8aea190 100644 --- a/app/src/main/java/com/github/odaridavid/weatherapp/data/weather/RefreshWeatherUseCase.kt +++ b/app/src/main/java/com/github/odaridavid/weatherapp/data/weather/DefaultRefreshWeatherUseCase.kt @@ -2,6 +2,7 @@ package com.github.odaridavid.weatherapp.data.weather import android.content.Context import com.github.odaridavid.weatherapp.R +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 @@ -10,12 +11,12 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import javax.inject.Inject -class RefreshWeatherUseCase @Inject constructor( +class DefaultRefreshWeatherUseCase @Inject constructor( private val weatherRepository: WeatherRepository, private val notificationUtil: NotificationUtil, private val context: Context -) { - operator fun invoke( +) :RefreshWeatherUseCase { + override operator fun invoke( defaultLocation: DefaultLocation, language: String, units: String 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..9911d2b 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,11 +1,13 @@ 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 @@ -24,4 +26,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/di/UseCaseModule.kt b/app/src/main/java/com/github/odaridavid/weatherapp/di/UseCaseModule.kt deleted file mode 100644 index 0a52f27..0000000 --- a/app/src/main/java/com/github/odaridavid/weatherapp/di/UseCaseModule.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.github.odaridavid.weatherapp.di - -import android.content.Context -import com.github.odaridavid.weatherapp.data.weather.DefaultWeatherRepository -import com.github.odaridavid.weatherapp.data.weather.RefreshWeatherUseCase -import com.github.odaridavid.weatherapp.util.NotificationUtil -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -object UseCaseModule { - @Provides - @Singleton - fun provideRefreshWeatherUseCase( - repository: DefaultWeatherRepository, - notificationUtil: NotificationUtil, - context: Context - ): RefreshWeatherUseCase { - return RefreshWeatherUseCase( - weatherRepository = repository, - notificationUtil = notificationUtil, - context = context - ) - } -} \ 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 index 44f2425..1c121a0 100644 --- a/app/src/main/java/com/github/odaridavid/weatherapp/worker/UpdatedWeatherWorker.kt +++ b/app/src/main/java/com/github/odaridavid/weatherapp/worker/UpdatedWeatherWorker.kt @@ -4,29 +4,25 @@ import android.content.Context import androidx.hilt.work.HiltWorker import androidx.work.CoroutineWorker import androidx.work.WorkerParameters -import com.github.odaridavid.weatherapp.R -import com.github.odaridavid.weatherapp.core.model.DefaultLocation -import com.github.odaridavid.weatherapp.core.model.SupportedLanguage -import com.github.odaridavid.weatherapp.core.model.Units -import com.github.odaridavid.weatherapp.data.weather.DefaultWeatherRepository -import com.github.odaridavid.weatherapp.data.weather.RefreshWeatherUseCase -import com.github.odaridavid.weatherapp.util.NotificationUtil +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: RefreshWeatherUseCase, + private val refreshWeatherUseCase: DefaultRefreshWeatherUseCase, + private val settingsRepository: SettingsRepository ): CoroutineWorker(context, params){ override suspend fun doWork(): Result { return try { - val defaultLocation = getDefaultLocation() - val language = getDefaultLanguage() - val units = getDefaultUnits() + val defaultLocation = settingsRepository.getDefaultLocation().first() + val language = settingsRepository.getLanguage().first() + val units = settingsRepository.getUnits().first() refreshWeatherUseCase.invoke( defaultLocation = defaultLocation, language = language, @@ -36,19 +32,4 @@ class UpdateWeatherWorker @AssistedInject constructor( Result.retry() } } - - private fun getDefaultLocation(): DefaultLocation { - return DefaultLocation( - latitude = 12.11, - longitude = 98.55, - ) - } - - private fun getDefaultLanguage(): String { - return SupportedLanguage.values().toString() - } - - private fun getDefaultUnits(): String { - return Units.METRIC.value - } } \ No newline at end of file From f92849205d5b4a98fd68e121ba5761f145cc84ed Mon Sep 17 00:00:00 2001 From: Caleb Langat <95022986+Mzazi25@users.noreply.github.com> Date: Thu, 8 Jun 2023 22:27:23 +0300 Subject: [PATCH 29/42] Merge develop --- .../core/api/RefreshWeatherUseCase.kt | 4 +-- .../weatherapp/core/api/WeatherRepository.kt | 4 +-- .../weatherapp/data/weather/ApiResult.kt | 9 ----- .../weather/DefaultRefreshWeatherUseCase.kt | 7 ++-- .../data/weather/DefaultWeatherRepository.kt | 26 +++++--------- .../weatherapp/data/weather/Mappers.kt | 18 ++++++++++ .../weatherapp/ui/home/HomeViewModel.kt | 10 +++--- .../weatherapp/WeatherRepositoryUnitTest.kt | 35 ++++++++++--------- 8 files changed, 58 insertions(+), 55 deletions(-) delete mode 100644 app/src/main/java/com/github/odaridavid/weatherapp/data/weather/ApiResult.kt 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 index a0833a0..576b2bc 100644 --- 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 @@ -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.data.weather.ApiResult import kotlinx.coroutines.flow.Flow interface RefreshWeatherUseCase { @@ -10,5 +10,5 @@ interface RefreshWeatherUseCase { defaultLocation: DefaultLocation, language: String, units: String - ): Flow> + ): 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 a1e6153..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.data.weather.ApiResult import kotlinx.coroutines.flow.Flow interface WeatherRepository { @@ -10,5 +10,5 @@ interface WeatherRepository { defaultLocation: DefaultLocation, language: String, units: String - ) : Flow> + ) : Flow> } diff --git a/app/src/main/java/com/github/odaridavid/weatherapp/data/weather/ApiResult.kt b/app/src/main/java/com/github/odaridavid/weatherapp/data/weather/ApiResult.kt deleted file mode 100644 index 5cdc40d..0000000 --- a/app/src/main/java/com/github/odaridavid/weatherapp/data/weather/ApiResult.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.github.odaridavid.weatherapp.data.weather - -import androidx.annotation.StringRes - -sealed class ApiResult { - data class Success(val data: T) : ApiResult() - - data class Error(@StringRes val messageId: Int) : ApiResult() -} 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 index 8aea190..a06229a 100644 --- 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 @@ -2,6 +2,7 @@ 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 @@ -20,17 +21,17 @@ class DefaultRefreshWeatherUseCase @Inject constructor( defaultLocation: DefaultLocation, language: String, units: String - ): Flow> = flow{ + ): Flow> = flow{ weatherRepository.fetchWeatherData( defaultLocation = defaultLocation, language = language, units =units ).collect{ result-> when (result) { - is ApiResult.Success -> { + is Result.Success -> { notificationUtil.makeNotification(context.getString(R.string.weather_updates)) } - is ApiResult.Error -> { + 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 52ae775..2c5dd2b 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 @@ -2,6 +2,7 @@ package com.github.odaridavid.weatherapp.data.weather import com.github.odaridavid.weatherapp.BuildConfig import com.github.odaridavid.weatherapp.R +import com.github.odaridavid.weatherapp.core.Result import com.github.odaridavid.weatherapp.core.api.Logger import com.github.odaridavid.weatherapp.core.api.WeatherRepository import com.github.odaridavid.weatherapp.core.model.DefaultLocation @@ -25,11 +26,11 @@ class DefaultWeatherRepository @Inject constructor( defaultLocation: DefaultLocation, language: String, units: String - ): Flow> = flow { + ): Flow> = flow { val cachedWeather = weatherDao.getWeather() if (cachedWeather != null && cachedWeather.isDataValid()) { val weatherData = cachedWeather.toCoreEntity(unit = units) - emit(ApiResult.Success(data = weatherData)) + emit(Result.Success(data = weatherData)) return@flow } val excludedData = "${ExcludedData.MINUTELY.value},${ExcludedData.ALERTS.value}" @@ -52,24 +53,15 @@ class DefaultWeatherRepository @Inject constructor( weatherDao.insertDailyWeather(dailyEntity) val getWeather = weatherDao.getWeather() val data = getWeather!!.toCoreEntity(unit = units) - emit(ApiResult.Success(data =data)) + emit(Result.Success(data =data)) } else { - val errorMessage = mapResponseCodeToErrorMessage(apiResponse.code()) - emit(ApiResult.Error(errorMessage)) - } - }.catch { throwable -> - val errorMessage = when (throwable) { - is IOException -> R.string.error_connection - else -> R.string.error_generic + val errorType = mapResponseCodeToErrorType(apiResponse.code()) + emit(Result.Error(errorType = errorType)) } + }.catch { throwable: Throwable -> + val errorType = mapThrowableToErrorType(throwable) logger.logException(throwable) - emit(ApiResult.Error(errorMessage)) - } - private fun mapResponseCodeToErrorMessage(code: Int): Int = when (code) { - HTTP_UNAUTHORIZED -> R.string.error_unauthorized - in 400..499 -> R.string.error_client - in 500..600 -> R.string.error_server - else -> R.string.error_generic + emit(Result.Error(errorType)) } private fun getLanguageValue(language: String) = 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 9b8515c..d0c2a3e 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 @@ -1,6 +1,7 @@ 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.model.CurrentWeather import com.github.odaridavid.weatherapp.core.model.DailyWeather import com.github.odaridavid.weatherapp.core.model.HourlyWeather @@ -14,6 +15,8 @@ import com.github.odaridavid.weatherapp.data.weather.local.entity.PopulatedWeath 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 @@ -181,4 +184,19 @@ fun TemperatureResponse.toTemperatureEntity(): TemperatureEntity { min = min, max = max ) +} + +fun mapThrowableToErrorType(throwable: Throwable): ErrorType { + val errorType = when (throwable) { + is IOException -> ErrorType.IO_CONNECTION + else -> ErrorType.GENERIC + } + return errorType +} + +fun mapResponseCodeToErrorType(code: Int): ErrorType = when (code) { + HttpURLConnection.HTTP_UNAUTHORIZED -> ErrorType.UNAUTHORIZED + 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/ui/home/HomeViewModel.kt b/app/src/main/java/com/github/odaridavid/weatherapp/ui/home/HomeViewModel.kt index 19e7faf..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,11 +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.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.data.weather.ApiResult import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -61,9 +61,9 @@ class HomeViewModel @Inject constructor( } } - private fun processResult(result: ApiResult) { + private fun processResult(result: Result) { when (result) { - is ApiResult.Success -> { + is Result.Success -> { val weatherData = result.data setState { copy( @@ -74,11 +74,11 @@ class HomeViewModel @Inject constructor( } } - is ApiResult.Error -> { + is Result.Error -> { setState { copy( isLoading = false, - errorMessageId = result.messageId + errorMessageId = mapErrorTypeToResourceId(result.errorType) ) } } 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 14f9b3b..34b5b5b 100644 --- a/app/src/test/java/com/github/odaridavid/weatherapp/WeatherRepositoryUnitTest.kt +++ b/app/src/test/java/com/github/odaridavid/weatherapp/WeatherRepositoryUnitTest.kt @@ -4,10 +4,11 @@ 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 import com.github.odaridavid.weatherapp.core.api.WeatherRepository import com.github.odaridavid.weatherapp.core.model.DefaultLocation -import com.github.odaridavid.weatherapp.data.weather.ApiResult +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 @@ -70,8 +71,8 @@ class WeatherRepositoryUnitTest { units = "metric" ).test { awaitItem().also { result -> - Truth.assertThat(result).isInstanceOf(ApiResult.Success::class.java) - Truth.assertThat((result as ApiResult.Success).data).isEqualTo(expectedResult) + Truth.assertThat(result).isInstanceOf(Result.Success::class.java) + Truth.assertThat((result as Result.Success).data).isEqualTo(expectedResult) } awaitComplete() } @@ -104,8 +105,8 @@ class WeatherRepositoryUnitTest { units = "metric" ).test { awaitItem().also { result -> - Truth.assertThat(result).isInstanceOf(ApiResult.Error::class.java) - Truth.assertThat((result as ApiResult.Error).messageId).isEqualTo(R.string.error_server) + Truth.assertThat(result).isInstanceOf(Result.Error::class.java) + Truth.assertThat((result as Result.Error).errorType).isEqualTo(ErrorType.SERVER) } awaitComplete() } @@ -138,8 +139,8 @@ class WeatherRepositoryUnitTest { units = "metric" ).test { awaitItem().also { result -> - Truth.assertThat(result).isInstanceOf(ApiResult.Error::class.java) - Truth.assertThat((result as ApiResult.Error).messageId).isEqualTo(R.string.error_client) + Truth.assertThat(result).isInstanceOf(Result.Error::class.java) + Truth.assertThat((result as Result.Error).errorType).isEqualTo(ErrorType.CLIENT) } awaitComplete() } @@ -172,8 +173,8 @@ class WeatherRepositoryUnitTest { units = "metric" ).test { awaitItem().also { result -> - Truth.assertThat(result).isInstanceOf(ApiResult.Error::class.java) - Truth.assertThat((result as ApiResult.Error).messageId).isEqualTo(R.string.error_unauthorized) + Truth.assertThat(result).isInstanceOf(Result.Error::class.java) + Truth.assertThat((result as Result.Error).errorType).isEqualTo(ErrorType.UNAUTHORIZED) } awaitComplete() } @@ -206,8 +207,8 @@ class WeatherRepositoryUnitTest { units = "metric" ).test { awaitItem().also { result -> - Truth.assertThat(result).isInstanceOf(ApiResult.Error::class.java) - Truth.assertThat((result as ApiResult.Error).messageId).isEqualTo(R.string.error_generic) + Truth.assertThat(result).isInstanceOf(Result.Error::class.java) + Truth.assertThat((result as Result.Error).errorType).isEqualTo(ErrorType.GENERIC) } awaitComplete() } @@ -237,8 +238,8 @@ class WeatherRepositoryUnitTest { units = "metric" ).test { awaitItem().also { result -> - Truth.assertThat(result).isInstanceOf(ApiResult.Error::class.java) - Truth.assertThat((result as ApiResult.Error).messageId).isEqualTo(R.string.error_connection) + Truth.assertThat(result).isInstanceOf(Result.Error::class.java) + Truth.assertThat((result as Result.Error).errorType).isEqualTo(ErrorType.IO_CONNECTION) } awaitComplete() } @@ -268,16 +269,16 @@ class WeatherRepositoryUnitTest { units = "metric" ).test { awaitItem().also { result -> - Truth.assertThat(result).isInstanceOf(ApiResult.Error::class.java) - Truth.assertThat((result as ApiResult.Error).messageId).isEqualTo(R.string.error_generic) + Truth.assertThat(result).isInstanceOf(Result.Error::class.java) + Truth.assertThat((result as Result.Error).errorType).isEqualTo(ErrorType.GENERIC) } awaitComplete() } } - private fun createWeatherRepository(): WeatherRepository = DefaultWeatherRepository( + private fun createWeatherRepository(logger: Logger = mockLogger): WeatherRepository = DefaultWeatherRepository( openWeatherService = mockOpenWeatherService, weatherDao = mockWeatherDao, - logger = mockLogger + logger = logger ) } From 521a1b8637b3df762edd6218309c20e053e05191 Mon Sep 17 00:00:00 2001 From: Caleb Langat <95022986+Mzazi25@users.noreply.github.com> Date: Mon, 19 Jun 2023 08:25:13 +0300 Subject: [PATCH 30/42] created CustomWorkerFactory --- .../weatherapp/worker/CustomWorkerFactory.kt | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 app/src/main/java/com/github/odaridavid/weatherapp/worker/CustomWorkerFactory.kt 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 From 98335c6f6f1599a14ed4e3583b9f8602669ea49f Mon Sep 17 00:00:00 2001 From: Caleb Langat <95022986+Mzazi25@users.noreply.github.com> Date: Mon, 19 Jun 2023 08:26:24 +0300 Subject: [PATCH 31/42] Updated daos, database and entity --- .../data/weather/local/WeatherDatabase.kt | 11 +- .../data/weather/local/dao/WeatherDao.kt | 11 +- .../weather/local/entity/WeatherEntity.kt | 132 ++++++++++-------- 3 files changed, 86 insertions(+), 68 deletions(-) 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 index 1360d28..fb68a6a 100644 --- 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 @@ -2,22 +2,23 @@ package com.github.odaridavid.weatherapp.data.weather.local import androidx.room.Database import androidx.room.RoomDatabase -import androidx.room.TypeConverters -import com.github.odaridavid.weatherapp.data.weather.local.converters.Converters import com.github.odaridavid.weatherapp.data.weather.local.dao.WeatherDao +import com.github.odaridavid.weatherapp.data.weather.local.entity.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], - version = 5, + DailyWeatherEntity::class, + WeatherInfoResponseEntity::class, + CurrentWeatherEntity::class], + version = 6, exportSchema = false ) -@TypeConverters(Converters::class) 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 index 2e8fbc2..9e8d33e 100644 --- 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 @@ -5,20 +5,22 @@ 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 weather_entity") - suspend fun getWeather(): PopulatedWeather? + @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: WeatherEntity) + suspend fun insertCurrentWeather(currentWeather: CurrentWeatherEntity) @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertHourlyWeather(hourly: List) @@ -26,4 +28,7 @@ interface WeatherDao { @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 index be22685..821e1aa 100644 --- 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 @@ -1,105 +1,117 @@ package com.github.odaridavid.weatherapp.data.weather.local.entity -import androidx.room.ColumnInfo import androidx.room.Embedded import androidx.room.Entity -import androidx.room.ForeignKey -import androidx.room.Index import androidx.room.PrimaryKey import androidx.room.Relation -import androidx.room.TypeConverters -import com.github.odaridavid.weatherapp.data.weather.local.converters.Converters data class PopulatedWeather( - @Embedded - val current: WeatherEntity, - - @Relation(parentColumn = "identity", entityColumn ="dt_column", entity = HourlyWeatherEntity::class) - val hourly: List, - - @Relation(parentColumn = "identity", entityColumn ="dt", entity = DailyWeatherEntity::class) - val daily: List + @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 = current.dt + val currentTime = System.currentTimeMillis() val fifteenMinutesAgo = currentTime - 15 * 60 * 1000 - val isCurrentValid = current.lastRefreshed >= fifteenMinutesAgo && current.isValid - val areHourlyValid = hourly.all { it.lastRefreshed >= fifteenMinutesAgo && it.isValid } - val areDailyValid = daily.all { it.lastRefreshed >= fifteenMinutesAgo && it.isValid } + 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(tableName = "weather_entity") +@Entity data class WeatherEntity( - @PrimaryKey @ColumnInfo(name = "identity") - val id: Int, - val dt: Long, - val feels_like: Float, + @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 temp_max: Float, - val temp_min: Float, - val description: String, - val icon: String, - val main: String, val lastRefreshed: Long = 0, - val isValid: Boolean = false + val isValid: Boolean = false, ) -@Entity( - tableName = "hourly_weather", - foreignKeys = [ - ForeignKey(entity = WeatherEntity::class, - parentColumns = ["identity"], - childColumns = ["dt_column"], - onDelete = ForeignKey.CASCADE - )], - indices = [Index("dt_column")] +data class CurrentWithWeatherInfo( + @Embedded val currentWeather: CurrentWeatherEntity, + @Relation( + parentColumn = "currentId", + entityColumn = "id", + ) + val weather: List ) + +@Entity data class HourlyWeatherEntity( - @PrimaryKey - val id: Int= 0, - @ColumnInfo(name = "dt_column") + @PrimaryKey(autoGenerate = true) + val hourlyId: Long = 0, val dt: Long, val temperature: Float, - @TypeConverters(Converters::class) - val weather: List, val lastRefreshed: Long = 0, - val isValid: Boolean = false + val isValid: Boolean = false, ) -@Entity( - tableName = "daily_weather", - foreignKeys = [ - ForeignKey(entity = WeatherEntity::class, - parentColumns = ["identity"], - childColumns = ["dt"], - onDelete = ForeignKey.CASCADE - )], - indices = [Index("dt")] -) +@Entity data class DailyWeatherEntity( - @PrimaryKey - val id: Int= 0, - @ColumnInfo(name = "dt") + @PrimaryKey(autoGenerate = true) + val dailyId: Long = 0, val dt: Long, @Embedded val temperature: TemperatureEntity, - @TypeConverters(Converters::class) - val weather: List, val lastRefreshed: Long = 0, - val isValid: Boolean = false + 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 -) \ No newline at end of file +) From ae05432d79d658806747cae5b08132f7b2e876c7 Mon Sep 17 00:00:00 2001 From: Caleb Langat <95022986+Mzazi25@users.noreply.github.com> Date: Mon, 19 Jun 2023 08:27:45 +0300 Subject: [PATCH 32/42] updated viewmodel component with singleton component --- .../com/github/odaridavid/weatherapp/di/RepositoryModule.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 9911d2b..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 @@ -12,9 +12,10 @@ 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 From 710c0ad3bf0f78fcdc343faa201961ff9d97e1ac Mon Sep 17 00:00:00 2001 From: Caleb Langat <95022986+Mzazi25@users.noreply.github.com> Date: Mon, 19 Jun 2023 08:29:20 +0300 Subject: [PATCH 33/42] Added latitude and longitude into the weather core model and updated mappers --- .../weatherapp/core/model/Weather.kt | 2 + .../weatherapp/data/weather/Mappers.kt | 149 ++++++++++++------ .../data/weather/WeatherResponse.kt | 2 + 3 files changed, 105 insertions(+), 48 deletions(-) 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/Mappers.kt b/app/src/main/java/com/github/odaridavid/weatherapp/data/weather/Mappers.kt index d0c2a3e..11c2944 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,13 +4,18 @@ 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 @@ -23,6 +28,8 @@ 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) } @@ -56,6 +63,13 @@ 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( @@ -64,36 +78,31 @@ fun TemperatureResponse.toCoreModel(unit: String): Temperature = ) fun PopulatedWeather.toCoreEntity(unit: String): Weather = Weather( - current = toCurrentWeather(unit = unit), + 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) } ) -private fun PopulatedWeather.toCurrentWeather(unit: String): CurrentWeather = +fun CurrentWithWeatherInfo.asCoreEntity(unit: String): CurrentWeather = CurrentWeather( - temperature = formatTemperatureValue(current.temp, unit), - feelsLike = formatTemperatureValue(current.feels_like, unit), - weather = listOf( - WeatherInfo( - id = current.id, - main = current.main, - description = current.description, - icon = current.icon - ) - ) + temperature = formatTemperatureValue(currentWeather.temp, unit), + feelsLike = formatTemperatureValue(currentWeather.feelsLike, unit), + weather = weather.map { it.asCoreModel() } ) -fun DailyWeatherEntity.asCoreModel(unit: String): DailyWeather = +fun DailyWithWeatherInfo.asCoreModel(unit: String): DailyWeather = DailyWeather( - forecastedTime = getDate(dt,"EEEE dd/M"), - temperature = temperature.asCoreModel(unit = unit), + forecastedTime = getDate(dailyWeatherEntity.dt,"EEEE dd/M"), + temperature = dailyWeatherEntity.temperature.asCoreModel(unit = unit), weather = weather.map { it.asCoreModel() } ) -fun HourlyWeatherEntity.asCoreModel(unit: String): HourlyWeather = +fun HourlyWithWeatherInfo.asCoreModel(unit: String): HourlyWeather = HourlyWeather( - forecastedTime = getDate(dt,"HH:SS"), - temperature = formatTemperatureValue(temperature, unit), + forecastedTime = getDate(hourlyWeatherEntity.dt,"HH:SS"), + temperature = formatTemperatureValue(hourlyWeatherEntity.temperature, unit), weather = weather.map { it.asCoreModel() } ) @@ -110,10 +119,6 @@ fun TemperatureEntity.asCoreModel(unit: String): Temperature = max = formatTemperatureValue(max, unit) ) -fun WeatherInfoResponseEntity.toWeatherInfo(): WeatherInfo { - return WeatherInfo(id, main, description, icon) -} - private fun formatTemperatureValue(temperature: Float, unit: String): String = "${temperature.roundToInt()}${getUnitSymbols(unit = unit)}" @@ -130,54 +135,102 @@ 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, - weather = hourlyResponse.weather.map { it.toWeatherInfoResponse() } ) } return hourlyWeatherEntities } + fun WeatherResponse.toDailyEntity():List { val dailyWeatherEntities = daily.map { dailyResponse -> DailyWeatherEntity( dt = dailyResponse.forecastedTime, temperature = dailyResponse.temperature.toTemperatureEntity(), - weather = dailyResponse.weather.map { it.toWeatherInfoResponse() } ) } return dailyWeatherEntities } -fun WeatherResponse.toCurrentWeatherEntity(): WeatherEntity { - val currentTime = System.currentTimeMillis() - val currentWeatherInfo = current.weather.first() - - return WeatherEntity( - dt = currentTime, - id = 0, - feels_like = current.feelsLike, - temp = current.temperature, - temp_max = current.temperature, - temp_min = current.temperature, - description = currentWeatherInfo.description, - icon = currentWeatherInfo.icon, - main = currentWeatherInfo.main, - lastRefreshed = currentTime, - isValid = true, - ) +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 } -private fun WeatherInfoResponse.toWeatherInfoResponse(): WeatherInfoResponseEntity { - return WeatherInfoResponseEntity( - id = id, - main = main, - description = description, - icon = icon +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( 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 From 09a8f5930cb128bfb69328659ccbddf95f305e0f Mon Sep 17 00:00:00 2001 From: Caleb Langat <95022986+Mzazi25@users.noreply.github.com> Date: Mon, 19 Jun 2023 08:30:00 +0300 Subject: [PATCH 34/42] refactored weather update scheduler --- .../odaridavid/weatherapp/worker/WeatherUpdateScheduler.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 index 86496b9..d0757d3 100644 --- a/app/src/main/java/com/github/odaridavid/weatherapp/worker/WeatherUpdateScheduler.kt +++ b/app/src/main/java/com/github/odaridavid/weatherapp/worker/WeatherUpdateScheduler.kt @@ -1,6 +1,5 @@ package com.github.odaridavid.weatherapp.worker -import android.content.Context import androidx.work.Constraints import androidx.work.NetworkType import androidx.work.PeriodicWorkRequestBuilder @@ -9,7 +8,7 @@ import java.util.concurrent.TimeUnit import javax.inject.Inject class WeatherUpdateScheduler @Inject constructor( - private val context: Context + private val workManager: WorkManager ) { private val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) @@ -22,6 +21,6 @@ class WeatherUpdateScheduler @Inject constructor( .setConstraints(constraints) .build() - WorkManager.getInstance(context).enqueue(refreshWeatherRequest) + workManager.enqueue(refreshWeatherRequest) } } From 06314f42ef721e7f287b68102e846c4541b7c7fc Mon Sep 17 00:00:00 2001 From: Caleb Langat <95022986+Mzazi25@users.noreply.github.com> Date: Mon, 19 Jun 2023 08:30:36 +0300 Subject: [PATCH 35/42] deleted converters and replaced with room relations --- .../weather/local/converters/Converters.kt | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 app/src/main/java/com/github/odaridavid/weatherapp/data/weather/local/converters/Converters.kt diff --git a/app/src/main/java/com/github/odaridavid/weatherapp/data/weather/local/converters/Converters.kt b/app/src/main/java/com/github/odaridavid/weatherapp/data/weather/local/converters/Converters.kt deleted file mode 100644 index f0ac9fb..0000000 --- a/app/src/main/java/com/github/odaridavid/weatherapp/data/weather/local/converters/Converters.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.github.odaridavid.weatherapp.data.weather.local.converters - -import androidx.room.ProvidedTypeConverter -import androidx.room.TypeConverter -import com.github.odaridavid.weatherapp.data.weather.local.entity.WeatherInfoResponseEntity -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json - -@ProvidedTypeConverter -class Converters { - - @TypeConverter - fun fromWeatherInfo(value: List?): String = Json.encodeToString(value) - - @TypeConverter - fun toWeatherInfo(value: String?) = value?.let { Json.decodeFromString>(it) } - -} From 8b77bb8f994694f9c2b3a3d793af69075b784a05 Mon Sep 17 00:00:00 2001 From: Caleb Langat <95022986+Mzazi25@users.noreply.github.com> Date: Mon, 19 Jun 2023 08:31:02 +0300 Subject: [PATCH 36/42] refactored usecase --- .../weatherapp/data/weather/DefaultRefreshWeatherUseCase.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 index a06229a..657bf0c 100644 --- 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 @@ -8,6 +8,7 @@ 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 @@ -15,7 +16,7 @@ import javax.inject.Inject class DefaultRefreshWeatherUseCase @Inject constructor( private val weatherRepository: WeatherRepository, private val notificationUtil: NotificationUtil, - private val context: Context + @ApplicationContext private val context: Context ) :RefreshWeatherUseCase { override operator fun invoke( defaultLocation: DefaultLocation, From e283bb221199f656a1c2c8b21b6b56073f4268b8 Mon Sep 17 00:00:00 2001 From: Caleb Langat <95022986+Mzazi25@users.noreply.github.com> Date: Mon, 19 Jun 2023 08:31:52 +0300 Subject: [PATCH 37/42] removed type converters from the local module --- .../github/odaridavid/weatherapp/di/LocalModule.kt | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) 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 index 471742c..a115e43 100644 --- a/app/src/main/java/com/github/odaridavid/weatherapp/di/LocalModule.kt +++ b/app/src/main/java/com/github/odaridavid/weatherapp/di/LocalModule.kt @@ -2,8 +2,8 @@ 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.converters.Converters import com.github.odaridavid.weatherapp.data.weather.local.dao.WeatherDao import com.github.odaridavid.weatherapp.util.NotificationUtil import com.github.odaridavid.weatherapp.worker.WeatherUpdateScheduler @@ -22,7 +22,6 @@ object LocalModule { fun provideWeatherDatabase(@ApplicationContext app: Context): WeatherDatabase { return Room .databaseBuilder(app, WeatherDatabase::class.java, "weather_database") - .addTypeConverter(Converters()) .build() } @@ -34,8 +33,14 @@ object LocalModule { @Singleton @Provides - fun provideWeatherUpdateScheduler(@ApplicationContext context: Context): WeatherUpdateScheduler { - return WeatherUpdateScheduler(context) + fun provideWeatherUpdateScheduler(workManager: WorkManager): WeatherUpdateScheduler { + return WeatherUpdateScheduler(workManager) + } + + @Singleton + @Provides + fun provideWorkManager(@ApplicationContext context: Context): WorkManager { + return WorkManager.getInstance(context) } @Provides From 12e09e03e41cbcadaa41bf36181b40ffa480ab59 Mon Sep 17 00:00:00 2001 From: Caleb Langat <95022986+Mzazi25@users.noreply.github.com> Date: Mon, 19 Jun 2023 08:32:31 +0300 Subject: [PATCH 38/42] refactored repository to read daily and hourly lists --- .../data/weather/DefaultWeatherRepository.kt | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) 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 2c5dd2b..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.R import com.github.odaridavid.weatherapp.core.Result import com.github.odaridavid.weatherapp.core.api.Logger import com.github.odaridavid.weatherapp.core.api.WeatherRepository @@ -13,8 +12,6 @@ import com.github.odaridavid.weatherapp.data.weather.local.dao.WeatherDao import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.flow -import java.io.IOException -import java.net.HttpURLConnection.HTTP_UNAUTHORIZED import javax.inject.Inject class DefaultWeatherRepository @Inject constructor( @@ -27,7 +24,10 @@ class DefaultWeatherRepository @Inject constructor( language: String, units: String ): Flow> = flow { - val cachedWeather = weatherDao.getWeather() + 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)) @@ -45,15 +45,17 @@ class DefaultWeatherRepository @Inject constructor( ) val response = apiResponse.body() if (apiResponse.isSuccessful && response != null) { - val currentWeather = apiResponse.body()!!.toCurrentWeatherEntity() - val hourlyEntity = apiResponse.body()!!.toHourlyEntity() - val dailyEntity = apiResponse.body()!!.toDailyEntity() + 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(hourlyEntity) - weatherDao.insertDailyWeather(dailyEntity) - val getWeather = weatherDao.getWeather() - val data = getWeather!!.toCoreEntity(unit = units) - emit(Result.Success(data =data)) + weatherDao.insertHourlyWeather(hourlyWeather) + weatherDao.insertDailyWeather(dailyWeather) + weatherDao.insertWeatherInfo(weatherInfo) + val result = weatherEntity.toCoreEntity(unit = units) + emit(Result.Success(data = result)) } else { val errorType = mapResponseCodeToErrorType(apiResponse.code()) emit(Result.Error(errorType = errorType)) From e32d492c186151101a1bc6099add1c23994b075d Mon Sep 17 00:00:00 2001 From: Caleb Langat <95022986+Mzazi25@users.noreply.github.com> Date: Mon, 19 Jun 2023 08:33:16 +0300 Subject: [PATCH 39/42] updated weather app with custom worker instead of hiltworker --- .../main/java/com/github/odaridavid/weatherapp/WeatherApp.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 79162b1..17a70a6 100644 --- a/app/src/main/java/com/github/odaridavid/weatherapp/WeatherApp.kt +++ b/app/src/main/java/com/github/odaridavid/weatherapp/WeatherApp.kt @@ -3,6 +3,7 @@ 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 @@ -10,7 +11,7 @@ import javax.inject.Inject @HiltAndroidApp class WeatherApp : Application(), Configuration.Provider{ @Inject - lateinit var workerFactory: HiltWorkerFactory + lateinit var workerFactory: CustomWorkerFactory @Inject lateinit var weatherUpdateScheduler: WeatherUpdateScheduler From 81f4d4b864f53a2bb28936088ba25f8babd6036d Mon Sep 17 00:00:00 2001 From: Caleb Langat <95022986+Mzazi25@users.noreply.github.com> Date: Mon, 19 Jun 2023 12:07:13 +0300 Subject: [PATCH 40/42] Maoppers update --- .../com/github/odaridavid/weatherapp/data/weather/Mappers.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 11c2944..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 @@ -111,7 +111,7 @@ fun WeatherInfoResponseEntity.asCoreModel(): WeatherInfo = id = id, main = main, description = description, - icon = "${BuildConfig.OPEN_WEATHER_ICONS_URL}$icon@2x.png" + icon = BuildConfig.OPEN_WEATHER_ICONS_URL ) fun TemperatureEntity.asCoreModel(unit: String): Temperature = Temperature( From c868b82b45ade28afccd9c23d6cbd2ba8144b08b Mon Sep 17 00:00:00 2001 From: Caleb Langat <95022986+Mzazi25@users.noreply.github.com> Date: Mon, 19 Jun 2023 12:07:42 +0300 Subject: [PATCH 41/42] Updated Fakes with the Populated Weather --- .../com/github/odaridavid/weatherapp/Fakes.kt | 66 +++++++++++-------- 1 file changed, 37 insertions(+), 29 deletions(-) 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 8d1c148..3886bbe 100644 --- a/app/src/test/java/com/github/odaridavid/weatherapp/Fakes.kt +++ b/app/src/test/java/com/github/odaridavid/weatherapp/Fakes.kt @@ -1,21 +1,14 @@ package com.github.odaridavid.weatherapp import com.github.odaridavid.weatherapp.core.model.CurrentWeather -import com.github.odaridavid.weatherapp.core.model.DailyWeather -import com.github.odaridavid.weatherapp.core.model.HourlyWeather -import com.github.odaridavid.weatherapp.core.model.Temperature 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.DailyWeatherResponse -import com.github.odaridavid.weatherapp.data.weather.HourlyWeatherResponse -import com.github.odaridavid.weatherapp.data.weather.TemperatureResponse 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.DailyWeatherEntity -import com.github.odaridavid.weatherapp.data.weather.local.entity.HourlyWeatherEntity +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.TemperatureEntity import com.github.odaridavid.weatherapp.data.weather.local.entity.WeatherEntity import com.github.odaridavid.weatherapp.data.weather.local.entity.WeatherInfoResponseEntity @@ -25,15 +18,17 @@ val fakeSuccessWeatherResponse = WeatherResponse( feelsLike = 2.0f, weather = listOf( WeatherInfoResponse( - id = 1, - main = "main", - description = "desc", - icon = "icon" - ) + id = 1, + main = "main", + description = "desc", + icon = "icon" + ) ) ), hourly = listOf(), - daily = listOf() + daily = listOf(), + lat = 0.00, + long = 0.00 ) val fakeSuccessMappedWeatherResponse = Weather( @@ -45,12 +40,14 @@ val fakeSuccessMappedWeatherResponse = Weather( id = 1, main = "main", description = "desc", - icon = "icon" + icon = BuildConfig.OPEN_WEATHER_ICONS_URL ) ) ), hourly = listOf(), - daily = listOf() + daily = listOf(), + lat = 0.00, + long = 0.00 ) val fakeSuccessResponse = Weather( current = CurrentWeather( @@ -61,25 +58,36 @@ val fakeSuccessResponse = Weather( id = 1, main = "main", description = "desc", - icon = "icon" + icon = BuildConfig.OPEN_WEATHER_ICONS_URL ) ) ), hourly = listOf(), - daily = listOf() + daily = listOf(), + lat = 0.00, + long = 0.00 ) val fakePopulatedResponse = PopulatedWeather( - current = WeatherEntity( - dt = 1L, - main = "main", - temp = 3.0F, - temp_max = 0.0F, - temp_min = 0.0F, - description ="desc", - icon = "icon", - id = 1, - feels_like = 2.0f, + 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() From 15c0363705162c8174d85b1f11549eb93d6346f2 Mon Sep 17 00:00:00 2001 From: Caleb Langat <95022986+Mzazi25@users.noreply.github.com> Date: Mon, 19 Jun 2023 12:08:10 +0300 Subject: [PATCH 42/42] Fixed failing tests --- .../odaridavid/weatherapp/HomeViewModelIntegrationTest.kt | 2 +- .../github/odaridavid/weatherapp/WeatherRepositoryUnitTest.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 e4faf0f..37a639d 100644 --- a/app/src/test/java/com/github/odaridavid/weatherapp/HomeViewModelIntegrationTest.kt +++ b/app/src/test/java/com/github/odaridavid/weatherapp/HomeViewModelIntegrationTest.kt @@ -58,7 +58,7 @@ class HomeViewModelIntegrationTest { } returns Response.success( fakeSuccessWeatherResponse ) - coEvery { mockWeatherDao.getWeather() } returns fakePopulatedResponse + coEvery { mockWeatherDao.getWeather(any(), any()) } returns fakePopulatedResponse val weatherRepository = DefaultWeatherRepository( 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 34b5b5b..a8115b3 100644 --- a/app/src/test/java/com/github/odaridavid/weatherapp/WeatherRepositoryUnitTest.kt +++ b/app/src/test/java/com/github/odaridavid/weatherapp/WeatherRepositoryUnitTest.kt @@ -56,7 +56,7 @@ class WeatherRepositoryUnitTest { } returns Response.success( fakeSuccessWeatherResponse ) - coEvery { mockWeatherDao.getWeather() } returns fakePopulatedResponse + coEvery { mockWeatherDao.getWeather(any(), any()) } returns fakePopulatedResponse val weatherRepository = createWeatherRepository()