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()