codeskraps vor 1 Jahr
Ursprung
Commit
a67febf658
42 geänderte Dateien mit 634 neuen und 280 gelöschten Zeilen
  1. 4 4
      app/build.gradle.kts
  2. BIN
      app/release/app-release.aab
  3. BIN
      app/release/app-release.apk
  4. BIN
      app/src/main/ic_launcher-playstore_feature.png
  5. 21 33
      app/src/main/java/com/example/weather/app/ui/MainActivity.kt
  6. 5 0
      feature/common/build.gradle.kts
  7. 22 0
      feature/common/src/main/java/com/trifork/feature/common/components/ObserveAsEvent.kt
  8. 2 2
      feature/common/src/main/java/com/trifork/feature/common/data/local/GeoLocationEntity.kt
  9. 1 1
      feature/common/src/main/java/com/trifork/feature/common/data/local/GeocodingDB.kt
  10. 22 0
      feature/common/src/main/java/com/trifork/feature/common/data/local/GeocodingDao.kt
  11. 26 0
      feature/common/src/main/java/com/trifork/feature/common/data/mappers/GeocodingMapper.kt
  12. 57 0
      feature/common/src/main/java/com/trifork/feature/common/data/repository/LocalGeocodingRepositoryImpl.kt
  13. 14 0
      feature/common/src/main/java/com/trifork/feature/common/di/FeatureModule.kt
  14. 18 0
      feature/common/src/main/java/com/trifork/feature/common/di/RepositoryModule.kt
  15. 2 2
      feature/common/src/main/java/com/trifork/feature/common/domain/model/GeoLocation.kt
  16. 8 0
      feature/common/src/main/java/com/trifork/feature/common/domain/model/LatLong.kt
  17. 15 0
      feature/common/src/main/java/com/trifork/feature/common/domain/repository/LocalGeocodingRepository.kt
  18. 15 0
      feature/common/src/main/java/com/trifork/feature/common/mvi/StateReducerFlow.kt
  19. 3 1
      feature/common/src/main/java/com/trifork/feature/common/navigation/Screen.kt
  20. 3 3
      feature/common/src/main/java/com/trifork/feature/common/util/Resouce.kt
  21. 1 6
      feature/geocoding/build.gradle.kts
  22. 0 19
      feature/geocoding/src/main/java/com/trifork/feature/geocoding/data/local/GeocodingDao.kt
  23. 1 25
      feature/geocoding/src/main/java/com/trifork/feature/geocoding/data/mappers/GeocodingMappers.kt
  24. 1 35
      feature/geocoding/src/main/java/com/trifork/feature/geocoding/data/repository/GeocodingRepositoryImpl.kt
  25. 0 13
      feature/geocoding/src/main/java/com/trifork/feature/geocoding/di/FeatureModule.kt
  26. 1 7
      feature/geocoding/src/main/java/com/trifork/feature/geocoding/domain/repository/GeocodingRepository.kt
  27. 32 40
      feature/geocoding/src/main/java/com/trifork/feature/geocoding/presentation/GeocodingViewModel.kt
  28. 19 22
      feature/geocoding/src/main/java/com/trifork/feature/geocoding/presentation/components/GeocodingScreen.kt
  29. 1 1
      feature/geocoding/src/main/java/com/trifork/feature/geocoding/presentation/mvi/GeoEvent.kt
  30. 2 1
      feature/geocoding/src/main/java/com/trifork/feature/geocoding/presentation/mvi/GeoState.kt
  31. 1 1
      feature/weather/build.gradle.kts
  32. 11 0
      feature/weather/src/main/java/com/trifork/feature/weather/data/mappers/WeatherMappers.kt
  33. 100 33
      feature/weather/src/main/java/com/trifork/feature/weather/presentation/WeatherViewModel.kt
  34. 81 0
      feature/weather/src/main/java/com/trifork/feature/weather/presentation/components/SaveLocationDialog.kt
  35. 1 1
      feature/weather/src/main/java/com/trifork/feature/weather/presentation/components/WeatherCard.kt
  36. 3 3
      feature/weather/src/main/java/com/trifork/feature/weather/presentation/components/WeatherForecast.kt
  37. 95 14
      feature/weather/src/main/java/com/trifork/feature/weather/presentation/components/WeatherScreen.kt
  38. 5 0
      feature/weather/src/main/java/com/trifork/feature/weather/presentation/mvi/WeatherAction.kt
  39. 8 4
      feature/weather/src/main/java/com/trifork/feature/weather/presentation/mvi/WeatherEvent.kt
  40. 4 2
      feature/weather/src/main/java/com/trifork/feature/weather/presentation/mvi/WeatherState.kt
  41. 24 6
      feature/weather/src/main/res/drawable/ic_light_drizzel_night.xml
  42. 5 1
      gradle/libs.versions.toml

+ 4 - 4
app/build.gradle.kts

@@ -13,8 +13,8 @@ android {
         applicationId = "com.arklan.weather"
         minSdk = 26
         targetSdk = 34
-        versionCode = 1
-        versionName = "1.0"
+        versionCode = 2
+        versionName = "1.1"
 
         testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
         vectorDrawables {
@@ -64,7 +64,7 @@ dependencies {
     implementation(libs.androidx.activity.compose)
     implementation(libs.androidx.navigation.compose)
     implementation(libs.androidx.lifecycle.runtime.compose)
-    implementation(libs.android.compose.material)
+    implementation(libs.android.compose.material3)
     implementation(platform("androidx.compose:compose-bom:2023.10.00"))
     implementation("androidx.compose.ui:ui")
     implementation("androidx.compose.ui:ui-graphics")
@@ -73,7 +73,7 @@ dependencies {
 
     //Dagger - Hilt
     implementation(libs.hilt.android)
-    implementation(libs.androidx.constraintlayout)
+    implementation(libs.hilt.navigation.compose)
     ksp(libs.hilt.android.compiler)
 
     testImplementation(libs.junit.junit)

BIN
app/release/app-release.aab


BIN
app/release/app-release.apk


BIN
app/src/main/ic_launcher-playstore_feature.png


+ 21 - 33
app/src/main/java/com/example/weather/app/ui/MainActivity.kt

@@ -3,7 +3,9 @@ package com.example.weather.app.ui
 import android.os.Bundle
 import androidx.activity.ComponentActivity
 import androidx.activity.compose.setContent
-import androidx.activity.viewModels
+import androidx.compose.runtime.getValue
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import androidx.navigation.NavType
 import androidx.navigation.compose.NavHost
 import androidx.navigation.compose.composable
@@ -13,11 +15,8 @@ import com.example.weather.app.ui.theme.WeatherTheme
 import com.trifork.feature.common.navigation.Screen
 import com.trifork.feature.geocoding.presentation.GeocodingViewModel
 import com.trifork.feature.geocoding.presentation.components.GeocodingScreen
-import com.trifork.feature.weather.domain.model.WeatherLocation
 import com.trifork.feature.weather.presentation.WeatherViewModel
 import com.trifork.feature.weather.presentation.components.WeatherScreen
-import com.trifork.feature.weather.presentation.mvi.WeatherEvent
-import com.trifork.feature.weather.presentation.mvi.WeatherState
 import dagger.hilt.android.AndroidEntryPoint
 
 @AndroidEntryPoint
@@ -35,42 +34,31 @@ class MainActivity : ComponentActivity() {
                         Screen.Weather.route,
                         arguments = listOf(
                             navArgument("name") { type = NavType.StringType },
-                            navArgument("lat") { type = NavType.FloatType },
-                            navArgument("long") { type = NavType.FloatType }
+                            navArgument("lat") { type = NavType.StringType },
+                            navArgument("long") { type = NavType.StringType }
                         )
-                    ) { backStackEntry ->
+                    ) {
 
-
-                        val geoLocation = backStackEntry.arguments?.let {
-                            val name = it.getString("name") ?: ""
-                            val lat = it.getFloat("lat").toDouble()
-                            val long = it.getFloat("long").toDouble()
-
-                            it.remove("name")
-                            it.remove("lat")
-                            it.remove("long")
-
-                            if (name.isNotBlank() && lat != .0 && long != .0) {
-                                WeatherLocation(
-                                    name = name,
-                                    lat = lat,
-                                    long = long
-                                )
-                            } else WeatherLocation()
-                        } ?: WeatherLocation()
-
-                        val viewModel by viewModels<WeatherViewModel>()
-
-                        viewModel.state.handleEvent(WeatherEvent.LoadWeatherInfo(geoLocation))
+                        val viewModel = hiltViewModel<WeatherViewModel>()
+                        val state by viewModel.state.collectAsStateWithLifecycle()
 
                         WeatherScreen(
-                            navController,
-                            viewModel
+                            navController = navController,
+                            state = state,
+                            handleEvent = viewModel.state::handleEvent,
+                            action = viewModel.action
                         )
                     }
                     composable(Screen.Geocoding.route) {
-                        val viewModel by viewModels<GeocodingViewModel>()
-                        GeocodingScreen(navController, viewModel)
+                        val viewModel = hiltViewModel<GeocodingViewModel>()
+                        val state by viewModel.state.collectAsStateWithLifecycle()
+
+                        GeocodingScreen(
+                            navController = navController,
+                            state = state,
+                            handleEvent = viewModel.state::handleEvent,
+                            action = viewModel.action
+                        )
                     }
                 }
             }

+ 5 - 0
feature/common/build.gradle.kts

@@ -61,6 +61,11 @@ dependencies {
     implementation(libs.androidx.constraintlayout)
     ksp(libs.hilt.android.compiler)
 
+    // Room
+    implementation(libs.room.runtime)
+    annotationProcessor(libs.room.compiler)
+    ksp(libs.room.compiler)
+
     testImplementation(libs.junit.junit)
     androidTestImplementation(libs.androidx.junit)
     androidTestImplementation(libs.espresso.core)

+ 22 - 0
feature/common/src/main/java/com/trifork/feature/common/components/ObserveAsEvent.kt

@@ -0,0 +1,22 @@
+package com.trifork.feature.common.components
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.ui.platform.LocalLifecycleOwner
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.withContext
+
+@Composable
+fun <T> ObserveAsEvents(flow: Flow<T>, onAction: (T) -> Unit) {
+    val lifecycleOwner = LocalLifecycleOwner.current
+    LaunchedEffect(flow, lifecycleOwner) {
+        lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+            withContext(Dispatchers.Main.immediate) {
+                flow.collect(onAction)
+            }
+        }
+    }
+}

+ 2 - 2
feature/geocoding/src/main/java/com/trifork/feature/geocoding/data/local/GeoLocationEntity.kt → feature/common/src/main/java/com/trifork/feature/common/data/local/GeoLocationEntity.kt

@@ -1,4 +1,4 @@
-package com.trifork.feature.geocoding.data.local
+package com.trifork.feature.common.data.local
 
 import androidx.room.Entity
 import androidx.room.PrimaryKey
@@ -12,4 +12,4 @@ data class GeoLocationEntity(
     val longitude: Double,
     val country: String,
     val admin1: String?
-)
+)

+ 1 - 1
feature/geocoding/src/main/java/com/trifork/feature/geocoding/data/local/GeocodingDB.kt → feature/common/src/main/java/com/trifork/feature/common/data/local/GeocodingDB.kt

@@ -1,4 +1,4 @@
-package com.trifork.feature.geocoding.data.local
+package com.trifork.feature.common.data.local
 
 import androidx.room.Database
 import androidx.room.RoomDatabase

+ 22 - 0
feature/common/src/main/java/com/trifork/feature/common/data/local/GeocodingDao.kt

@@ -0,0 +1,22 @@
+package com.trifork.feature.common.data.local
+
+import androidx.room.Dao
+import androidx.room.Delete
+import androidx.room.Query
+import androidx.room.Upsert
+
+@Dao
+interface GeocodingDao {
+
+    @Query("SELECT * FROM GeoLocationEntity")
+    fun getAll(): List<GeoLocationEntity>
+
+    @Upsert
+    fun insert(geoLocationEntity: GeoLocationEntity)
+
+    @Query("DELETE FROM GeoLocationEntity WHERE name = :name")
+    fun delete(name: String)
+
+    @Query("SELECT * FROM GeoLocationEntity WHERE latitude=:latitude AND longitude=:longitude")
+    fun getByLocation(latitude: Double, longitude: Double): List<GeoLocationEntity>
+}

+ 26 - 0
feature/common/src/main/java/com/trifork/feature/common/data/mappers/GeocodingMapper.kt

@@ -0,0 +1,26 @@
+package com.trifork.feature.common.data.mappers
+
+import com.trifork.feature.common.data.local.GeoLocationEntity
+import com.trifork.feature.common.domain.model.GeoLocation
+
+fun GeoLocationEntity.toGeocoding(): GeoLocation {
+    return GeoLocation(
+        name = name,
+        latitude = latitude,
+        longitude = longitude,
+        country = country,
+        admin1 = admin1,
+        cached = true
+    )
+}
+
+fun GeoLocation.toGeoLocationEntity(): GeoLocationEntity {
+    return GeoLocationEntity(
+        uid = kotlin.random.Random.nextInt(),
+        name = name,
+        latitude = latitude,
+        longitude = longitude,
+        country = country,
+        admin1 = admin1
+    )
+}

+ 57 - 0
feature/common/src/main/java/com/trifork/feature/common/data/repository/LocalGeocodingRepositoryImpl.kt

@@ -0,0 +1,57 @@
+package com.trifork.feature.common.data.repository
+
+import android.util.Log
+import com.trifork.feature.common.data.local.GeocodingDB
+import com.trifork.feature.common.data.mappers.toGeoLocationEntity
+import com.trifork.feature.common.data.mappers.toGeocoding
+import com.trifork.feature.common.domain.model.GeoLocation
+import com.trifork.feature.common.domain.repository.LocalGeocodingRepository
+import com.trifork.feature.common.util.Resource
+import javax.inject.Inject
+
+class LocalGeocodingRepositoryImpl @Inject constructor(
+    private val geocodingDB: GeocodingDB
+) : LocalGeocodingRepository {
+
+    override suspend fun getCachedGeoLocation(): Resource<List<GeoLocation>> {
+        return try {
+            Resource.Success(
+                data = geocodingDB.geocodingDao().getAll().map { it.toGeocoding() }
+            )
+        } catch (e: Exception) {
+            e.printStackTrace()
+            Resource.Error(e.message ?: "An unknown error occurred.")
+        }
+    }
+
+    override suspend fun saveCacheGeoLocation(geoLocation: GeoLocation): Resource<Unit> {
+        return try {
+            geocodingDB.geocodingDao().insert(geoLocation.toGeoLocationEntity())
+            Resource.Success(Unit)
+        } catch (e: Exception) {
+            e.printStackTrace()
+            Resource.Error(e.message ?: "An unknown error occurred.")
+        }
+    }
+
+    override suspend fun deleteCacheGeoLocation(geoLocation: GeoLocation): Resource<Unit> {
+        return try {
+            geocodingDB.geocodingDao().delete(geoLocation.name)
+            Resource.Success(Unit)
+        } catch (e: Exception) {
+            e.printStackTrace()
+            Resource.Error(e.message ?: "An unknown error occurred.")
+        }
+    }
+
+    override suspend fun isCached(latitude: Double, longitude: Double): Resource<Boolean> {
+        return try {
+            Resource.Success(
+                geocodingDB.geocodingDao().getByLocation(latitude, longitude).isNotEmpty()
+            )
+        } catch (e: Exception) {
+            e.printStackTrace()
+            Resource.Error(e.message ?: "An unknown error occurred.")
+        }
+    }
+}

+ 14 - 0
feature/common/src/main/java/com/trifork/feature/common/di/FeatureModule.kt

@@ -1,5 +1,8 @@
 package com.trifork.feature.common.di
 
+import android.app.Application
+import androidx.room.Room
+import com.trifork.feature.common.data.local.GeocodingDB
 import com.trifork.feature.common.dispatcher.DispatcherProvider
 import com.trifork.feature.common.dispatcher.StandardDispatcherProvider
 import dagger.Module
@@ -17,4 +20,15 @@ object FeatureModule {
     fun providesDispatcherProvider(): DispatcherProvider {
         return StandardDispatcherProvider()
     }
+
+    @Provides
+    @Singleton
+    fun providesGeocodingDB(
+        application: Application
+    ): GeocodingDB {
+        return Room.databaseBuilder(
+            application,
+            GeocodingDB::class.java, "database-name"
+        ).build()
+    }
 }

+ 18 - 0
feature/common/src/main/java/com/trifork/feature/common/di/RepositoryModule.kt

@@ -0,0 +1,18 @@
+package com.trifork.feature.common.di
+
+import com.trifork.feature.common.data.repository.LocalGeocodingRepositoryImpl
+import com.trifork.feature.common.domain.repository.LocalGeocodingRepository
+import dagger.Binds
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ViewModelComponent
+
+@Module
+@InstallIn(ViewModelComponent::class)
+abstract class RepositoryModule {
+
+    @Binds
+    abstract fun bindsGeocodingRepository(
+        localGeocodingRepositoryImpl: LocalGeocodingRepositoryImpl
+    ): LocalGeocodingRepository
+}

+ 2 - 2
feature/geocoding/src/main/java/com/trifork/feature/geocoding/domain/model/GeoLocation.kt → feature/common/src/main/java/com/trifork/feature/common/domain/model/GeoLocation.kt

@@ -1,4 +1,4 @@
-package com.trifork.feature.geocoding.domain.model
+package com.trifork.feature.common.domain.model
 
 data class GeoLocation(
     val name: String,
@@ -7,4 +7,4 @@ data class GeoLocation(
     val country: String,
     val admin1: String?,
     val cached: Boolean = false
-)
+)

+ 8 - 0
feature/common/src/main/java/com/trifork/feature/common/domain/model/LatLong.kt

@@ -0,0 +1,8 @@
+package com.trifork.feature.common.domain.model
+
+import java.io.Serializable
+
+data class LatLong(
+    var latitude: Double,
+    var longitude: Double,
+) : Serializable

+ 15 - 0
feature/common/src/main/java/com/trifork/feature/common/domain/repository/LocalGeocodingRepository.kt

@@ -0,0 +1,15 @@
+package com.trifork.feature.common.domain.repository
+
+import com.trifork.feature.common.domain.model.GeoLocation
+import com.trifork.feature.common.util.Resource
+
+interface LocalGeocodingRepository {
+
+    suspend fun getCachedGeoLocation(): Resource<List<GeoLocation>>
+
+    suspend fun saveCacheGeoLocation(geoLocation: GeoLocation): Resource<Unit>
+
+    suspend fun deleteCacheGeoLocation(geoLocation: GeoLocation): Resource<Unit>
+
+    suspend fun isCached(latitude: Double, longitude: Double): Resource<Boolean>
+}

+ 15 - 0
feature/common/src/main/java/com/trifork/feature/common/mvi/StateReducerFlow.kt

@@ -50,4 +50,19 @@ private class StateReducerFlowImpl<STATE, EVENT>(
             error("Missed event $event! You are doing something wrong during state transformation.")
         }
     }
+}
+
+abstract class StateReducerViewModel<STATE, EVENT, ACTION> : ViewModel() {
+
+    private val initState by lazy { initState() }
+    val state = StateReducerFlow(
+        initialState = initState,
+        reduceState = ::reduceState
+    )
+
+    protected val actionChannel = Channel<ACTION>()
+    val action = actionChannel.receiveAsFlow()
+
+    protected abstract fun initState(): STATE
+    protected abstract fun reduceState(currentState: STATE, event: EVENT): STATE
 }

+ 3 - 1
feature/common/src/main/java/com/trifork/feature/common/navigation/Screen.kt

@@ -3,7 +3,9 @@ package com.trifork.feature.common.navigation
 sealed class Screen(val route: String) {
     data object Weather : Screen("weather/{name}/{lat}/{long}") {
         fun createRoute(name: String, lat: Double, long: Double): String {
-            return "weather/$name/$lat/$long"
+            return "weather/$name/${lat.toBigDecimal().toPlainString()}/${
+                long.toBigDecimal().toPlainString()
+            }"
         }
     }
 

+ 3 - 3
feature/common/src/main/java/com/trifork/feature/common/util/Resouce.kt

@@ -1,6 +1,6 @@
 package com.trifork.feature.common.util
 
-sealed class Resource<T>(val data: T? = null, val message: String? = null) {
-    class Success<T>(data: T?): Resource<T>(data)
-    class Error<T>(message: String, data: T? = null): Resource<T>(data, message)
+sealed interface Resource<T> {
+    class Success<T>(val data: T) : Resource<T>
+    class Error<T>(val message: String) : Resource<T>
 }

+ 1 - 6
feature/geocoding/build.gradle.kts

@@ -58,7 +58,7 @@ dependencies {
     implementation(libs.androidx.activity.compose)
     implementation(libs.androidx.navigation.compose)
     implementation(libs.androidx.lifecycle.runtime.compose)
-    implementation(libs.android.compose.material)
+    implementation(libs.android.compose.material3)
     implementation(platform("androidx.compose:compose-bom:2023.10.00"))
     implementation("androidx.compose.ui:ui")
     implementation("androidx.compose.ui:ui-graphics")
@@ -74,11 +74,6 @@ dependencies {
     implementation(libs.retrofit.retrofit)
     implementation(libs.retrofit.converter.moshi)
 
-    // Room
-    implementation(libs.room.runtime)
-    annotationProcessor(libs.room.compiler)
-    ksp(libs.room.compiler)
-
     testImplementation(libs.junit.junit)
     androidTestImplementation(libs.androidx.junit)
     androidTestImplementation(libs.espresso.core)

+ 0 - 19
feature/geocoding/src/main/java/com/trifork/feature/geocoding/data/local/GeocodingDao.kt

@@ -1,19 +0,0 @@
-package com.trifork.feature.geocoding.data.local
-
-import androidx.room.Dao
-import androidx.room.Delete
-import androidx.room.Query
-import androidx.room.Upsert
-
-@Dao
-interface GeocodingDao {
-
-    @Query("SELECT * FROM GeoLocationEntity")
-    fun getAll(): List<GeoLocationEntity>
-
-    @Upsert
-    fun insert(geoLocationEntity: GeoLocationEntity)
-
-    @Delete
-    fun delete(geoLocationEntity: GeoLocationEntity)
-}

+ 1 - 25
feature/geocoding/src/main/java/com/trifork/feature/geocoding/data/mappers/GeocodingMappers.kt

@@ -1,9 +1,7 @@
 package com.trifork.feature.geocoding.data.mappers
 
-import com.trifork.feature.geocoding.data.local.GeoLocationEntity
+import com.trifork.feature.common.domain.model.GeoLocation
 import com.trifork.feature.geocoding.data.remote.GeocodingDto
-import com.trifork.feature.geocoding.domain.model.GeoLocation
-import kotlin.random.Random
 
 fun GeocodingDto.toGeocoding(): List<GeoLocation> {
     return results.map {
@@ -17,25 +15,3 @@ fun GeocodingDto.toGeocoding(): List<GeoLocation> {
         )
     }
 }
-
-fun GeoLocationEntity.toGeocoding(): GeoLocation {
-    return GeoLocation(
-        name = name,
-        latitude = latitude,
-        longitude = longitude,
-        country = country,
-        admin1 = admin1,
-        cached = true
-    )
-}
-
-fun GeoLocation.toGeoLocationEntity(): GeoLocationEntity {
-    return GeoLocationEntity(
-        uid = Random.nextInt(),
-        name = name,
-        latitude = latitude,
-        longitude = longitude,
-        country = country,
-        admin1 = admin1
-    )
-}

+ 1 - 35
feature/geocoding/src/main/java/com/trifork/feature/geocoding/data/repository/GeocodingRepositoryImpl.kt

@@ -1,17 +1,14 @@
 package com.trifork.feature.geocoding.data.repository
 
+import com.trifork.feature.common.domain.model.GeoLocation
 import com.trifork.feature.common.util.Resource
-import com.trifork.feature.geocoding.data.local.GeocodingDB
-import com.trifork.feature.geocoding.data.mappers.toGeoLocationEntity
 import com.trifork.feature.geocoding.data.mappers.toGeocoding
 import com.trifork.feature.geocoding.data.remote.GeocodingApi
-import com.trifork.feature.geocoding.domain.model.GeoLocation
 import com.trifork.feature.geocoding.domain.repository.GeocodingRepository
 import javax.inject.Inject
 
 class GeocodingRepositoryImpl @Inject constructor(
     private val api: GeocodingApi,
-    private val db: GeocodingDB
 ) : GeocodingRepository {
 
     override suspend fun getGeoLocation(query: String): Resource<List<GeoLocation>> {
@@ -30,35 +27,4 @@ class GeocodingRepositoryImpl @Inject constructor(
             Resource.Error(e.message ?: "An unknown error occurred.")
         }
     }
-
-    override suspend fun getCachedGeoLocation(): Resource<List<GeoLocation>> {
-        return try {
-            Resource.Success(
-                data = db.geocodingDao().getAll().map { it.toGeocoding() }
-            )
-        } catch (e: Exception) {
-            e.printStackTrace()
-            Resource.Error(e.message ?: "An unknown error occurred.")
-        }
-    }
-
-    override suspend fun saveCacheGeoLocation(geoLocation: GeoLocation): Resource<Unit> {
-        return try {
-            db.geocodingDao().insert(geoLocation.toGeoLocationEntity())
-            Resource.Success(Unit)
-        } catch (e: Exception) {
-            e.printStackTrace()
-            Resource.Error(e.message ?: "An unknown error occurred.")
-        }
-    }
-
-    override suspend fun deleteCacheGeoLocation(geoLocation: GeoLocation): Resource<Unit> {
-        return try {
-            db.geocodingDao().delete(geoLocation.toGeoLocationEntity())
-            Resource.Success(Unit)
-        } catch (e: Exception) {
-            e.printStackTrace()
-            Resource.Error(e.message ?: "An unknown error occurred.")
-        }
-    }
 }

+ 0 - 13
feature/geocoding/src/main/java/com/trifork/feature/geocoding/di/FeatureModule.kt

@@ -1,8 +1,5 @@
 package com.trifork.feature.geocoding.di
 
-import android.app.Application
-import androidx.room.Room
-import com.trifork.feature.geocoding.data.local.GeocodingDB
 import com.trifork.feature.geocoding.data.remote.GeocodingApi
 import dagger.Module
 import dagger.Provides
@@ -24,14 +21,4 @@ object FeatureModule {
             .build()
             .create()
     }
-
-    @Provides
-    fun providesGeocodingDB(
-        application: Application
-    ): GeocodingDB {
-        return Room.databaseBuilder(
-            application,
-            GeocodingDB::class.java, "database-name"
-        ).build()
-    }
 }

+ 1 - 7
feature/geocoding/src/main/java/com/trifork/feature/geocoding/domain/repository/GeocodingRepository.kt

@@ -1,15 +1,9 @@
 package com.trifork.feature.geocoding.domain.repository
 
+import com.trifork.feature.common.domain.model.GeoLocation
 import com.trifork.feature.common.util.Resource
-import com.trifork.feature.geocoding.domain.model.GeoLocation
 
 interface GeocodingRepository {
 
     suspend fun getGeoLocation(query: String): Resource<List<GeoLocation>>
-
-    suspend fun getCachedGeoLocation(): Resource<List<GeoLocation>>
-
-    suspend fun saveCacheGeoLocation(geoLocation: GeoLocation): Resource<Unit>
-
-    suspend fun deleteCacheGeoLocation(geoLocation: GeoLocation): Resource<Unit>
 }

+ 32 - 40
feature/geocoding/src/main/java/com/trifork/feature/geocoding/presentation/GeocodingViewModel.kt

@@ -1,54 +1,48 @@
 package com.trifork.feature.geocoding.presentation
 
-import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
 import com.trifork.feature.common.dispatcher.DispatcherProvider
-import com.trifork.feature.common.mvi.StateReducerFlow
+import com.trifork.feature.common.domain.model.GeoLocation
+import com.trifork.feature.common.domain.repository.LocalGeocodingRepository
+import com.trifork.feature.common.mvi.StateReducerViewModel
 import com.trifork.feature.common.util.Resource
-import com.trifork.feature.geocoding.domain.model.GeoLocation
 import com.trifork.feature.geocoding.domain.repository.GeocodingRepository
 import com.trifork.feature.geocoding.presentation.mvi.GeoAction
 import com.trifork.feature.geocoding.presentation.mvi.GeoEvent
 import com.trifork.feature.geocoding.presentation.mvi.GeoState
 import dagger.hilt.android.lifecycle.HiltViewModel
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.flow.receiveAsFlow
 import kotlinx.coroutines.launch
 import javax.inject.Inject
 
 @HiltViewModel
 class GeocodingViewModel @Inject constructor(
+    private val localGeocodingRepository: LocalGeocodingRepository,
     private val geocodingRepository: GeocodingRepository,
     private val dispatcherProvider: DispatcherProvider
-) : ViewModel() {
+) : StateReducerViewModel<GeoState, GeoEvent, GeoAction>() {
 
-    val state = StateReducerFlow(
-        initialState = GeoState.initial,
-        reduceState = ::reduceState,
-    )
-    private val _action = Channel<GeoAction>()
-    val action = _action.receiveAsFlow()
+    override fun initState(): GeoState = GeoState.initial
 
-    private fun reduceState(
+    override fun reduceState(
         currentState: GeoState,
         event: GeoEvent
     ): GeoState {
         return when (event) {
-            is GeoEvent.Search -> searchGeoLocation(currentState, event.query)
-            is GeoEvent.Delete -> deleteGeoLocation(currentState, event.geoLocation)
-            is GeoEvent.Save -> saveGeoLocation(currentState, event.geoLocation)
-            is GeoEvent.Loaded -> geoLocationsLoaded(currentState, event.geoLocations)
-            is GeoEvent.Error -> handleError(currentState, event.message)
-            is GeoEvent.LoadCache -> loadCache(currentState)
+            is GeoEvent.Search -> onSearchGeoLocation(currentState, event.query)
+            is GeoEvent.Delete -> onDeleteGeoLocation(currentState, event.geoLocation)
+            is GeoEvent.Save -> onSaveGeoLocation(currentState, event.geoLocation)
+            is GeoEvent.Loaded -> onGeoLocationsLoaded(currentState, event.geoLocations)
+            is GeoEvent.Error -> onHandleError(currentState, event.message)
+            is GeoEvent.LoadCache -> onLoadCache(currentState)
         }
     }
 
-    private fun loadCache(currentState: GeoState): GeoState {
+    private fun onLoadCache(currentState: GeoState): GeoState {
         viewModelScope.launch(dispatcherProvider.io) {
-            when (val result = geocodingRepository.getCachedGeoLocation()) {
+            when (val result = localGeocodingRepository.getCachedGeoLocation()) {
                 is Resource.Success -> {
-                    if ((result.data?.size ?: 0) > 0) {
-                        val cachedGeoLocations = result.data!!.sortedBy { it.name }
+                    if (result.data.isNotEmpty()) {
+                        val cachedGeoLocations = result.data.sortedBy { it.name }
                         state.handleEvent(GeoEvent.Loaded(cachedGeoLocations))
 
                     } else {
@@ -58,7 +52,7 @@ class GeocodingViewModel @Inject constructor(
 
                 is Resource.Error -> {
                     state.handleEvent(GeoEvent.Error("No results"))
-                    _action.send(GeoAction.ShowToast("Issue loading cache!!!"))
+                    actionChannel.send(GeoAction.ShowToast("Issue loading cache!!!"))
                 }
             }
         }
@@ -70,11 +64,9 @@ class GeocodingViewModel @Inject constructor(
     }
 
     private suspend fun loadCache(): List<GeoLocation> {
-        return when (val result = geocodingRepository.getCachedGeoLocation()) {
+        return when (val result = localGeocodingRepository.getCachedGeoLocation()) {
             is Resource.Success -> {
-                if ((result.data?.size ?: 0) > 0) {
-                    result.data!!
-                } else {
+                result.data.ifEmpty {
                     emptyList()
                 }
             }
@@ -83,7 +75,7 @@ class GeocodingViewModel @Inject constructor(
         }
     }
 
-    private fun geoLocationsLoaded(
+    private fun onGeoLocationsLoaded(
         currentState: GeoState,
         geoLocations: List<GeoLocation>
     ): GeoState {
@@ -94,12 +86,12 @@ class GeocodingViewModel @Inject constructor(
         )
     }
 
-    private fun searchGeoLocation(currentState: GeoState, query: String): GeoState {
+    private fun onSearchGeoLocation(currentState: GeoState, query: String): GeoState {
         viewModelScope.launch(dispatcherProvider.io) {
             when (val result = geocodingRepository.getGeoLocation(query)) {
                 is Resource.Success -> {
-                    if ((result.data?.size ?: 0) > 0) {
-                        state.handleEvent(GeoEvent.Loaded(result.data!!.map { mapped ->
+                    if (result.data.isNotEmpty()) {
+                        state.handleEvent(GeoEvent.Loaded(result.data.map { mapped ->
                             val cachedGeoLocations = loadCache()
                             val found = cachedGeoLocations.firstOrNull {
                                 it.longitude == mapped.longitude && it.latitude == mapped.latitude
@@ -112,7 +104,7 @@ class GeocodingViewModel @Inject constructor(
                 }
 
                 is Resource.Error -> {
-                    state.handleEvent(GeoEvent.Error(result.message!!))
+                    state.handleEvent(GeoEvent.Error(result.message))
                 }
             }
         }
@@ -123,10 +115,10 @@ class GeocodingViewModel @Inject constructor(
         )
     }
 
-    private fun saveGeoLocation(currentState: GeoState, geoLocation: GeoLocation): GeoState {
+    private fun onSaveGeoLocation(currentState: GeoState, geoLocation: GeoLocation): GeoState {
         viewModelScope.launch(dispatcherProvider.io) {
             val geoLocations = state.value.geoLocations
-            when (geocodingRepository.saveCacheGeoLocation(geoLocation)) {
+            when (localGeocodingRepository.saveCacheGeoLocation(geoLocation)) {
                 is Resource.Success -> {
                     state.handleEvent(GeoEvent.Loaded(geoLocations.map {
                         if (geoLocation.latitude == it.latitude && geoLocation.longitude == it.longitude) {
@@ -136,30 +128,30 @@ class GeocodingViewModel @Inject constructor(
                 }
 
                 is Resource.Error -> {
-                    _action.send(GeoAction.ShowToast("Issue saving!!!"))
+                    actionChannel.send(GeoAction.ShowToast("Issue saving!!!"))
                 }
             }
         }
         return currentState
     }
 
-    private fun deleteGeoLocation(currentState: GeoState, geoLocation: GeoLocation): GeoState {
+    private fun onDeleteGeoLocation(currentState: GeoState, geoLocation: GeoLocation): GeoState {
         viewModelScope.launch(dispatcherProvider.io) {
-            when (geocodingRepository.deleteCacheGeoLocation(geoLocation)) {
+            when (localGeocodingRepository.deleteCacheGeoLocation(geoLocation)) {
                 is Resource.Success -> {
                     val geoLocations = state.value.geoLocations
                     state.handleEvent(GeoEvent.Loaded(geoLocations.minus(geoLocation)))
                 }
 
                 is Resource.Error -> {
-                    _action.send(GeoAction.ShowToast("Issue deleting!!!"))
+                    actionChannel.send(GeoAction.ShowToast("Issue deleting!!!"))
                 }
             }
         }
         return currentState
     }
 
-    private fun handleError(currentState: GeoState, message: String): GeoState {
+    private fun onHandleError(currentState: GeoState, message: String): GeoState {
         return currentState.copy(
             isLoading = false,
             error = message,

+ 19 - 22
feature/geocoding/src/main/java/com/trifork/feature/geocoding/presentation/components/GeocodingScreen.kt

@@ -30,8 +30,6 @@ import androidx.compose.material3.Text
 import androidx.compose.material3.TopAppBar
 import androidx.compose.material3.TopAppBarDefaults
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
@@ -50,39 +48,38 @@ import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.dp
 import androidx.lifecycle.compose.LifecycleResumeEffect
 import androidx.navigation.NavController
+import com.trifork.feature.common.components.ObserveAsEvents
 import com.trifork.feature.common.navigation.Screen
-import com.trifork.feature.geocoding.presentation.GeocodingViewModel
 import com.trifork.feature.geocoding.presentation.mvi.GeoAction
 import com.trifork.feature.geocoding.presentation.mvi.GeoEvent
+import com.trifork.feature.geocoding.presentation.mvi.GeoState
+import kotlinx.coroutines.flow.Flow
 
 @OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
 @Composable
 fun GeocodingScreen(
     navController: NavController,
-    viewModel: GeocodingViewModel
+    state: GeoState,
+    handleEvent: (GeoEvent) -> Unit,
+    action: Flow<GeoAction>
 ) {
     val context = LocalContext.current
-    val state by viewModel.state.collectAsState()
 
     LifecycleResumeEffect {
-        viewModel.state.handleEvent(GeoEvent.LoadCache)
+        handleEvent(GeoEvent.LoadCache)
         onPauseOrDispose { }
     }
 
-    LaunchedEffect(viewModel.action) {
-        viewModel
-            .action
-            .collect { geoAction ->
-                when (geoAction) {
-                    is GeoAction.ShowToast -> {
-                        Toast.makeText(
-                            context,
-                            geoAction.message,
-                            Toast.LENGTH_SHORT,
-                        ).show()
-                    }
-                }
+    ObserveAsEvents(flow = action){onAction->
+        when(onAction){
+            is GeoAction.ShowToast -> {
+                Toast.makeText(
+                    context,
+                    onAction.message,
+                    Toast.LENGTH_SHORT,
+                ).show()
             }
+        }
     }
 
     Scaffold(
@@ -130,7 +127,7 @@ fun GeocodingScreen(
                         onSearch = {
                             keyboardController?.hide()
                             focusManager.clearFocus()
-                            viewModel.state.handleEvent(GeoEvent.Search(text))
+                            handleEvent(GeoEvent.Search(text))
                         }
                     )
                 )
@@ -142,7 +139,7 @@ fun GeocodingScreen(
                         )
                     } else if (state.error != null) {
                         Text(
-                            text = state.error ?: "",
+                            text = state.error,
                             color = MaterialTheme.colorScheme.error,
                             textAlign = TextAlign.Center,
                             modifier = Modifier.align(Alignment.Center)
@@ -192,7 +189,7 @@ fun GeocodingScreen(
                                             .align(CenterVertically)
                                             .padding(end = 16.dp)
                                             .clickable {
-                                                viewModel.state.handleEvent(
+                                                handleEvent(
                                                     if (geoLocation.cached) {
                                                         GeoEvent.Delete(geoLocation)
                                                     } else {

+ 1 - 1
feature/geocoding/src/main/java/com/trifork/feature/geocoding/presentation/mvi/GeoEvent.kt

@@ -1,6 +1,6 @@
 package com.trifork.feature.geocoding.presentation.mvi
 
-import com.trifork.feature.geocoding.domain.model.GeoLocation
+import com.trifork.feature.common.domain.model.GeoLocation
 
 sealed interface GeoEvent {
     data class Search(val query: String) : GeoEvent

+ 2 - 1
feature/geocoding/src/main/java/com/trifork/feature/geocoding/presentation/mvi/GeoState.kt

@@ -1,6 +1,7 @@
 package com.trifork.feature.geocoding.presentation.mvi
 
-import com.trifork.feature.geocoding.domain.model.GeoLocation
+import com.trifork.feature.common.domain.model.GeoLocation
+
 
 data class GeoState(
     val geoLocations: List<GeoLocation>,

+ 1 - 1
feature/weather/build.gradle.kts

@@ -59,11 +59,11 @@ dependencies {
     implementation(libs.androidx.navigation.compose)
     implementation(libs.androidx.lifecycle.runtime.compose)
     implementation(libs.android.compose.material)
+    implementation(libs.android.compose.material3)
     implementation(platform("androidx.compose:compose-bom:2023.10.00"))
     implementation("androidx.compose.ui:ui")
     implementation("androidx.compose.ui:ui-graphics")
     implementation("androidx.compose.ui:ui-tooling-preview")
-    implementation("androidx.compose.material3:material3")
 
     implementation(libs.kotlinx.collections.immutable)
 

+ 11 - 0
feature/weather/src/main/java/com/trifork/feature/weather/data/mappers/WeatherMappers.kt

@@ -1,5 +1,6 @@
 package com.trifork.feature.weather.data.mappers
 
+import com.trifork.feature.common.domain.model.GeoLocation
 import com.trifork.feature.weather.data.remote.SunDataDto
 import com.trifork.feature.weather.data.remote.WeatherDataDto
 import com.trifork.feature.weather.data.remote.WeatherDto
@@ -80,4 +81,14 @@ fun WeatherDto.toWeatherInfo(): WeatherInfo {
         weatherDataPerDay = weatherDataMap,
         currentWeatherData = currentWeatherData,
     )
+}
+
+fun WeatherInfo.toGeoLocation(): GeoLocation {
+    return GeoLocation(
+        name = geoLocation,
+        latitude = latitude,
+        longitude = longitude,
+        country = "",
+        admin1 = ""
+    )
 }

+ 100 - 33
feature/weather/src/main/java/com/trifork/feature/weather/presentation/WeatherViewModel.kt

@@ -1,14 +1,18 @@
 package com.trifork.feature.weather.presentation
 
-import androidx.lifecycle.ViewModel
+import android.util.Log
+import androidx.lifecycle.SavedStateHandle
 import androidx.lifecycle.viewModelScope
 import com.trifork.feature.common.dispatcher.DispatcherProvider
-import com.trifork.feature.common.mvi.StateReducerFlow
+import com.trifork.feature.common.domain.repository.LocalGeocodingRepository
+import com.trifork.feature.common.mvi.StateReducerViewModel
 import com.trifork.feature.common.util.Resource
+import com.trifork.feature.weather.data.mappers.toGeoLocation
 import com.trifork.feature.weather.domain.location.LocationTracker
 import com.trifork.feature.weather.domain.model.WeatherInfo
 import com.trifork.feature.weather.domain.model.WeatherLocation
 import com.trifork.feature.weather.domain.repository.WeatherRepository
+import com.trifork.feature.weather.presentation.mvi.WeatherAction
 import com.trifork.feature.weather.presentation.mvi.WeatherEvent
 import com.trifork.feature.weather.presentation.mvi.WeatherState
 import dagger.hilt.android.lifecycle.HiltViewModel
@@ -17,34 +21,40 @@ import javax.inject.Inject
 
 @HiltViewModel
 class WeatherViewModel @Inject constructor(
+    private val localGeocodingRepository: LocalGeocodingRepository,
     private val weatherRepository: WeatherRepository,
     private val locationTracker: LocationTracker,
-    private val dispatcherProvider: DispatcherProvider
-) : ViewModel() {
+    private val dispatcherProvider: DispatcherProvider,
+    private val savedStateHandle: SavedStateHandle
+) : StateReducerViewModel<WeatherState, WeatherEvent, WeatherAction>() {
 
-    val state = StateReducerFlow(
-        initialState = WeatherState.initial,
-        reduceState = ::reduceState,
-    )
+    override fun initState(): WeatherState = WeatherState.initial
 
-    private fun reduceState(
+    override fun reduceState(
         currentState: WeatherState,
         event: WeatherEvent
     ): WeatherState {
+        Log.v("WeatherViewModel", "event: $event, state: ${state.value.weatherInfo?.geoLocation}")
         return when (event) {
-            is WeatherEvent.LoadWeatherInfo -> loadWeatherInfo(currentState, event.geoLocation)
-            is WeatherEvent.UpdateHourlyInfo -> updateHourlyInfo(currentState, event.weatherInfo)
+            is WeatherEvent.LoadWeatherInfo -> onLoadWeatherInfo(currentState, event.geoLocation)
+            is WeatherEvent.UpdateHourlyInfo -> onUpdateHourlyInfo(currentState, event.weatherInfo)
             is WeatherEvent.Refresh -> onRefresh(currentState)
             is WeatherEvent.Error -> handleError(currentState, event.message)
+            is WeatherEvent.About -> currentState
+            is WeatherEvent.CheckCache -> onCheckCached(currentState, event.weatherInfo)
+            is WeatherEvent.IsCache -> onIsCached(currentState, event.isCached)
+            is WeatherEvent.Resume -> onResume(currentState)
+            is WeatherEvent.Save -> onSave(currentState, event.weatherInfo)
+            is WeatherEvent.Delete -> onDelete(currentState, event.weatherInfo)
         }
     }
 
-    private fun loadWeatherInfo(
+    private fun onLoadWeatherInfo(
         currentState: WeatherState,
         geoLocation: WeatherLocation
     ): WeatherState {
         viewModelScope.launch(dispatcherProvider.io) {
-            val location = if (geoLocation.lat == 0.0 || geoLocation.long == 0.0) {
+            val location = if (geoLocation.lat == .0 || geoLocation.long == .0) {
                 locationTracker.getCurrentLocation()?.let {
                     WeatherLocation(
                         "Current Location",
@@ -60,15 +70,14 @@ class WeatherViewModel @Inject constructor(
                 when (val result =
                     weatherRepository.getWeatherData(intLocation.lat, intLocation.long)) {
                     is Resource.Success -> {
-                        state.handleEvent(
-                            WeatherEvent.UpdateHourlyInfo(
-                                result.data!!.copy(
-                                    geoLocation = intLocation.name,
-                                    latitude = intLocation.lat,
-                                    longitude = intLocation.long
-                                )
-                            )
+                        val weatherInfo = result.data.copy(
+                            geoLocation = intLocation.name,
+                            latitude = intLocation.lat,
+                            longitude = intLocation.long
                         )
+
+                        state.handleEvent(WeatherEvent.UpdateHourlyInfo(weatherInfo))
+                        state.handleEvent(WeatherEvent.CheckCache(weatherInfo))
                     }
 
                     is Resource.Error -> {
@@ -86,7 +95,7 @@ class WeatherViewModel @Inject constructor(
         )
     }
 
-    private fun updateHourlyInfo(
+    private fun onUpdateHourlyInfo(
         currentState: WeatherState,
         weatherInfo: WeatherInfo
     ): WeatherState {
@@ -103,32 +112,90 @@ class WeatherViewModel @Inject constructor(
                 when (val result =
                     weatherRepository.getWeatherData(intLocation.latitude, intLocation.longitude)) {
                     is Resource.Success -> {
-                        state.handleEvent(
-                            WeatherEvent.UpdateHourlyInfo(
-                                result.data!!.copy(
-                                    geoLocation = intLocation.geoLocation,
-                                    latitude = intLocation.latitude,
-                                    longitude = intLocation.longitude
-                                )
-                            )
+                        val weatherInfo = result.data.copy(
+                            geoLocation = intLocation.geoLocation,
+                            latitude = intLocation.latitude,
+                            longitude = intLocation.longitude
                         )
+
+                        state.handleEvent(WeatherEvent.UpdateHourlyInfo(weatherInfo))
+                        state.handleEvent(WeatherEvent.CheckCache(weatherInfo))
                     }
 
                     is Resource.Error -> {
                         state.handleEvent(WeatherEvent.Error("Check Internet"))
                     }
                 }
-            } ?: kotlin.run {
-                state.handleEvent(WeatherEvent.Error("Check GPS"))
             }
         }
         return currentState.copy(
-            isLoading = false,
+            isLoading = true,
             error = null,
             weatherInfo = currentState.weatherInfo
         )
     }
 
+    private fun onCheckCached(currentState: WeatherState, weatherInfo: WeatherInfo): WeatherState {
+        viewModelScope.launch(dispatcherProvider.io) {
+            val result =
+                localGeocodingRepository.isCached(weatherInfo.latitude, weatherInfo.longitude)
+            when (result) {
+                is Resource.Error -> state.handleEvent(WeatherEvent.IsCache(false))
+                is Resource.Success -> state.handleEvent(WeatherEvent.IsCache(result.data))
+            }
+        }
+        return currentState
+    }
+
+    private fun onIsCached(currentState: WeatherState, isCached: Boolean): WeatherState {
+        return currentState.copy(cached = isCached)
+    }
+
+    private fun onResume(currentState: WeatherState): WeatherState {
+        try {
+            val name: String = savedStateHandle.get<String>("name") ?: "Current Location"
+            val lat: String = savedStateHandle.get<String>("lat") ?: ".0"
+            val long: String = savedStateHandle.get<String>("long") ?: ".0"
+
+            val weatherLocation = WeatherLocation(
+                name = name,
+                lat = lat.toDouble(),
+                long = long.toDouble()
+            )
+
+            state.handleEvent(WeatherEvent.LoadWeatherInfo(weatherLocation))
+        } catch (e: Exception) {
+            state.handleEvent(WeatherEvent.LoadWeatherInfo(WeatherLocation()))
+        }
+        return currentState
+    }
+
+    private fun onSave(currentState: WeatherState, weatherInfo: WeatherInfo): WeatherState {
+        viewModelScope.launch(dispatcherProvider.io) {
+            if (weatherInfo.geoLocation.isBlank()) {
+                actionChannel.send(WeatherAction.Toast("Location can't be blank !!!"))
+            } else {
+                when (val result =
+                    localGeocodingRepository.saveCacheGeoLocation(weatherInfo.toGeoLocation())) {
+                    is Resource.Success -> state.handleEvent(WeatherEvent.CheckCache(weatherInfo))
+                    is Resource.Error -> actionChannel.send(WeatherAction.Toast(result.message))
+                }
+            }
+        }
+        return currentState
+    }
+
+    private fun onDelete(currentState: WeatherState, weatherInfo: WeatherInfo): WeatherState {
+        viewModelScope.launch(dispatcherProvider.io) {
+            when (val result =
+                localGeocodingRepository.deleteCacheGeoLocation(weatherInfo.toGeoLocation())) {
+                is Resource.Success -> state.handleEvent(WeatherEvent.CheckCache(weatherInfo))
+                is Resource.Error -> actionChannel.send(WeatherAction.Toast(result.message))
+            }
+        }
+        return currentState
+    }
+
     private fun handleError(currentState: WeatherState, message: String): WeatherState {
         return currentState.copy(
             isLoading = false,

+ 81 - 0
feature/weather/src/main/java/com/trifork/feature/weather/presentation/components/SaveLocationDialog.kt

@@ -0,0 +1,81 @@
+package com.trifork.feature.weather.presentation.components
+
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import com.trifork.feature.weather.domain.model.WeatherInfo
+import com.trifork.feature.weather.presentation.mvi.WeatherEvent
+
+@Composable
+fun SaveLocationDialog(
+    weatherInfo: WeatherInfo,
+    showDialog: Boolean,
+    handleEvent: (WeatherEvent) -> Unit,
+    onDismissRequest: () -> Unit
+) {
+    if (showDialog) {
+        if (weatherInfo.geoLocation == "Current Location") {
+            var nameLocation by remember {
+                mutableStateOf(weatherInfo.geoLocation)
+            }
+
+            AlertDialog(
+                onDismissRequest = onDismissRequest,
+                containerColor = MaterialTheme.colorScheme.primaryContainer,
+                title = {
+                    Text(text = "Save Current Location")
+                },
+                text = {
+                    Column {
+                        Text("Set a name for the current location:")
+                        Spacer(modifier = Modifier.height(10.dp))
+                        OutlinedTextField(
+                            value = nameLocation,
+                            onValueChange = { nameLocation = it.trim() })
+                    }
+                },
+                confirmButton = {
+                    Button(
+                        colors = ButtonDefaults.buttonColors(
+                            containerColor = MaterialTheme.colorScheme.primary
+                        ),
+                        onClick = {
+                            handleEvent(WeatherEvent.Save(weatherInfo.copy(geoLocation = nameLocation)))
+                            onDismissRequest()
+                        }
+                    ) {
+                        Text("Save")
+                    }
+                },
+                dismissButton = {
+                    Button(
+                        colors = ButtonDefaults.buttonColors(
+                            containerColor = MaterialTheme.colorScheme.background,
+                            contentColor = MaterialTheme.colorScheme.onBackground
+                        ),
+                        onClick = onDismissRequest
+                    ) {
+                        Text("Cancel")
+                    }
+                }
+            )
+        } else {
+            handleEvent(WeatherEvent.Save(weatherInfo))
+        }
+    }
+}

+ 1 - 1
feature/weather/src/main/java/com/trifork/feature/weather/presentation/components/WeatherCard.kt

@@ -57,7 +57,7 @@ fun WeatherCardPreview() {
 
 @Composable
 fun WeatherCard(
-    data: com.trifork.feature.weather.domain.model.WeatherData,
+    data: WeatherData,
     backgroundColor: Color,
     modifier: Modifier = Modifier
 ) {

+ 3 - 3
feature/weather/src/main/java/com/trifork/feature/weather/presentation/components/WeatherForecast.kt

@@ -30,7 +30,7 @@ import java.util.Locale
 fun WeatherForecast(
     weatherInfo: WeatherInfo,
     perDay: ImmutableList<WeatherData>,
-    viewModel: WeatherViewModel,
+    handleEvent: (WeatherEvent) -> Unit,
     modifier: Modifier = Modifier,
 ) {
     Column(
@@ -50,7 +50,7 @@ fun WeatherForecast(
             Text(
                 text = "${today.dayOfMonth} ${
                     today.month.toString().lowercase(Locale.getDefault())
-                }",
+                }, ${today.dayOfWeek}",
                 modifier = Modifier.align(Alignment.Bottom),
                 fontSize = 16.sp,
                 color = MaterialTheme.colorScheme.primary
@@ -72,7 +72,7 @@ fun WeatherForecast(
                         .height(100.dp)
                         .padding(horizontal = 16.dp)
                         .clickable {
-                            viewModel.state.handleEvent(
+                            handleEvent(
                                 WeatherEvent.UpdateHourlyInfo(
                                     WeatherInfo(
                                         geoLocation = weatherInfo.geoLocation,

+ 95 - 14
feature/weather/src/main/java/com/trifork/feature/weather/presentation/components/WeatherScreen.kt

@@ -1,9 +1,11 @@
 package com.trifork.feature.weather.presentation.components
 
 import android.Manifest
+import android.widget.Toast
 import androidx.activity.compose.rememberLauncherForActivityResult
 import androidx.activity.result.contract.ActivityResultContracts
 import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Spacer
@@ -14,12 +16,17 @@ import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.shape.CircleShape
 import androidx.compose.material.ExperimentalMaterialApi
 import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.material.icons.filled.FavoriteBorder
 import androidx.compose.material.icons.filled.LocationOn
+import androidx.compose.material.icons.filled.MoreVert
 import androidx.compose.material.icons.filled.Search
 import androidx.compose.material.pullrefresh.PullRefreshIndicator
 import androidx.compose.material.pullrefresh.pullRefresh
 import androidx.compose.material.pullrefresh.rememberPullRefreshState
 import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuItem
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.FloatingActionButton
 import androidx.compose.material3.Icon
@@ -31,37 +38,66 @@ import androidx.compose.material3.TopAppBar
 import androidx.compose.material3.TopAppBarDefaults.topAppBarColors
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.LifecycleEventEffect
+import androidx.lifecycle.compose.LifecycleResumeEffect
 import androidx.navigation.NavController
+import com.trifork.feature.common.components.ObserveAsEvents
 import com.trifork.feature.common.navigation.Screen
 import com.trifork.feature.weather.domain.model.WeatherLocation
-import com.trifork.feature.weather.presentation.WeatherViewModel
+import com.trifork.feature.weather.presentation.mvi.WeatherAction
 import com.trifork.feature.weather.presentation.mvi.WeatherEvent
+import com.trifork.feature.weather.presentation.mvi.WeatherState
+import kotlinx.coroutines.flow.Flow
 
 @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
 @Composable
 fun WeatherScreen(
     navController: NavController,
-    viewModel: WeatherViewModel
+    state: WeatherState,
+    handleEvent: (WeatherEvent) -> Unit,
+    action: Flow<WeatherAction>
 ) {
-    val state by viewModel.state.collectAsState()
+
+    var menuExpanded by remember {
+        mutableStateOf(false)
+    }
+    var showDialog by remember {
+        mutableStateOf(false)
+    }
 
     val permissionLauncher = rememberLauncherForActivityResult(
         ActivityResultContracts.RequestMultiplePermissions()
     ) { map ->
         if (map.values.filter { it }.size > 1) {
-            viewModel.state.handleEvent(WeatherEvent.Refresh)
+            handleEvent(WeatherEvent.Refresh)
+        }
+    }
+
+    LifecycleResumeEffect(Unit) {
+        handleEvent(WeatherEvent.Resume)
+        onPauseOrDispose {}
+    }
+
+    val context = LocalContext.current
+    ObserveAsEvents(flow = action) { onAction ->
+        when (onAction) {
+            is WeatherAction.Toast -> Toast.makeText(context, onAction.message, Toast.LENGTH_LONG)
+                .show()
         }
     }
 
     val isLoading = state.isLoading
     val pullRefreshState = rememberPullRefreshState(isLoading, {
-        viewModel.state.handleEvent(WeatherEvent.Refresh)
+        handleEvent(WeatherEvent.Refresh)
     })
 
 
@@ -94,16 +130,51 @@ fun WeatherScreen(
                 },
                 actions = {
                     IconButton(onClick = {
-                        viewModel.state.handleEvent(
+                        if (state.cached && state.weatherInfo != null) {
+                            handleEvent(WeatherEvent.Delete(state.weatherInfo))
+                        } else {
+                            showDialog = true
+                        }
+                    }) {
+                        Icon(
+                            imageVector = if (state.cached) Icons.Default.Favorite else Icons.Default.FavoriteBorder,
+                            tint = if (state.cached) MaterialTheme.colorScheme.surface else MaterialTheme.colorScheme.outline,
+                            contentDescription = "Cached"
+                        )
+                    }
+                    IconButton(onClick = {
+                        handleEvent(
                             WeatherEvent.LoadWeatherInfo(WeatherLocation())
                         )
                     }) {
                         Icon(
                             imageVector = Icons.Filled.LocationOn,
                             tint = MaterialTheme.colorScheme.primary,
-                            contentDescription = "Refresh"
+                            contentDescription = "Current Location"
+                        )
+                    }
+                    /*
+                    IconButton(onClick = { menuExpanded = true }) {
+                        Icon(
+                            imageVector = Icons.Default.MoreVert,
+                            tint = MaterialTheme.colorScheme.primary,
+                            contentDescription = "Menu"
                         )
                     }
+                    DropdownMenu(
+                        modifier = Modifier.background(MaterialTheme.colorScheme.background),
+                        expanded = menuExpanded,
+                        onDismissRequest = { menuExpanded = false })
+                    {
+                        DropdownMenuItem(
+                            onClick = { showDialog = true },
+                            text = { Text(text = "Save Location") }
+                        )
+                        DropdownMenuItem(
+                            onClick = { /*TODO*/ },
+                            text = { Text(text = "About") }
+                        )
+                    }*/
                 },
             )
         },
@@ -132,9 +203,9 @@ fun WeatherScreen(
                 CircularProgressIndicator(
                     modifier = Modifier.align(Alignment.Center)
                 )
-            } else if (state.error != null) {
+            } else if (state.error != null || state.weatherInfo == null) {
                 Text(
-                    text = state.error ?: "",
+                    text = state.error ?: "No Location Set !!!",
                     color = MaterialTheme.colorScheme.error,
                     textAlign = TextAlign.Center,
                     modifier = Modifier.align(Alignment.Center)
@@ -148,7 +219,7 @@ fun WeatherScreen(
                         LazyColumn {
                             item {
                                 WeatherCard(
-                                    data = state.weatherInfo!!.currentWeatherData!!,
+                                    data = state.weatherInfo.currentWeatherData!!,
                                     backgroundColor = MaterialTheme.colorScheme.primaryContainer
                                 )
                             }
@@ -167,12 +238,12 @@ fun WeatherScreen(
                             .background(MaterialTheme.colorScheme.background)
                     ) {
                         item {
-                            state.weatherInfo!!.weatherDataPerDay.forEach { perDay ->
+                            state.weatherInfo.weatherDataPerDay.forEach { perDay ->
                                 Spacer(modifier = Modifier.height(16.dp))
                                 WeatherForecast(
-                                    state.weatherInfo!!,
+                                    state.weatherInfo,
                                     perDay.value,
-                                    viewModel
+                                    handleEvent
                                 )
                             }
                             Spacer(modifier = Modifier.height(16.dp))
@@ -180,6 +251,16 @@ fun WeatherScreen(
                     }
                 }
             }
+
+            state.weatherInfo?.let {
+                SaveLocationDialog(
+                    weatherInfo = state.weatherInfo,
+                    showDialog = showDialog,
+                    handleEvent = handleEvent
+                ) {
+                    showDialog = false
+                }
+            }
         }
     }
 }

+ 5 - 0
feature/weather/src/main/java/com/trifork/feature/weather/presentation/mvi/WeatherAction.kt

@@ -0,0 +1,5 @@
+package com.trifork.feature.weather.presentation.mvi
+
+sealed interface WeatherAction {
+    data class Toast(val message: String) : WeatherAction
+}

+ 8 - 4
feature/weather/src/main/java/com/trifork/feature/weather/presentation/mvi/WeatherEvent.kt

@@ -4,13 +4,17 @@ import com.trifork.feature.weather.domain.model.WeatherInfo
 import com.trifork.feature.weather.domain.model.WeatherLocation
 
 sealed interface WeatherEvent {
-    data class LoadWeatherInfo(val geoLocation: WeatherLocation) :
-        WeatherEvent
+    data class LoadWeatherInfo(val geoLocation: WeatherLocation) : WeatherEvent
 
-    data class UpdateHourlyInfo(val weatherInfo: WeatherInfo) :
-        WeatherEvent
+    data class UpdateHourlyInfo(val weatherInfo: WeatherInfo) : WeatherEvent
 
     data object Refresh : WeatherEvent
 
     data class Error(val message: String) : WeatherEvent
+    data object About : WeatherEvent
+    data class CheckCache(val weatherInfo: WeatherInfo) : WeatherEvent
+    data class IsCache(val isCached: Boolean) : WeatherEvent
+    data object Resume : WeatherEvent
+    data class Save(val weatherInfo: WeatherInfo) : WeatherEvent
+    data class Delete(val weatherInfo: WeatherInfo) : WeatherEvent
 }

+ 4 - 2
feature/weather/src/main/java/com/trifork/feature/weather/presentation/mvi/WeatherState.kt

@@ -5,13 +5,15 @@ import com.trifork.feature.weather.domain.model.WeatherInfo
 data class WeatherState(
     val isLoading: Boolean,
     val error: String?,
-    val weatherInfo: WeatherInfo?
+    val weatherInfo: WeatherInfo?,
+    val cached: Boolean
 ) {
     companion object {
         val initial = WeatherState(
             isLoading = true,
             error = null,
-            weatherInfo = null
+            weatherInfo = null,
+            cached = false
         )
     }
 }

+ 24 - 6
feature/weather/src/main/res/drawable/ic_light_drizzel_night.xml

@@ -26,11 +26,29 @@
       android:strokeWidth="1.2"
       android:fillColor="#57a0ee"
       android:strokeColor="#fff"/>
-  <path
-      android:pathData="M26.918,32.944L25.529,40.822"
-      android:strokeWidth="2"
-      android:fillColor="#00000000"
-      android:strokeColor="#91c0f8"
-      android:strokeLineCap="round"/>
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M28,32.5L28,37.5"
+        android:strokeWidth="1.2"
+        android:strokeColor="#57a0ee"
+        android:strokeLineCap="round" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M29.768,33.232L26.232,36.768"
+        android:strokeWidth="1"
+        android:strokeColor="#57a0ee"
+        android:strokeLineCap="round" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M30.5,35L25.5,35"
+        android:strokeWidth="1"
+        android:strokeColor="#57a0ee"
+        android:strokeLineCap="round" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M29.768,36.768L26.232,33.232"
+        android:strokeWidth="1"
+        android:strokeColor="#57a0ee"
+        android:strokeLineCap="round" />
   </group>
 </vector>

+ 5 - 1
gradle/libs.versions.toml

@@ -5,6 +5,7 @@ compose = "1.8.2"
 immutable = "0.3.6"
 location = "21.0.1"
 hilt = "2.48.1"
+hilt-navigation = "1.1.0"
 navigationCompose = "2.7.6"
 lyfecyleCompose = "2.7.0-rc02"
 retrofit = "2.9.0"
@@ -14,6 +15,7 @@ espresso = "3.5.1"
 coroutines = "1.7.3"
 room = "2.6.1"
 material = "1.5.4"
+material3 = "1.1.2"
 org-jetbrains-kotlin-android = "1.9.10"
 appcompat = "1.6.1"
 com-google-android-material-material = "1.11.0"
@@ -27,6 +29,7 @@ androidx-activity-compose = { group = "androidx.activity", name = "activity-comp
 androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
 androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lyfecyleCompose" }
 android-compose-material = { group = "androidx.compose.material", name = "material", version.ref = "material" }
+android-compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" }
 
 kotlinx-collections-immutable = { group = "org.jetbrains.kotlinx", name = "kotlinx-collections-immutable", version.ref = "immutable" }
 
@@ -36,6 +39,7 @@ android-play-services-location = { group = "com.google.android.gms", name = "pla
 hilt-android-gradle-plugin = { module = "com.google.dagger:hilt-android-gradle-plugin", version.ref = "hilt" }
 hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
 hilt-android-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
+hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hilt-navigation" }
 
 # Retrofit
 retrofit-retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
@@ -60,4 +64,4 @@ com-android-library = { id = "com.android.library", version.ref = "androidGradle
 org-jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "org-jetbrains-kotlin-android" }
 com-google-devtools-ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
 com-google-dagger-hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
-dagger-hilt = { id = "dagger.hilt.android.plugin", version.ref = "hilt"}
+dagger-hilt = { id = "dagger.hilt.android.plugin", version.ref = "hilt" }