From 9b3a1a1dcb2ffbc7572e6cff83c97a78018059fe Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Manuel=20Nu=C3=B1ez?= <03.manu@gmail.com>
Date: Tue, 16 Apr 2024 17:33:03 -0400
Subject: [PATCH 1/8] Added navigation bottom bar structure #46
---
.../com/manuelnunez/apps/MainActivity.kt | 7 +-
.../kotlin/com/manuelnunez/apps/MainApp.kt | 72 +++++++
.../com/manuelnunez/apps/MainDestination.kt | 50 +++++
app/src/main/res/values/strings.xml | 2 +
.../apps/core/ui/component/Navigation.kt | 188 ++++++++++++++++++
.../manuelnunez/apps/core/ui/theme/Theme.kt | 13 +-
6 files changed, 318 insertions(+), 14 deletions(-)
create mode 100644 app/src/main/kotlin/com/manuelnunez/apps/MainApp.kt
create mode 100644 app/src/main/kotlin/com/manuelnunez/apps/MainDestination.kt
create mode 100644 core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/Navigation.kt
diff --git a/app/src/main/kotlin/com/manuelnunez/apps/MainActivity.kt b/app/src/main/kotlin/com/manuelnunez/apps/MainActivity.kt
index d524f9d..8ff5351 100644
--- a/app/src/main/kotlin/com/manuelnunez/apps/MainActivity.kt
+++ b/app/src/main/kotlin/com/manuelnunez/apps/MainActivity.kt
@@ -6,9 +6,7 @@ import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
-import androidx.navigation.compose.rememberNavController
import com.manuelnunez.apps.core.ui.theme.MainTheme
-import com.manuelnunez.apps.navigation.MainNavigation
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
@@ -23,9 +21,6 @@ class MainActivity : ComponentActivity() {
enableEdgeToEdge()
- setContent {
- val navController = rememberNavController()
- MainTheme { MainNavigation(navController = navController) }
- }
+ setContent { MainTheme { MainApp() } }
}
}
diff --git a/app/src/main/kotlin/com/manuelnunez/apps/MainApp.kt b/app/src/main/kotlin/com/manuelnunez/apps/MainApp.kt
new file mode 100644
index 0000000..4e997f1
--- /dev/null
+++ b/app/src/main/kotlin/com/manuelnunez/apps/MainApp.kt
@@ -0,0 +1,72 @@
+package com.manuelnunez.apps
+
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.WindowInsetsSides
+import androidx.compose.foundation.layout.consumeWindowInsets
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.only
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.safeDrawing
+import androidx.compose.foundation.layout.windowInsetsPadding
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.navigation.compose.rememberNavController
+import com.manuelnunez.apps.core.ui.component.MainGradientBackground
+import com.manuelnunez.apps.core.ui.component.MainNavigationBar
+import com.manuelnunez.apps.core.ui.component.MainNavigationBarItem
+import com.manuelnunez.apps.navigation.MainNavigation
+
+@Composable
+fun MainApp() {
+ val navController = rememberNavController()
+ var currentMainDestination by rememberSaveable { mutableStateOf(MainDestination.HOME) }
+
+ Scaffold(
+ bottomBar = {
+ MainNavigationBar {
+ mainDestinations().forEach { destination ->
+ MainNavigationBarItem(
+ selected = currentMainDestination == destination,
+ onClick = {
+ currentMainDestination = destination
+ navController.onNavigateToDestination(destination)
+ },
+ icon = {
+ Icon(
+ imageVector = destination.unselectedIcon,
+ contentDescription = null,
+ )
+ },
+ selectedIcon = {
+ Icon(
+ imageVector = destination.selectedIcon,
+ contentDescription = null,
+ )
+ },
+ label = { Text(stringResource(destination.iconTextId)) })
+ }
+ }
+ }) { paddingValues ->
+ Row(
+ Modifier.fillMaxSize()
+ .padding(paddingValues)
+ .consumeWindowInsets(paddingValues)
+ .windowInsetsPadding(
+ WindowInsets.safeDrawing.only(
+ WindowInsetsSides.Horizontal,
+ ),
+ ),
+ ) {
+ MainGradientBackground { MainNavigation(navController = navController) }
+ }
+ }
+}
diff --git a/app/src/main/kotlin/com/manuelnunez/apps/MainDestination.kt b/app/src/main/kotlin/com/manuelnunez/apps/MainDestination.kt
new file mode 100644
index 0000000..b44b249
--- /dev/null
+++ b/app/src/main/kotlin/com/manuelnunez/apps/MainDestination.kt
@@ -0,0 +1,50 @@
+package com.manuelnunez.apps
+
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.material.icons.filled.Home
+import androidx.compose.material.icons.outlined.FavoriteBorder
+import androidx.compose.material.icons.outlined.Home
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.navigation.NavGraph.Companion.findStartDestination
+import androidx.navigation.NavHostController
+import androidx.navigation.navOptions
+import com.manuelnunez.apps.features.home.ui.navigation.navigateToHome
+
+enum class MainDestination(
+ val selectedIcon: ImageVector,
+ val unselectedIcon: ImageVector,
+ val iconTextId: Int,
+ val titleTextId: Int,
+) {
+ HOME(
+ selectedIcon = Icons.Default.Home,
+ unselectedIcon = Icons.Outlined.Home,
+ iconTextId = R.string.home_destination_title,
+ titleTextId = R.string.home_destination_title),
+ FAVORITES(
+ selectedIcon = Icons.Default.Favorite,
+ unselectedIcon = Icons.Outlined.FavoriteBorder,
+ iconTextId = R.string.favorites_destination_title,
+ titleTextId = R.string.favorites_destination_title)
+}
+
+fun NavHostController.onNavigateToDestination(destination: MainDestination) {
+ val topLevelNavOptions = navOptions {
+ // Pop up to the start destination of the graph to
+ // avoid building up a large stack of destinations
+ // on the back stack as users select items
+ popUpTo(graph.findStartDestination().id) { saveState = true }
+ // Avoid multiple copies of the same destination when
+ // reselecting the same item
+ launchSingleTop = true
+ // Restore state when reselecting a previously selected item
+ restoreState = true
+ }
+ when (destination) {
+ MainDestination.HOME -> navigateToHome(topLevelNavOptions)
+ MainDestination.FAVORITES -> {} // navigateToFavorites()
+ }
+}
+
+fun mainDestinations() = listOf(MainDestination.HOME, MainDestination.FAVORITES)
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 4c5d06a..d5d7b69 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,3 +1,5 @@
Purrfect Pics
+ Home
+ Favorites
\ No newline at end of file
diff --git a/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/Navigation.kt b/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/Navigation.kt
new file mode 100644
index 0000000..44c8765
--- /dev/null
+++ b/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/Navigation.kt
@@ -0,0 +1,188 @@
+package com.manuelnunez.apps.core.ui.component
+
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.material.icons.filled.Home
+import androidx.compose.material.icons.outlined.FavoriteBorder
+import androidx.compose.material.icons.outlined.Home
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.NavigationBar
+import androidx.compose.material3.NavigationBarItem
+import androidx.compose.material3.NavigationBarItemDefaults
+import androidx.compose.material3.NavigationRail
+import androidx.compose.material3.NavigationRailItem
+import androidx.compose.material3.NavigationRailItemDefaults
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import com.manuelnunez.apps.core.ui.theme.MainTheme
+import com.manuelnunez.apps.core.ui.utils.ThemePreviews
+
+/** Wraps Material 3 [NavigationBarItem]. */
+@Composable
+fun RowScope.MainNavigationBarItem(
+ selected: Boolean,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ alwaysShowLabel: Boolean = true,
+ icon: @Composable () -> Unit,
+ selectedIcon: @Composable () -> Unit = icon,
+ label: @Composable (() -> Unit)? = null,
+) {
+ NavigationBarItem(
+ selected = selected,
+ onClick = onClick,
+ icon = if (selected) selectedIcon else icon,
+ modifier = modifier,
+ enabled = enabled,
+ label = label,
+ alwaysShowLabel = alwaysShowLabel,
+ colors =
+ NavigationBarItemDefaults.colors(
+ selectedIconColor = NavigationDefaults.navigationSelectedItemColor(),
+ unselectedIconColor = NavigationDefaults.navigationContentColor(),
+ selectedTextColor = NavigationDefaults.navigationSelectedItemColor(),
+ unselectedTextColor = NavigationDefaults.navigationContentColor(),
+ indicatorColor = NavigationDefaults.navigationIndicatorColor(),
+ ),
+ )
+}
+
+/** Wraps Material 3 [NavigationBar]. */
+@Composable
+fun MainNavigationBar(
+ modifier: Modifier = Modifier,
+ content: @Composable RowScope.() -> Unit,
+) {
+ NavigationBar(
+ modifier = modifier,
+ contentColor = NavigationDefaults.navigationContentColor(),
+ tonalElevation = 0.dp,
+ content = content,
+ )
+}
+
+/** Wraps Material 3 [NavigationRailItem]. */
+@Composable
+fun MainNavigationRailItem(
+ selected: Boolean,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ alwaysShowLabel: Boolean = true,
+ icon: @Composable () -> Unit,
+ selectedIcon: @Composable () -> Unit = icon,
+ label: @Composable (() -> Unit)? = null,
+) {
+ NavigationRailItem(
+ selected = selected,
+ onClick = onClick,
+ icon = if (selected) selectedIcon else icon,
+ modifier = modifier,
+ enabled = enabled,
+ label = label,
+ alwaysShowLabel = alwaysShowLabel,
+ colors =
+ NavigationRailItemDefaults.colors(
+ selectedIconColor = NavigationDefaults.navigationSelectedItemColor(),
+ unselectedIconColor = NavigationDefaults.navigationContentColor(),
+ selectedTextColor = NavigationDefaults.navigationSelectedItemColor(),
+ unselectedTextColor = NavigationDefaults.navigationContentColor(),
+ indicatorColor = NavigationDefaults.navigationIndicatorColor(),
+ ),
+ )
+}
+
+/** Wraps Material 3 [NavigationRail]. */
+@Composable
+fun MainNavigationRail(
+ modifier: Modifier = Modifier,
+ header: @Composable (ColumnScope.() -> Unit)? = null,
+ content: @Composable ColumnScope.() -> Unit,
+) {
+ NavigationRail(
+ modifier = modifier,
+ containerColor = Color.Transparent,
+ contentColor = NavigationDefaults.navigationContentColor(),
+ header = header,
+ content = content,
+ )
+}
+
+@ThemePreviews
+@Composable
+fun NavigationBarPreview() {
+ val items = listOf("Home", "Favorites")
+ val icons = listOf(Icons.Outlined.Home, Icons.Outlined.FavoriteBorder)
+ val selectedIcons = listOf(Icons.Default.Home, Icons.Default.Favorite)
+
+ MainTheme {
+ MainNavigationBar {
+ items.forEachIndexed { index, item ->
+ MainNavigationBarItem(
+ icon = {
+ Icon(
+ imageVector = icons[index],
+ contentDescription = item,
+ )
+ },
+ selectedIcon = {
+ Icon(
+ imageVector = selectedIcons[index],
+ contentDescription = item,
+ )
+ },
+ label = { Text(item) },
+ selected = index == 0,
+ onClick = {},
+ )
+ }
+ }
+ }
+}
+
+@ThemePreviews
+@Composable
+fun NavigationRailPreview() {
+ val items = listOf("Home", "Favorites")
+ val icons = listOf(Icons.Outlined.Home, Icons.Outlined.FavoriteBorder)
+ val selectedIcons = listOf(Icons.Default.Home, Icons.Default.Favorite)
+
+ MainTheme {
+ MainNavigationRail {
+ items.forEachIndexed { index, item ->
+ MainNavigationRailItem(
+ icon = {
+ Icon(
+ imageVector = icons[index],
+ contentDescription = item,
+ )
+ },
+ selectedIcon = {
+ Icon(
+ imageVector = selectedIcons[index],
+ contentDescription = item,
+ )
+ },
+ label = { Text(item) },
+ selected = index == 1,
+ onClick = {},
+ )
+ }
+ }
+ }
+}
+
+object NavigationDefaults {
+ @Composable fun navigationContentColor() = MaterialTheme.colorScheme.onSurfaceVariant
+
+ @Composable fun navigationSelectedItemColor() = MaterialTheme.colorScheme.onPrimaryContainer
+
+ @Composable fun navigationIndicatorColor() = MaterialTheme.colorScheme.primaryContainer
+}
diff --git a/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/theme/Theme.kt b/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/theme/Theme.kt
index c35280a..c0b74b2 100644
--- a/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/theme/Theme.kt
+++ b/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/theme/Theme.kt
@@ -13,7 +13,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
-import com.manuelnunez.apps.core.ui.component.MainGradientBackground
val LightColorScheme =
lightColorScheme(
@@ -138,13 +137,11 @@ fun MainTheme(
LocalGradientColors provides gradientColors,
LocalBackgroundTheme provides backgroundTheme,
) {
- MainGradientBackground {
- MaterialTheme(
- colorScheme = colorScheme,
- typography = Typography,
- content = content,
- )
- }
+ MaterialTheme(
+ colorScheme = colorScheme,
+ typography = Typography,
+ content = content,
+ )
}
}
From 77ae25e7e000f0a7cc317ebcd317ed44f13d9d3f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Manuel=20Nu=C3=B1ez?= <03.manu@gmail.com>
Date: Tue, 16 Apr 2024 17:43:50 -0400
Subject: [PATCH 2/8] Cleanup code
---
.../core/data/datasource/CataasCatsRemoteDataSource.kt | 9 +--------
.../core/data/datasource/PexeelsCatsRemoteDataSource.kt | 9 +--------
.../services/executors/ServiceExecutorRetrofitImpl.kt | 8 ++++----
.../apps/core/ui/component/AdaptableVerticalGrid.kt | 3 ++-
.../manuelnunez/apps/core/ui/component/ErrorDialog.kt | 2 +-
core/ui/src/main/res/values/strings.xml | 4 ----
.../manuelnunez/apps/feature/detail/ui/DetailViewTest.kt | 2 +-
.../feature/detail/ui/components/DetailErrorScreen.kt | 3 ++-
features/detail/ui/src/main/res/values/strings.xml | 1 +
.../manuelnunez/apps/features/home/ui/HomeViewTest.kt | 4 ++--
.../apps/features/home/ui/components/HomeScreen.kt | 4 ++--
features/home/ui/src/main/res/values/strings.xml | 1 +
12 files changed, 18 insertions(+), 32 deletions(-)
diff --git a/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/datasource/CataasCatsRemoteDataSource.kt b/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/datasource/CataasCatsRemoteDataSource.kt
index de9d737..d14fd19 100644
--- a/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/datasource/CataasCatsRemoteDataSource.kt
+++ b/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/datasource/CataasCatsRemoteDataSource.kt
@@ -33,14 +33,7 @@ constructor(private val servicesExecutor: ServicesExecutor, private val apiServi
override fun getItems(): Either, ServiceError> {
val response = servicesExecutor.execute(RetrofitServiceRequest(apiService.searchCats()))
- return response.fold(
- success = {
- eitherSuccess(it.data)
- },
- error = {
- eitherError(it)
- }
- )
+ return response.fold(success = { eitherSuccess(it.data) }, error = { eitherError(it) })
}
override fun getAllItems(): Flow> =
diff --git a/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/datasource/PexeelsCatsRemoteDataSource.kt b/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/datasource/PexeelsCatsRemoteDataSource.kt
index 2ca0ca6..d3ef3b7 100644
--- a/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/datasource/PexeelsCatsRemoteDataSource.kt
+++ b/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/datasource/PexeelsCatsRemoteDataSource.kt
@@ -33,14 +33,7 @@ constructor(private val servicesExecutor: ServicesExecutor, private val apiServi
override fun getItems(): Either {
val response = servicesExecutor.execute(RetrofitServiceRequest(apiService.searchCats()))
- return response.fold(
- success = {
- eitherSuccess(it.data)
- },
- error = {
- eitherError(it)
- }
- )
+ return response.fold(success = { eitherSuccess(it.data) }, error = { eitherError(it) })
}
override fun getAllItems(): Flow> =
diff --git a/core/services/src/main/kotlin/com/manuelnunez/apps/core/services/executors/ServiceExecutorRetrofitImpl.kt b/core/services/src/main/kotlin/com/manuelnunez/apps/core/services/executors/ServiceExecutorRetrofitImpl.kt
index e96261c..150540a 100644
--- a/core/services/src/main/kotlin/com/manuelnunez/apps/core/services/executors/ServiceExecutorRetrofitImpl.kt
+++ b/core/services/src/main/kotlin/com/manuelnunez/apps/core/services/executors/ServiceExecutorRetrofitImpl.kt
@@ -31,13 +31,13 @@ class ServiceExecutorRetrofitImpl : ServicesExecutor {
headers = response.headers().toMultimap()))
}
} catch (ex: TimeoutException) {
- eitherError(ServiceError("Timeout error: ${ex.message}", -1, emptyMap()))
+ eitherError(ServiceError("Timeout error: ${ex.message}", -1, emptyMap()))
} catch (ex: NetworkException) {
- eitherError(ServiceError("Network error: ${ex.message}", -1, emptyMap()))
+ eitherError(ServiceError("Network error: ${ex.message}", -1, emptyMap()))
} catch (ex: JsonSyntaxException) {
- eitherError(ServiceError("Json data parsing error: ${ex.message}", -1, emptyMap()))
+ eitherError(ServiceError("Json data parsing error: ${ex.message}", -1, emptyMap()))
} catch (e: Exception) {
- eitherError(ServiceError("Unknown error: ${e.message}", -1, emptyMap()))
+ eitherError(ServiceError("Unknown error: ${e.message}", -1, emptyMap()))
}
}
}
diff --git a/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/AdaptableVerticalGrid.kt b/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/AdaptableVerticalGrid.kt
index 311c191..8589718 100644
--- a/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/AdaptableVerticalGrid.kt
+++ b/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/AdaptableVerticalGrid.kt
@@ -79,7 +79,8 @@ fun AdaptableVerticalGridPreview() {
Card(Modifier.size(50.dp, 80.dp).padding(horizontal = 10.dp).padding(bottom = 20.dp)) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Icon(
- painter = painterResource(id = R.drawable.ic_broken_image), contentDescription = "")
+ painter = painterResource(id = R.drawable.ic_broken_image),
+ contentDescription = null)
}
}
}
diff --git a/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/ErrorDialog.kt b/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/ErrorDialog.kt
index e12188f..92f42e9 100644
--- a/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/ErrorDialog.kt
+++ b/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/ErrorDialog.kt
@@ -39,7 +39,7 @@ fun ErrorDialog(
when {
openAlertDialog.value -> {
AlertDialog(
- icon = { Icon(icon, contentDescription = "Warning Icon") },
+ icon = { Icon(icon, contentDescription = null) },
title = { Text(text = dialogTitle) },
text = { Text(text = dialogText) },
onDismissRequest = { onDismissRequest() },
diff --git a/core/ui/src/main/res/values/strings.xml b/core/ui/src/main/res/values/strings.xml
index d4bf228..d474e51 100644
--- a/core/ui/src/main/res/values/strings.xml
+++ b/core/ui/src/main/res/values/strings.xml
@@ -1,15 +1,11 @@
- Featured
Popular
Go back
An Error has occurred. Please try again.
An Error has occurred
- An Error has occurred in Featured section
- An Error has occurred in Popular section
"Ups!"
Retry
- An Error has occurred. Please go back and try again.
Confirm
Dismiss
diff --git a/features/detail/ui/src/androidTest/kotlin/com/manuelnunez/apps/feature/detail/ui/DetailViewTest.kt b/features/detail/ui/src/androidTest/kotlin/com/manuelnunez/apps/feature/detail/ui/DetailViewTest.kt
index 0f5f1e7..ba45abd 100644
--- a/features/detail/ui/src/androidTest/kotlin/com/manuelnunez/apps/feature/detail/ui/DetailViewTest.kt
+++ b/features/detail/ui/src/androidTest/kotlin/com/manuelnunez/apps/feature/detail/ui/DetailViewTest.kt
@@ -47,7 +47,7 @@ class DetailViewTest {
composeTestRule
.onNodeWithText(
- composeTestRule.activity.resources.getString(RCU.string.alert_error_try_again_back),
+ composeTestRule.activity.resources.getString(R.string.alert_error_try_again_back),
substring = true,
)
.assertExists()
diff --git a/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/feature/detail/ui/components/DetailErrorScreen.kt b/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/feature/detail/ui/components/DetailErrorScreen.kt
index a0153e5..d3efe87 100644
--- a/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/feature/detail/ui/components/DetailErrorScreen.kt
+++ b/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/feature/detail/ui/components/DetailErrorScreen.kt
@@ -5,6 +5,7 @@ import androidx.compose.ui.res.stringResource
import com.manuelnunez.apps.core.ui.component.ErrorDialog
import com.manuelnunez.apps.core.ui.theme.MainTheme
import com.manuelnunez.apps.core.ui.utils.OrientationPreviews
+import com.manuelnunez.apps.features.detail.ui.R
import com.manuelnunez.apps.core.ui.R as RCU
@Composable
@@ -12,7 +13,7 @@ fun DetailErrorScreen(onBackClick: () -> Unit) {
ErrorDialog(
onConfirmClick = onBackClick,
dialogTitle = stringResource(id = RCU.string.alert_error_title),
- dialogText = stringResource(id = RCU.string.alert_error_try_again_back))
+ dialogText = stringResource(id = R.string.alert_error_try_again_back))
}
@OrientationPreviews
diff --git a/features/detail/ui/src/main/res/values/strings.xml b/features/detail/ui/src/main/res/values/strings.xml
index 6f5effd..5e67771 100644
--- a/features/detail/ui/src/main/res/values/strings.xml
+++ b/features/detail/ui/src/main/res/values/strings.xml
@@ -2,4 +2,5 @@
Share
Checkout this image from Purrfect Pics!: %1$s
Share Image
+ An Error has occurred. Please go back and try again.
\ No newline at end of file
diff --git a/features/home/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/home/ui/HomeViewTest.kt b/features/home/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/home/ui/HomeViewTest.kt
index e1cf49d..6b22b4d 100644
--- a/features/home/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/home/ui/HomeViewTest.kt
+++ b/features/home/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/home/ui/HomeViewTest.kt
@@ -30,7 +30,7 @@ class HomeViewTest {
// Feature loader
composeTestRule
.onNodeWithContentDescription(
- composeTestRule.activity.resources.getString(RCU.string.section_feature),
+ composeTestRule.activity.resources.getString(R.string.section_feature),
)
.assertExists()
@@ -59,7 +59,7 @@ class HomeViewTest {
// Feature title
composeTestRule
.onNodeWithText(
- composeTestRule.activity.resources.getString(RCU.string.section_feature),
+ composeTestRule.activity.resources.getString(R.string.section_feature),
substring = true,
)
.assertExists()
diff --git a/features/home/ui/src/main/kotlin/com/manuelnunez/apps/features/home/ui/components/HomeScreen.kt b/features/home/ui/src/main/kotlin/com/manuelnunez/apps/features/home/ui/components/HomeScreen.kt
index 0284e53..6f52fb5 100644
--- a/features/home/ui/src/main/kotlin/com/manuelnunez/apps/features/home/ui/components/HomeScreen.kt
+++ b/features/home/ui/src/main/kotlin/com/manuelnunez/apps/features/home/ui/components/HomeScreen.kt
@@ -54,7 +54,7 @@ fun HomeScreen(
FeaturedItemsState.Loading ->
item {
LoadingIndicator(
- loaderContentDescription = stringResource(id = RCU.string.section_feature))
+ loaderContentDescription = stringResource(id = R.string.section_feature))
}
FeaturedItemsState.Error ->
item { ItemError(stringResource(id = R.string.alert_error_feature)) }
@@ -83,7 +83,7 @@ private fun FeaturedItem(items: List- , navigateToDetails: (Item) -> Unit) {
Column {
SurfaceText(
modifier = Modifier.padding(vertical = 6.dp, horizontal = 20.dp),
- text = stringResource(id = RCU.string.section_feature))
+ text = stringResource(id = R.string.section_feature))
Spacer(modifier = Modifier.height(10.dp))
diff --git a/features/home/ui/src/main/res/values/strings.xml b/features/home/ui/src/main/res/values/strings.xml
index 2e9e4c9..b339b97 100644
--- a/features/home/ui/src/main/res/values/strings.xml
+++ b/features/home/ui/src/main/res/values/strings.xml
@@ -1,4 +1,5 @@
+ Featured
See more
An Error has occurred in Featured section
An Error has occurred in Popular section
From a130b3324002cf683cdb41bccfd88ae32b279465 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Manuel=20Nu=C3=B1ez?= <03.manu@gmail.com>
Date: Wed, 17 Apr 2024 00:45:37 -0400
Subject: [PATCH 3/8] Added base structure of favorites module #47
---
app/build.gradle.kts | 1 +
.../com/manuelnunez/apps/MainDestination.kt | 3 +-
.../apps/navigation/MainNavigation.kt | 10 +--
features/favorites/domain/.gitignore | 1 +
features/favorites/domain/build.gradle.kts | 38 +++++++++++
.../domain/src/main/AndroidManifest.xml | 4 ++
features/favorites/ui/.gitignore | 1 +
features/favorites/ui/build.gradle.kts | 67 +++++++++++++++++++
.../favorites/ui/src/main/AndroidManifest.xml | 4 ++
.../feature/favorites/ui/FavoritesView.kt | 17 +++++
.../ui/navigation/FavoritesNavigation.kt | 16 +++++
settings.gradle.kts | 4 ++
12 files changed, 161 insertions(+), 5 deletions(-)
create mode 100644 features/favorites/domain/.gitignore
create mode 100644 features/favorites/domain/build.gradle.kts
create mode 100644 features/favorites/domain/src/main/AndroidManifest.xml
create mode 100644 features/favorites/ui/.gitignore
create mode 100644 features/favorites/ui/build.gradle.kts
create mode 100644 features/favorites/ui/src/main/AndroidManifest.xml
create mode 100644 features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/feature/favorites/ui/FavoritesView.kt
create mode 100644 features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/feature/favorites/ui/navigation/FavoritesNavigation.kt
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 8261cb7..0aaf1e0 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -75,6 +75,7 @@ dependencies {
implementation(projects.features.home.ui)
implementation(projects.features.detail.ui)
implementation(projects.features.seemore.ui)
+ implementation(projects.features.favorites.ui)
implementation(libs.androidx.core.splashscreen)
implementation(libs.androidx.core.ktx)
diff --git a/app/src/main/kotlin/com/manuelnunez/apps/MainDestination.kt b/app/src/main/kotlin/com/manuelnunez/apps/MainDestination.kt
index b44b249..2a81196 100644
--- a/app/src/main/kotlin/com/manuelnunez/apps/MainDestination.kt
+++ b/app/src/main/kotlin/com/manuelnunez/apps/MainDestination.kt
@@ -9,6 +9,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavHostController
import androidx.navigation.navOptions
+import com.manuelnunez.apps.feature.favorites.ui.navigation.navigateToFavorites
import com.manuelnunez.apps.features.home.ui.navigation.navigateToHome
enum class MainDestination(
@@ -43,7 +44,7 @@ fun NavHostController.onNavigateToDestination(destination: MainDestination) {
}
when (destination) {
MainDestination.HOME -> navigateToHome(topLevelNavOptions)
- MainDestination.FAVORITES -> {} // navigateToFavorites()
+ MainDestination.FAVORITES -> navigateToFavorites(topLevelNavOptions)
}
}
diff --git a/app/src/main/kotlin/com/manuelnunez/apps/navigation/MainNavigation.kt b/app/src/main/kotlin/com/manuelnunez/apps/navigation/MainNavigation.kt
index 0886528..99cdf1e 100644
--- a/app/src/main/kotlin/com/manuelnunez/apps/navigation/MainNavigation.kt
+++ b/app/src/main/kotlin/com/manuelnunez/apps/navigation/MainNavigation.kt
@@ -4,12 +4,13 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
-import com.manuelnunez.apps.feature.detail.ui.navigation.detailScreen
-import com.manuelnunez.apps.feature.detail.ui.navigation.navigateToDetail
-import com.manuelnunez.apps.feature.seemore.ui.navigation.navigateToSeeMore
-import com.manuelnunez.apps.feature.seemore.ui.navigation.seeMoreScreen
+import com.manuelnunez.apps.feature.favorites.ui.navigation.favoritesScreen
+import com.manuelnunez.apps.features.detail.ui.navigation.detailScreen
+import com.manuelnunez.apps.features.detail.ui.navigation.navigateToDetail
import com.manuelnunez.apps.features.home.ui.navigation.HOME_ROUTE
import com.manuelnunez.apps.features.home.ui.navigation.homeScreen
+import com.manuelnunez.apps.features.seemore.ui.navigation.navigateToSeeMore
+import com.manuelnunez.apps.features.seemore.ui.navigation.seeMoreScreen
@Composable
fun MainNavigation(
@@ -25,5 +26,6 @@ fun MainNavigation(
seeMoreScreen(
onBackClick = { navController.navigateUp() },
navigateToDetails = navController::navigateToDetail)
+ favoritesScreen()
}
}
diff --git a/features/favorites/domain/.gitignore b/features/favorites/domain/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/features/favorites/domain/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/features/favorites/domain/build.gradle.kts b/features/favorites/domain/build.gradle.kts
new file mode 100644
index 0000000..4a25a1b
--- /dev/null
+++ b/features/favorites/domain/build.gradle.kts
@@ -0,0 +1,38 @@
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.ksp)
+}
+
+android {
+ namespace = "com.manuelnunez.apps.features.home.domain"
+ compileSdk = 34
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+
+ defaultConfig { minSdk = 21 }
+
+ kotlinOptions { jvmTarget = JavaVersion.VERSION_17.toString() }
+
+ tasks.withType { useJUnitPlatform() }
+
+ packaging { resources { excludes.add("META-INF/{LICENSE-notice.md,LICENSE.md}") } }
+}
+
+dependencies {
+ implementation(projects.core.common)
+ implementation(projects.core.domain)
+
+ implementation(libs.kotlinx.coroutines.android)
+
+ // HILT
+ implementation(libs.hilt.android)
+ ksp(libs.hilt.compiler)
+
+ testImplementation(libs.junit)
+ testImplementation(libs.mockk)
+ testImplementation(libs.turbine)
+}
diff --git a/features/favorites/domain/src/main/AndroidManifest.xml b/features/favorites/domain/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..44008a4
--- /dev/null
+++ b/features/favorites/domain/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/features/favorites/ui/.gitignore b/features/favorites/ui/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/features/favorites/ui/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/features/favorites/ui/build.gradle.kts b/features/favorites/ui/build.gradle.kts
new file mode 100644
index 0000000..97b9097
--- /dev/null
+++ b/features/favorites/ui/build.gradle.kts
@@ -0,0 +1,67 @@
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.ksp)
+}
+
+android {
+ namespace = "com.manuelnunez.apps.features.favorites.ui"
+ compileSdk = 34
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+
+ defaultConfig {
+ minSdk = 21
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ kotlinOptions { jvmTarget = JavaVersion.VERSION_17.toString() }
+
+ buildFeatures { compose = true }
+
+ composeOptions { kotlinCompilerExtensionVersion = libs.versions.androidxComposeCompiler.get() }
+
+ tasks.withType { useJUnitPlatform() }
+
+ packaging { resources { excludes.add("META-INF/{LICENSE-notice.md,LICENSE.md}") } }
+
+ buildTypes { release { isMinifyEnabled = false } }
+}
+
+dependencies {
+ implementation(projects.core.common)
+ implementation(projects.core.domain)
+ implementation(projects.core.ui)
+
+ // Arch Components
+ implementation(libs.androidx.lifecycle.runtime.compose)
+ implementation(libs.androidx.lifecycle.viewmodel.compose)
+ implementation(libs.androidx.hilt.navigation.compose)
+
+ // Hilt
+ implementation(libs.hilt.android)
+ debugImplementation(libs.androidx.ui.tooling)
+ ksp(libs.hilt.compiler)
+
+ // Coil
+ implementation(libs.coil.kt)
+ implementation(libs.coil.kt.compose)
+ implementation(libs.coil.kt.gif)
+
+ // Compose
+ implementation(libs.androidx.compose.ui)
+ implementation(libs.androidx.compose.ui.tooling.preview)
+ implementation(libs.androidx.compose.material3)
+
+ // Local tests: jUnit, coroutines, Android runner
+ testImplementation(libs.junit)
+ testImplementation(libs.kotlinx.coroutines.test)
+ testImplementation(libs.mockk)
+ testImplementation(libs.turbine)
+
+ api(libs.androidx.compose.ui.test.junit4)
+ debugApi(libs.androidx.compose.ui.test.manifest)
+}
diff --git a/features/favorites/ui/src/main/AndroidManifest.xml b/features/favorites/ui/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..44008a4
--- /dev/null
+++ b/features/favorites/ui/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/feature/favorites/ui/FavoritesView.kt b/features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/feature/favorites/ui/FavoritesView.kt
new file mode 100644
index 0000000..9328950
--- /dev/null
+++ b/features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/feature/favorites/ui/FavoritesView.kt
@@ -0,0 +1,17 @@
+package com.manuelnunez.apps.feature.favorites.ui
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.safeContent
+import androidx.compose.foundation.layout.windowInsetsPadding
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import com.manuelnunez.apps.core.ui.component.SurfaceText
+
+@Composable
+fun FavoritesView() {
+ Column(Modifier.fillMaxSize().windowInsetsPadding(WindowInsets.safeContent)) {
+ SurfaceText(text = "Favoritos")
+ }
+}
diff --git a/features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/feature/favorites/ui/navigation/FavoritesNavigation.kt b/features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/feature/favorites/ui/navigation/FavoritesNavigation.kt
new file mode 100644
index 0000000..d5b40d1
--- /dev/null
+++ b/features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/feature/favorites/ui/navigation/FavoritesNavigation.kt
@@ -0,0 +1,16 @@
+package com.manuelnunez.apps.feature.favorites.ui.navigation
+
+import androidx.navigation.NavController
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.NavOptions
+import androidx.navigation.compose.composable
+import com.manuelnunez.apps.feature.favorites.ui.FavoritesView
+
+const val FAVORITES_ROUTE = "favorites_route"
+
+fun NavController.navigateToFavorites(navOptions: NavOptions) =
+ navigate(FAVORITES_ROUTE, navOptions)
+
+fun NavGraphBuilder.favoritesScreen() {
+ composable(route = FAVORITES_ROUTE) { FavoritesView() }
+}
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 96ffefe..902094e 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -45,3 +45,7 @@ include(":features:detail:ui")
include(":features:seemore:ui")
include(":features:seemore:domain")
+
+include(":features:favorites:ui")
+
+include(":features:favorites:domain")
From ba4ff25ef25438d24736f282616ed4476c92a848 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Manuel=20Nu=C3=B1ez?= <03.manu@gmail.com>
Date: Wed, 17 Apr 2024 00:46:01 -0400
Subject: [PATCH 4/8] Updated package name
---
.../kotlin/com/manuelnunez/apps/core/data/di/DataModule.kt | 2 +-
.../apps/core/data/repository/SeeMoreRepositoryImpl.kt | 2 +-
.../apps/core/data/repository/SeeMoreRepositoryTest.kt | 2 +-
.../apps/{feature => features}/detail/ui/DetailViewTest.kt | 7 +++----
.../apps/{feature => features}/detail/ui/DetailView.kt | 6 +++---
.../detail/ui/components/DetailErrorScreen.kt | 2 +-
.../detail/ui/components/DetailScreen.kt | 2 +-
.../detail/ui/navigation/DetailNavigation.kt | 4 ++--
.../seemore/domain/repository/SeeMoreRepository.kt | 2 +-
.../seemore/domain/usecase/GetAllItemUseCase.kt | 4 ++--
.../apps/feature/seemore/domain/GetAllItemUseCaseTest.kt | 6 +++---
.../{feature => features}/seemore/ui/SeeMoreViewTest.kt | 6 +++---
.../apps/{feature => features}/seemore/ui/SeeMoreView.kt | 6 +++---
.../{feature => features}/seemore/ui/SeeMoreViewModel.kt | 4 ++--
.../seemore/ui/components/SeeMoreErrorScreen.kt | 2 +-
.../seemore/ui/components/SeeMoreScreen.kt | 2 +-
.../seemore/ui/navigation/SeeMoreNavigation.kt | 4 ++--
.../seemore/ui/viewmodel/SeeMoreViewModelTest.kt | 6 +++---
18 files changed, 34 insertions(+), 35 deletions(-)
rename features/detail/ui/src/androidTest/kotlin/com/manuelnunez/apps/{feature => features}/detail/ui/DetailViewTest.kt (89%)
rename features/detail/ui/src/main/kotlin/com/manuelnunez/apps/{feature => features}/detail/ui/DetailView.kt (58%)
rename features/detail/ui/src/main/kotlin/com/manuelnunez/apps/{feature => features}/detail/ui/components/DetailErrorScreen.kt (92%)
rename features/detail/ui/src/main/kotlin/com/manuelnunez/apps/{feature => features}/detail/ui/components/DetailScreen.kt (98%)
rename features/detail/ui/src/main/kotlin/com/manuelnunez/apps/{feature => features}/detail/ui/navigation/DetailNavigation.kt (88%)
rename features/seemore/domain/src/main/kotlin/com/manuelnunez/apps/{feature => features}/seemore/domain/repository/SeeMoreRepository.kt (75%)
rename features/seemore/domain/src/main/kotlin/com/manuelnunez/apps/{feature => features}/seemore/domain/usecase/GetAllItemUseCase.kt (81%)
rename features/seemore/ui/src/androidTest/kotlin/com/manuelnunez/apps/{feature => features}/seemore/ui/SeeMoreViewTest.kt (90%)
rename features/seemore/ui/src/main/kotlin/com/manuelnunez/apps/{feature => features}/seemore/ui/SeeMoreView.kt (86%)
rename features/seemore/ui/src/main/kotlin/com/manuelnunez/apps/{feature => features}/seemore/ui/SeeMoreViewModel.kt (88%)
rename features/seemore/ui/src/main/kotlin/com/manuelnunez/apps/{feature => features}/seemore/ui/components/SeeMoreErrorScreen.kt (92%)
rename features/seemore/ui/src/main/kotlin/com/manuelnunez/apps/{feature => features}/seemore/ui/components/SeeMoreScreen.kt (98%)
rename features/seemore/ui/src/main/kotlin/com/manuelnunez/apps/{feature => features}/seemore/ui/navigation/SeeMoreNavigation.kt (83%)
rename features/seemore/ui/src/test/kotlin/com/manuelnunez/apps/{feature => features}/seemore/ui/viewmodel/SeeMoreViewModelTest.kt (87%)
diff --git a/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/di/DataModule.kt b/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/di/DataModule.kt
index 8b7eb99..e3bdfec 100644
--- a/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/di/DataModule.kt
+++ b/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/di/DataModule.kt
@@ -6,8 +6,8 @@ import com.manuelnunez.apps.core.data.datasource.PexelsCatsRemoteDataSource
import com.manuelnunez.apps.core.data.datasource.PexelsCatsRemoteDataSourceImpl
import com.manuelnunez.apps.core.data.repository.HomeRepositoryImpl
import com.manuelnunez.apps.core.data.repository.SeeMoreRepositoryImpl
-import com.manuelnunez.apps.feature.seemore.domain.repository.SeeMoreRepository
import com.manuelnunez.apps.features.home.domain.repository.HomeRepository
+import com.manuelnunez.apps.features.seemore.domain.repository.SeeMoreRepository
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
diff --git a/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/repository/SeeMoreRepositoryImpl.kt b/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/repository/SeeMoreRepositoryImpl.kt
index deb97ba..eba25ce 100644
--- a/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/repository/SeeMoreRepositoryImpl.kt
+++ b/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/repository/SeeMoreRepositoryImpl.kt
@@ -1,7 +1,7 @@
package com.manuelnunez.apps.core.data.repository
import com.manuelnunez.apps.core.data.datasource.PexelsCatsRemoteDataSource
-import com.manuelnunez.apps.feature.seemore.domain.repository.SeeMoreRepository
+import com.manuelnunez.apps.features.seemore.domain.repository.SeeMoreRepository
import javax.inject.Inject
class SeeMoreRepositoryImpl
diff --git a/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/repository/SeeMoreRepositoryTest.kt b/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/repository/SeeMoreRepositoryTest.kt
index cd3567b..b5c0244 100644
--- a/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/repository/SeeMoreRepositoryTest.kt
+++ b/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/repository/SeeMoreRepositoryTest.kt
@@ -3,7 +3,7 @@ package com.manuelnunez.apps.core.data.repository
import androidx.paging.PagingData
import com.manuelnunez.apps.core.data.datasource.PexelsCatsRemoteDataSource
import com.manuelnunez.apps.core.data.utils.mockItems
-import com.manuelnunez.apps.feature.seemore.domain.repository.SeeMoreRepository
+import com.manuelnunez.apps.features.seemore.domain.repository.SeeMoreRepository
import io.mockk.confirmVerified
import io.mockk.every
import io.mockk.mockk
diff --git a/features/detail/ui/src/androidTest/kotlin/com/manuelnunez/apps/feature/detail/ui/DetailViewTest.kt b/features/detail/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/detail/ui/DetailViewTest.kt
similarity index 89%
rename from features/detail/ui/src/androidTest/kotlin/com/manuelnunez/apps/feature/detail/ui/DetailViewTest.kt
rename to features/detail/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/detail/ui/DetailViewTest.kt
index ba45abd..33316cf 100644
--- a/features/detail/ui/src/androidTest/kotlin/com/manuelnunez/apps/feature/detail/ui/DetailViewTest.kt
+++ b/features/detail/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/detail/ui/DetailViewTest.kt
@@ -1,4 +1,4 @@
-package com.manuelnunez.apps.feature.detail.ui
+package com.manuelnunez.apps.features.detail.ui
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.assertHasClickAction
@@ -7,9 +7,8 @@ import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import com.manuelnunez.apps.core.domain.model.Item
-import com.manuelnunez.apps.feature.detail.ui.components.DetailErrorScreen
-import com.manuelnunez.apps.feature.detail.ui.components.DetailScreen
-import com.manuelnunez.apps.features.detail.ui.R
+import com.manuelnunez.apps.features.detail.ui.components.DetailErrorScreen
+import com.manuelnunez.apps.features.detail.ui.components.DetailScreen
import org.junit.Rule
import org.junit.Test
import com.manuelnunez.apps.core.ui.R as RCU
diff --git a/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/feature/detail/ui/DetailView.kt b/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/features/detail/ui/DetailView.kt
similarity index 58%
rename from features/detail/ui/src/main/kotlin/com/manuelnunez/apps/feature/detail/ui/DetailView.kt
rename to features/detail/ui/src/main/kotlin/com/manuelnunez/apps/features/detail/ui/DetailView.kt
index 4c52df9..61e3747 100644
--- a/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/feature/detail/ui/DetailView.kt
+++ b/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/features/detail/ui/DetailView.kt
@@ -1,9 +1,9 @@
-package com.manuelnunez.apps.feature.detail.ui
+package com.manuelnunez.apps.features.detail.ui
import androidx.compose.runtime.Composable
import com.manuelnunez.apps.core.domain.model.Item
-import com.manuelnunez.apps.feature.detail.ui.components.DetailErrorScreen
-import com.manuelnunez.apps.feature.detail.ui.components.DetailScreen
+import com.manuelnunez.apps.features.detail.ui.components.DetailErrorScreen
+import com.manuelnunez.apps.features.detail.ui.components.DetailScreen
@Composable
fun DetailView(onBackClick: () -> Unit, item: Item?) {
diff --git a/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/feature/detail/ui/components/DetailErrorScreen.kt b/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/features/detail/ui/components/DetailErrorScreen.kt
similarity index 92%
rename from features/detail/ui/src/main/kotlin/com/manuelnunez/apps/feature/detail/ui/components/DetailErrorScreen.kt
rename to features/detail/ui/src/main/kotlin/com/manuelnunez/apps/features/detail/ui/components/DetailErrorScreen.kt
index d3efe87..436709f 100644
--- a/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/feature/detail/ui/components/DetailErrorScreen.kt
+++ b/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/features/detail/ui/components/DetailErrorScreen.kt
@@ -1,4 +1,4 @@
-package com.manuelnunez.apps.feature.detail.ui.components
+package com.manuelnunez.apps.features.detail.ui.components
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
diff --git a/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/feature/detail/ui/components/DetailScreen.kt b/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/features/detail/ui/components/DetailScreen.kt
similarity index 98%
rename from features/detail/ui/src/main/kotlin/com/manuelnunez/apps/feature/detail/ui/components/DetailScreen.kt
rename to features/detail/ui/src/main/kotlin/com/manuelnunez/apps/features/detail/ui/components/DetailScreen.kt
index f200c72..a5a46f9 100644
--- a/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/feature/detail/ui/components/DetailScreen.kt
+++ b/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/features/detail/ui/components/DetailScreen.kt
@@ -1,4 +1,4 @@
-package com.manuelnunez.apps.feature.detail.ui.components
+package com.manuelnunez.apps.features.detail.ui.components
import android.content.Intent
import android.content.res.Configuration
diff --git a/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/feature/detail/ui/navigation/DetailNavigation.kt b/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/features/detail/ui/navigation/DetailNavigation.kt
similarity index 88%
rename from features/detail/ui/src/main/kotlin/com/manuelnunez/apps/feature/detail/ui/navigation/DetailNavigation.kt
rename to features/detail/ui/src/main/kotlin/com/manuelnunez/apps/features/detail/ui/navigation/DetailNavigation.kt
index 1758f3b..2213ae3 100644
--- a/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/feature/detail/ui/navigation/DetailNavigation.kt
+++ b/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/features/detail/ui/navigation/DetailNavigation.kt
@@ -1,4 +1,4 @@
-package com.manuelnunez.apps.feature.detail.ui.navigation
+package com.manuelnunez.apps.features.detail.ui.navigation
import android.os.Bundle
import androidx.navigation.NavController
@@ -8,7 +8,7 @@ import androidx.navigation.compose.composable
import androidx.navigation.navOptions
import com.manuelnunez.apps.core.common.navigation.navigate
import com.manuelnunez.apps.core.domain.model.Item
-import com.manuelnunez.apps.feature.detail.ui.DetailView
+import com.manuelnunez.apps.features.detail.ui.DetailView
const val DETAIL_ITEM = "myItem"
const val DETAIL_ROUTE = "detail"
diff --git a/features/seemore/domain/src/main/kotlin/com/manuelnunez/apps/feature/seemore/domain/repository/SeeMoreRepository.kt b/features/seemore/domain/src/main/kotlin/com/manuelnunez/apps/features/seemore/domain/repository/SeeMoreRepository.kt
similarity index 75%
rename from features/seemore/domain/src/main/kotlin/com/manuelnunez/apps/feature/seemore/domain/repository/SeeMoreRepository.kt
rename to features/seemore/domain/src/main/kotlin/com/manuelnunez/apps/features/seemore/domain/repository/SeeMoreRepository.kt
index 45a41f7..877e07f 100644
--- a/features/seemore/domain/src/main/kotlin/com/manuelnunez/apps/feature/seemore/domain/repository/SeeMoreRepository.kt
+++ b/features/seemore/domain/src/main/kotlin/com/manuelnunez/apps/features/seemore/domain/repository/SeeMoreRepository.kt
@@ -1,4 +1,4 @@
-package com.manuelnunez.apps.feature.seemore.domain.repository
+package com.manuelnunez.apps.features.seemore.domain.repository
import androidx.paging.PagingData
import com.manuelnunez.apps.core.domain.model.Item
diff --git a/features/seemore/domain/src/main/kotlin/com/manuelnunez/apps/feature/seemore/domain/usecase/GetAllItemUseCase.kt b/features/seemore/domain/src/main/kotlin/com/manuelnunez/apps/features/seemore/domain/usecase/GetAllItemUseCase.kt
similarity index 81%
rename from features/seemore/domain/src/main/kotlin/com/manuelnunez/apps/feature/seemore/domain/usecase/GetAllItemUseCase.kt
rename to features/seemore/domain/src/main/kotlin/com/manuelnunez/apps/features/seemore/domain/usecase/GetAllItemUseCase.kt
index 20cad66..2f6f4c5 100644
--- a/features/seemore/domain/src/main/kotlin/com/manuelnunez/apps/feature/seemore/domain/usecase/GetAllItemUseCase.kt
+++ b/features/seemore/domain/src/main/kotlin/com/manuelnunez/apps/features/seemore/domain/usecase/GetAllItemUseCase.kt
@@ -1,10 +1,10 @@
-package com.manuelnunez.apps.feature.seemore.domain.usecase
+package com.manuelnunez.apps.features.seemore.domain.usecase
import androidx.paging.PagingData
import com.manuelnunez.apps.core.common.dispatcher.CoroutineDispatcherProvider
import com.manuelnunez.apps.core.domain.model.Item
import com.manuelnunez.apps.core.domain.usecase.FlowUseCase
-import com.manuelnunez.apps.feature.seemore.domain.repository.SeeMoreRepository
+import com.manuelnunez.apps.features.seemore.domain.repository.SeeMoreRepository
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
diff --git a/features/seemore/domain/src/test/kotlin/com/manuelnunez/apps/feature/seemore/domain/GetAllItemUseCaseTest.kt b/features/seemore/domain/src/test/kotlin/com/manuelnunez/apps/feature/seemore/domain/GetAllItemUseCaseTest.kt
index d97e480..789a4d2 100644
--- a/features/seemore/domain/src/test/kotlin/com/manuelnunez/apps/feature/seemore/domain/GetAllItemUseCaseTest.kt
+++ b/features/seemore/domain/src/test/kotlin/com/manuelnunez/apps/feature/seemore/domain/GetAllItemUseCaseTest.kt
@@ -1,11 +1,11 @@
-package com.manuelnunez.apps.feature.seemore.domain
+package com.manuelnunez.apps.features.seemore.domain
import androidx.paging.PagingData
import app.cash.turbine.test
import com.manuelnunez.apps.core.common.test.MockkAllRule
import com.manuelnunez.apps.core.common.test.UnMockkAllRule
-import com.manuelnunez.apps.feature.seemore.domain.repository.SeeMoreRepository
-import com.manuelnunez.apps.feature.seemore.domain.usecase.GetAllItemUseCase
+import com.manuelnunez.apps.features.seemore.domain.repository.SeeMoreRepository
+import com.manuelnunez.apps.features.seemore.domain.usecase.GetAllItemUseCase
import io.mockk.confirmVerified
import io.mockk.every
import io.mockk.mockk
diff --git a/features/seemore/ui/src/androidTest/kotlin/com/manuelnunez/apps/feature/seemore/ui/SeeMoreViewTest.kt b/features/seemore/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/seemore/ui/SeeMoreViewTest.kt
similarity index 90%
rename from features/seemore/ui/src/androidTest/kotlin/com/manuelnunez/apps/feature/seemore/ui/SeeMoreViewTest.kt
rename to features/seemore/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/seemore/ui/SeeMoreViewTest.kt
index 95d9f2b..76cfbfa 100644
--- a/features/seemore/ui/src/androidTest/kotlin/com/manuelnunez/apps/feature/seemore/ui/SeeMoreViewTest.kt
+++ b/features/seemore/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/seemore/ui/SeeMoreViewTest.kt
@@ -1,4 +1,4 @@
-package com.manuelnunez.apps.feature.seemore.ui
+package com.manuelnunez.apps.features.seemore.ui
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.assertHasClickAction
@@ -8,8 +8,8 @@ import androidx.compose.ui.test.onNodeWithText
import androidx.paging.PagingData
import androidx.paging.compose.collectAsLazyPagingItems
import com.manuelnunez.apps.core.domain.model.Item
-import com.manuelnunez.apps.feature.seemore.ui.components.SeeMoreErrorScreen
-import com.manuelnunez.apps.feature.seemore.ui.components.SeeMoreScreen
+import com.manuelnunez.apps.features.seemore.ui.components.SeeMoreErrorScreen
+import com.manuelnunez.apps.features.seemore.ui.components.SeeMoreScreen
import kotlinx.coroutines.flow.flowOf
import org.junit.Rule
import org.junit.Test
diff --git a/features/seemore/ui/src/main/kotlin/com/manuelnunez/apps/feature/seemore/ui/SeeMoreView.kt b/features/seemore/ui/src/main/kotlin/com/manuelnunez/apps/features/seemore/ui/SeeMoreView.kt
similarity index 86%
rename from features/seemore/ui/src/main/kotlin/com/manuelnunez/apps/feature/seemore/ui/SeeMoreView.kt
rename to features/seemore/ui/src/main/kotlin/com/manuelnunez/apps/features/seemore/ui/SeeMoreView.kt
index 5e73ec2..4d569b6 100644
--- a/features/seemore/ui/src/main/kotlin/com/manuelnunez/apps/feature/seemore/ui/SeeMoreView.kt
+++ b/features/seemore/ui/src/main/kotlin/com/manuelnunez/apps/features/seemore/ui/SeeMoreView.kt
@@ -1,4 +1,4 @@
-package com.manuelnunez.apps.feature.seemore.ui
+package com.manuelnunez.apps.features.seemore.ui
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
@@ -10,8 +10,8 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.paging.LoadState
import androidx.paging.compose.collectAsLazyPagingItems
import com.manuelnunez.apps.core.domain.model.Item
-import com.manuelnunez.apps.feature.seemore.ui.components.SeeMoreErrorScreen
-import com.manuelnunez.apps.feature.seemore.ui.components.SeeMoreScreen
+import com.manuelnunez.apps.features.seemore.ui.components.SeeMoreErrorScreen
+import com.manuelnunez.apps.features.seemore.ui.components.SeeMoreScreen
@Composable
fun SeeMoreView(
diff --git a/features/seemore/ui/src/main/kotlin/com/manuelnunez/apps/feature/seemore/ui/SeeMoreViewModel.kt b/features/seemore/ui/src/main/kotlin/com/manuelnunez/apps/features/seemore/ui/SeeMoreViewModel.kt
similarity index 88%
rename from features/seemore/ui/src/main/kotlin/com/manuelnunez/apps/feature/seemore/ui/SeeMoreViewModel.kt
rename to features/seemore/ui/src/main/kotlin/com/manuelnunez/apps/features/seemore/ui/SeeMoreViewModel.kt
index 875d168..b166f36 100644
--- a/features/seemore/ui/src/main/kotlin/com/manuelnunez/apps/feature/seemore/ui/SeeMoreViewModel.kt
+++ b/features/seemore/ui/src/main/kotlin/com/manuelnunez/apps/features/seemore/ui/SeeMoreViewModel.kt
@@ -1,11 +1,11 @@
-package com.manuelnunez.apps.feature.seemore.ui
+package com.manuelnunez.apps.features.seemore.ui
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.PagingData
import androidx.paging.cachedIn
import com.manuelnunez.apps.core.domain.model.Item
-import com.manuelnunez.apps.feature.seemore.domain.usecase.GetAllItemUseCase
+import com.manuelnunez.apps.features.seemore.domain.usecase.GetAllItemUseCase
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
diff --git a/features/seemore/ui/src/main/kotlin/com/manuelnunez/apps/feature/seemore/ui/components/SeeMoreErrorScreen.kt b/features/seemore/ui/src/main/kotlin/com/manuelnunez/apps/features/seemore/ui/components/SeeMoreErrorScreen.kt
similarity index 92%
rename from features/seemore/ui/src/main/kotlin/com/manuelnunez/apps/feature/seemore/ui/components/SeeMoreErrorScreen.kt
rename to features/seemore/ui/src/main/kotlin/com/manuelnunez/apps/features/seemore/ui/components/SeeMoreErrorScreen.kt
index ea9f228..a0784f3 100644
--- a/features/seemore/ui/src/main/kotlin/com/manuelnunez/apps/feature/seemore/ui/components/SeeMoreErrorScreen.kt
+++ b/features/seemore/ui/src/main/kotlin/com/manuelnunez/apps/features/seemore/ui/components/SeeMoreErrorScreen.kt
@@ -1,4 +1,4 @@
-package com.manuelnunez.apps.feature.seemore.ui.components
+package com.manuelnunez.apps.features.seemore.ui.components
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
diff --git a/features/seemore/ui/src/main/kotlin/com/manuelnunez/apps/feature/seemore/ui/components/SeeMoreScreen.kt b/features/seemore/ui/src/main/kotlin/com/manuelnunez/apps/features/seemore/ui/components/SeeMoreScreen.kt
similarity index 98%
rename from features/seemore/ui/src/main/kotlin/com/manuelnunez/apps/feature/seemore/ui/components/SeeMoreScreen.kt
rename to features/seemore/ui/src/main/kotlin/com/manuelnunez/apps/features/seemore/ui/components/SeeMoreScreen.kt
index bc18564..6b9fde0 100644
--- a/features/seemore/ui/src/main/kotlin/com/manuelnunez/apps/feature/seemore/ui/components/SeeMoreScreen.kt
+++ b/features/seemore/ui/src/main/kotlin/com/manuelnunez/apps/features/seemore/ui/components/SeeMoreScreen.kt
@@ -1,4 +1,4 @@
-package com.manuelnunez.apps.feature.seemore.ui.components
+package com.manuelnunez.apps.features.seemore.ui.components
import android.content.res.Configuration
import androidx.compose.foundation.layout.Arrangement
diff --git a/features/seemore/ui/src/main/kotlin/com/manuelnunez/apps/feature/seemore/ui/navigation/SeeMoreNavigation.kt b/features/seemore/ui/src/main/kotlin/com/manuelnunez/apps/features/seemore/ui/navigation/SeeMoreNavigation.kt
similarity index 83%
rename from features/seemore/ui/src/main/kotlin/com/manuelnunez/apps/feature/seemore/ui/navigation/SeeMoreNavigation.kt
rename to features/seemore/ui/src/main/kotlin/com/manuelnunez/apps/features/seemore/ui/navigation/SeeMoreNavigation.kt
index caa8786..85b420a 100644
--- a/features/seemore/ui/src/main/kotlin/com/manuelnunez/apps/feature/seemore/ui/navigation/SeeMoreNavigation.kt
+++ b/features/seemore/ui/src/main/kotlin/com/manuelnunez/apps/features/seemore/ui/navigation/SeeMoreNavigation.kt
@@ -1,11 +1,11 @@
-package com.manuelnunez.apps.feature.seemore.ui.navigation
+package com.manuelnunez.apps.features.seemore.ui.navigation
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptionsBuilder
import androidx.navigation.compose.composable
import com.manuelnunez.apps.core.domain.model.Item
-import com.manuelnunez.apps.feature.seemore.ui.SeeMoreView
+import com.manuelnunez.apps.features.seemore.ui.SeeMoreView
const val SEE_MORE_ROUTE = "see_more"
diff --git a/features/seemore/ui/src/test/kotlin/com/manuelnunez/apps/feature/seemore/ui/viewmodel/SeeMoreViewModelTest.kt b/features/seemore/ui/src/test/kotlin/com/manuelnunez/apps/features/seemore/ui/viewmodel/SeeMoreViewModelTest.kt
similarity index 87%
rename from features/seemore/ui/src/test/kotlin/com/manuelnunez/apps/feature/seemore/ui/viewmodel/SeeMoreViewModelTest.kt
rename to features/seemore/ui/src/test/kotlin/com/manuelnunez/apps/features/seemore/ui/viewmodel/SeeMoreViewModelTest.kt
index f1f8090..b77bb3b 100644
--- a/features/seemore/ui/src/test/kotlin/com/manuelnunez/apps/feature/seemore/ui/viewmodel/SeeMoreViewModelTest.kt
+++ b/features/seemore/ui/src/test/kotlin/com/manuelnunez/apps/features/seemore/ui/viewmodel/SeeMoreViewModelTest.kt
@@ -1,11 +1,11 @@
-package com.manuelnunez.apps.feature.seemore.ui.viewmodel
+package com.manuelnunez.apps.features.seemore.ui.viewmodel
import androidx.paging.PagingData
import com.manuelnunez.apps.core.common.test.MockkAllRule
import com.manuelnunez.apps.core.common.test.UnMockkAllRule
import com.manuelnunez.apps.core.domain.model.Item
-import com.manuelnunez.apps.feature.seemore.domain.usecase.GetAllItemUseCase
-import com.manuelnunez.apps.feature.seemore.ui.SeeMoreViewModel
+import com.manuelnunez.apps.features.seemore.domain.usecase.GetAllItemUseCase
+import com.manuelnunez.apps.features.seemore.ui.SeeMoreViewModel
import io.mockk.confirmVerified
import io.mockk.every
import io.mockk.mockk
From 987a03864dad88b07570e0fdbc9b1249f93633ee Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Manuel=20Nu=C3=B1ez?= <03.manu@gmail.com>
Date: Wed, 17 Apr 2024 20:16:47 -0400
Subject: [PATCH 5/8] Added logic for favorites data store #48
---
core/data/build.gradle.kts | 7 ++-
.../datasource/local/FavoritesDataSource.kt | 46 ++++++++++++++++
.../CataasCatsRemoteDataSource.kt | 4 +-
.../PexeelsCatsRemoteDataSource.kt | 4 +-
.../paging/CataasCatsPagingSource.kt | 2 +-
.../paging/PexeelsCatsPagingSource.kt | 2 +-
.../apps/core/data/di/DataModule.kt | 16 ++++--
.../repository/FavoritesRepositoryImpl.kt | 21 ++++++++
.../data/repository/HomeRepositoryImpl.kt | 2 +-
.../data/repository/SeeMoreRepositoryImpl.kt | 2 +-
.../CataasCatsRemoteDataSourceTest.kt | 2 +
.../PexelsCatsRemoteDataSourceTest.kt | 2 +
.../data/repository/HomeRepositoryTest.kt | 2 +-
.../data/repository/SeeMoreRepositoryTest.kt | 2 +-
core/datastore-proto/.gitignore | 1 +
core/datastore-proto/build.gradle.kts | 42 +++++++++++++++
.../proto/serializer/ItemListSerializer.kt | 21 ++++++++
.../proto/serializer/di/DataStoreModule.kt | 32 +++++++++++
.../datastore-proto/src/main/proto/item.proto | 15 ++++++
.../domain/repository/FavoritesRepository.kt | 12 +++++
.../domain/usecase/GetFavoritesUseCase.kt | 18 +++++++
.../domain/usecase/RemoveFavoritesUseCase.kt | 21 ++++++++
.../domain/usecase/SaveFavoritesUseCase.kt | 21 ++++++++
features/favorites/ui/build.gradle.kts | 1 +
.../feature/favorites/ui/FavoritesView.kt | 39 +++++++++++++-
.../favorites/ui/FavoritesViewModel.kt | 54 +++++++++++++++++++
gradle/libs.versions.toml | 7 +++
settings.gradle.kts | 2 +
28 files changed, 383 insertions(+), 17 deletions(-)
create mode 100644 core/data/src/main/kotlin/com/manuelnunez/apps/core/data/datasource/local/FavoritesDataSource.kt
rename core/data/src/main/kotlin/com/manuelnunez/apps/core/data/datasource/{ => remote}/CataasCatsRemoteDataSource.kt (92%)
rename core/data/src/main/kotlin/com/manuelnunez/apps/core/data/datasource/{ => remote}/PexeelsCatsRemoteDataSource.kt (92%)
rename core/data/src/main/kotlin/com/manuelnunez/apps/core/data/datasource/{ => remote}/paging/CataasCatsPagingSource.kt (96%)
rename core/data/src/main/kotlin/com/manuelnunez/apps/core/data/datasource/{ => remote}/paging/PexeelsCatsPagingSource.kt (96%)
create mode 100644 core/data/src/main/kotlin/com/manuelnunez/apps/core/data/repository/FavoritesRepositoryImpl.kt
create mode 100644 core/datastore-proto/.gitignore
create mode 100644 core/datastore-proto/build.gradle.kts
create mode 100644 core/datastore-proto/src/main/kotlin/com/manuelnunez/apps/core/datastore/proto/serializer/ItemListSerializer.kt
create mode 100644 core/datastore-proto/src/main/kotlin/com/manuelnunez/apps/core/datastore/proto/serializer/di/DataStoreModule.kt
create mode 100644 core/datastore-proto/src/main/proto/item.proto
create mode 100644 features/favorites/domain/src/main/kotlin/com/manuelnunez/apps/features/favorites/domain/repository/FavoritesRepository.kt
create mode 100644 features/favorites/domain/src/main/kotlin/com/manuelnunez/apps/features/favorites/domain/usecase/GetFavoritesUseCase.kt
create mode 100644 features/favorites/domain/src/main/kotlin/com/manuelnunez/apps/features/favorites/domain/usecase/RemoveFavoritesUseCase.kt
create mode 100644 features/favorites/domain/src/main/kotlin/com/manuelnunez/apps/features/favorites/domain/usecase/SaveFavoritesUseCase.kt
create mode 100644 features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/feature/favorites/ui/FavoritesViewModel.kt
diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts
index a8cf0be..718bf96 100644
--- a/core/data/build.gradle.kts
+++ b/core/data/build.gradle.kts
@@ -26,15 +26,20 @@ dependencies {
implementation(projects.core.common)
implementation(projects.core.services)
implementation(projects.core.domain)
+ implementation(projects.core.datastoreProto)
implementation(projects.features.home.domain)
implementation(projects.features.seemore.domain)
+ implementation(projects.features.favorites.domain)
+
+ implementation(libs.androidx.dataStore.core)
+ implementation(libs.protobuf.kotlin.lite)
implementation(libs.retrofit.core)
implementation(libs.retrofit.gsonConverter)
+ implementation(libs.androidx.paging.common.ktx)
// HILT
implementation(libs.hilt.android)
- implementation(libs.androidx.paging.common.ktx)
ksp(libs.hilt.compiler)
testImplementation(libs.kotlinx.coroutines.test)
diff --git a/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/datasource/local/FavoritesDataSource.kt b/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/datasource/local/FavoritesDataSource.kt
new file mode 100644
index 0000000..76c4d16
--- /dev/null
+++ b/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/datasource/local/FavoritesDataSource.kt
@@ -0,0 +1,46 @@
+package com.manuelnunez.apps.core.data.datasource.local
+
+import androidx.datastore.core.DataStore
+import com.manuelnunez.apps.core.datastore.proto.Item
+import com.manuelnunez.apps.core.datastore.proto.ItemList
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import javax.inject.Inject
+import com.manuelnunez.apps.core.domain.model.Item as ItemModel
+
+class FavoritesDataSource @Inject constructor(private val itemDataStore: DataStore) {
+ val favorites: Flow
> =
+ itemDataStore.data.map {
+ it.itemsList.map { item ->
+ ItemModel(item.photoId, item.imageUrl, item.thumbnailUrl, item.description)
+ }
+ }
+
+ suspend fun addItemToFavorites(newItem: ItemModel) {
+ itemDataStore.updateData { itemList ->
+ val updatedItems =
+ itemList
+ .toBuilder()
+ .addItems(
+ Item.newBuilder()
+ .setPhotoId(newItem.photoId)
+ .setImageUrl(newItem.imageUrl)
+ .setThumbnailUrl(newItem.thumbnailUrl)
+ .setDescription(newItem.description)
+ .build())
+ .build()
+ updatedItems
+ }
+ }
+
+ suspend fun removeItemFromFavorites(itemPhotoId: String) {
+ itemDataStore.updateData { itemList ->
+ val updatedItems =
+ itemList
+ .toBuilder()
+ .removeItems(itemList.itemsList.indexOfFirst { it.photoId == itemPhotoId })
+ .build()
+ updatedItems
+ }
+ }
+}
diff --git a/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/datasource/CataasCatsRemoteDataSource.kt b/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/datasource/remote/CataasCatsRemoteDataSource.kt
similarity index 92%
rename from core/data/src/main/kotlin/com/manuelnunez/apps/core/data/datasource/CataasCatsRemoteDataSource.kt
rename to core/data/src/main/kotlin/com/manuelnunez/apps/core/data/datasource/remote/CataasCatsRemoteDataSource.kt
index d14fd19..cc687e7 100644
--- a/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/datasource/CataasCatsRemoteDataSource.kt
+++ b/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/datasource/remote/CataasCatsRemoteDataSource.kt
@@ -1,4 +1,4 @@
-package com.manuelnunez.apps.core.data.datasource
+package com.manuelnunez.apps.core.data.datasource.remote
import androidx.paging.Pager
import androidx.paging.PagingConfig
@@ -9,7 +9,7 @@ import com.manuelnunez.apps.core.common.eitherSuccess
import com.manuelnunez.apps.core.common.fold
import com.manuelnunez.apps.core.data.PAGE_SIZE
import com.manuelnunez.apps.core.data.PREFETCH_DISTANCE
-import com.manuelnunez.apps.core.data.datasource.paging.CataasCatsPagingSource
+import com.manuelnunez.apps.core.data.datasource.remote.paging.CataasCatsPagingSource
import com.manuelnunez.apps.core.domain.model.Item
import com.manuelnunez.apps.core.services.dto.CataasResponseDTO
import com.manuelnunez.apps.core.services.executors.RetrofitServiceRequest
diff --git a/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/datasource/PexeelsCatsRemoteDataSource.kt b/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/datasource/remote/PexeelsCatsRemoteDataSource.kt
similarity index 92%
rename from core/data/src/main/kotlin/com/manuelnunez/apps/core/data/datasource/PexeelsCatsRemoteDataSource.kt
rename to core/data/src/main/kotlin/com/manuelnunez/apps/core/data/datasource/remote/PexeelsCatsRemoteDataSource.kt
index d3ef3b7..811b1f5 100644
--- a/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/datasource/PexeelsCatsRemoteDataSource.kt
+++ b/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/datasource/remote/PexeelsCatsRemoteDataSource.kt
@@ -1,4 +1,4 @@
-package com.manuelnunez.apps.core.data.datasource
+package com.manuelnunez.apps.core.data.datasource.remote
import androidx.paging.Pager
import androidx.paging.PagingConfig
@@ -9,7 +9,7 @@ import com.manuelnunez.apps.core.common.eitherSuccess
import com.manuelnunez.apps.core.common.fold
import com.manuelnunez.apps.core.data.PAGE_SIZE
import com.manuelnunez.apps.core.data.PREFETCH_DISTANCE
-import com.manuelnunez.apps.core.data.datasource.paging.PexeelsCatsPagingSource
+import com.manuelnunez.apps.core.data.datasource.remote.paging.PexeelsCatsPagingSource
import com.manuelnunez.apps.core.domain.model.Item
import com.manuelnunez.apps.core.services.dto.PexelsSearchResponseDTO
import com.manuelnunez.apps.core.services.executors.RetrofitServiceRequest
diff --git a/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/datasource/paging/CataasCatsPagingSource.kt b/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/datasource/remote/paging/CataasCatsPagingSource.kt
similarity index 96%
rename from core/data/src/main/kotlin/com/manuelnunez/apps/core/data/datasource/paging/CataasCatsPagingSource.kt
rename to core/data/src/main/kotlin/com/manuelnunez/apps/core/data/datasource/remote/paging/CataasCatsPagingSource.kt
index 6334690..8ceae34 100644
--- a/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/datasource/paging/CataasCatsPagingSource.kt
+++ b/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/datasource/remote/paging/CataasCatsPagingSource.kt
@@ -1,4 +1,4 @@
-package com.manuelnunez.apps.core.data.datasource.paging
+package com.manuelnunez.apps.core.data.datasource.remote.paging
import androidx.paging.PagingSource
import androidx.paging.PagingState
diff --git a/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/datasource/paging/PexeelsCatsPagingSource.kt b/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/datasource/remote/paging/PexeelsCatsPagingSource.kt
similarity index 96%
rename from core/data/src/main/kotlin/com/manuelnunez/apps/core/data/datasource/paging/PexeelsCatsPagingSource.kt
rename to core/data/src/main/kotlin/com/manuelnunez/apps/core/data/datasource/remote/paging/PexeelsCatsPagingSource.kt
index eb20397..1ebbeba 100644
--- a/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/datasource/paging/PexeelsCatsPagingSource.kt
+++ b/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/datasource/remote/paging/PexeelsCatsPagingSource.kt
@@ -1,4 +1,4 @@
-package com.manuelnunez.apps.core.data.datasource.paging
+package com.manuelnunez.apps.core.data.datasource.remote.paging
import androidx.paging.PagingSource
import androidx.paging.PagingState
diff --git a/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/di/DataModule.kt b/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/di/DataModule.kt
index e3bdfec..fd061d9 100644
--- a/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/di/DataModule.kt
+++ b/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/di/DataModule.kt
@@ -1,11 +1,13 @@
package com.manuelnunez.apps.core.data.di
-import com.manuelnunez.apps.core.data.datasource.CataasCatsRemoteDataSource
-import com.manuelnunez.apps.core.data.datasource.CataasCatsRemoteDataSourceImpl
-import com.manuelnunez.apps.core.data.datasource.PexelsCatsRemoteDataSource
-import com.manuelnunez.apps.core.data.datasource.PexelsCatsRemoteDataSourceImpl
+import com.manuelnunez.apps.core.data.datasource.remote.CataasCatsRemoteDataSource
+import com.manuelnunez.apps.core.data.datasource.remote.CataasCatsRemoteDataSourceImpl
+import com.manuelnunez.apps.core.data.datasource.remote.PexelsCatsRemoteDataSource
+import com.manuelnunez.apps.core.data.datasource.remote.PexelsCatsRemoteDataSourceImpl
+import com.manuelnunez.apps.core.data.repository.FavoritesRepositoryImpl
import com.manuelnunez.apps.core.data.repository.HomeRepositoryImpl
import com.manuelnunez.apps.core.data.repository.SeeMoreRepositoryImpl
+import com.manuelnunez.apps.features.favorites.domain.repository.FavoritesRepository
import com.manuelnunez.apps.features.home.domain.repository.HomeRepository
import com.manuelnunez.apps.features.seemore.domain.repository.SeeMoreRepository
import dagger.Binds
@@ -26,6 +28,12 @@ abstract class DataModule {
@Binds
abstract fun bindsSeeMoreRepository(seeMoreRepository: SeeMoreRepositoryImpl): SeeMoreRepository
+ @Singleton
+ @Binds
+ abstract fun bindsFavoritesRepository(
+ seeMoreRepository: FavoritesRepositoryImpl
+ ): FavoritesRepository
+
@Singleton
@Binds
abstract fun providePexelsCatsRemoteDataSource(
diff --git a/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/repository/FavoritesRepositoryImpl.kt b/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/repository/FavoritesRepositoryImpl.kt
new file mode 100644
index 0000000..fa9eb2e
--- /dev/null
+++ b/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/repository/FavoritesRepositoryImpl.kt
@@ -0,0 +1,21 @@
+package com.manuelnunez.apps.core.data.repository
+
+import com.manuelnunez.apps.core.data.datasource.local.FavoritesDataSource
+import com.manuelnunez.apps.core.domain.model.Item
+import com.manuelnunez.apps.features.favorites.domain.repository.FavoritesRepository
+import kotlinx.coroutines.flow.Flow
+import javax.inject.Inject
+
+class FavoritesRepositoryImpl
+@Inject
+constructor(private val favoritesDataSource: FavoritesDataSource) : FavoritesRepository {
+ override fun getAllFavorites(): Flow> = favoritesDataSource.favorites
+
+ override suspend fun saveFavoriteItem(favoriteItem: Item) {
+ favoritesDataSource.addItemToFavorites(favoriteItem)
+ }
+
+ override suspend fun removeFavoriteItem(favoriteItem: Item) {
+ favoritesDataSource.removeItemFromFavorites(favoriteItem.photoId)
+ }
+}
diff --git a/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/repository/HomeRepositoryImpl.kt b/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/repository/HomeRepositoryImpl.kt
index 2177283..2aa3903 100644
--- a/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/repository/HomeRepositoryImpl.kt
+++ b/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/repository/HomeRepositoryImpl.kt
@@ -3,7 +3,7 @@ package com.manuelnunez.apps.core.data.repository
import com.manuelnunez.apps.core.common.eitherError
import com.manuelnunez.apps.core.common.eitherSuccess
import com.manuelnunez.apps.core.common.fold
-import com.manuelnunez.apps.core.data.datasource.PexelsCatsRemoteDataSource
+import com.manuelnunez.apps.core.data.datasource.remote.PexelsCatsRemoteDataSource
import com.manuelnunez.apps.core.data.mapper.toItems
import com.manuelnunez.apps.core.domain.model.ErrorModel
import com.manuelnunez.apps.features.home.domain.repository.HomeRepository
diff --git a/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/repository/SeeMoreRepositoryImpl.kt b/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/repository/SeeMoreRepositoryImpl.kt
index eba25ce..7c2798b 100644
--- a/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/repository/SeeMoreRepositoryImpl.kt
+++ b/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/repository/SeeMoreRepositoryImpl.kt
@@ -1,6 +1,6 @@
package com.manuelnunez.apps.core.data.repository
-import com.manuelnunez.apps.core.data.datasource.PexelsCatsRemoteDataSource
+import com.manuelnunez.apps.core.data.datasource.remote.PexelsCatsRemoteDataSource
import com.manuelnunez.apps.features.seemore.domain.repository.SeeMoreRepository
import javax.inject.Inject
diff --git a/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/datasource/CataasCatsRemoteDataSourceTest.kt b/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/datasource/CataasCatsRemoteDataSourceTest.kt
index 62f7130..73ff980 100644
--- a/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/datasource/CataasCatsRemoteDataSourceTest.kt
+++ b/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/datasource/CataasCatsRemoteDataSourceTest.kt
@@ -6,6 +6,8 @@ import app.cash.turbine.test
import com.manuelnunez.apps.core.common.Either
import com.manuelnunez.apps.core.common.eitherError
import com.manuelnunez.apps.core.common.eitherSuccess
+import com.manuelnunez.apps.core.data.datasource.remote.CataasCatsRemoteDataSource
+import com.manuelnunez.apps.core.data.datasource.remote.CataasCatsRemoteDataSourceImpl
import com.manuelnunez.apps.core.data.mapper.toItems
import com.manuelnunez.apps.core.data.utils.mockCataasResponseDTOS
import com.manuelnunez.apps.core.domain.model.Item
diff --git a/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/datasource/PexelsCatsRemoteDataSourceTest.kt b/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/datasource/PexelsCatsRemoteDataSourceTest.kt
index c56f297..a0f7c0b 100644
--- a/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/datasource/PexelsCatsRemoteDataSourceTest.kt
+++ b/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/datasource/PexelsCatsRemoteDataSourceTest.kt
@@ -6,6 +6,8 @@ import app.cash.turbine.test
import com.manuelnunez.apps.core.common.Either
import com.manuelnunez.apps.core.common.eitherError
import com.manuelnunez.apps.core.common.eitherSuccess
+import com.manuelnunez.apps.core.data.datasource.remote.PexelsCatsRemoteDataSource
+import com.manuelnunez.apps.core.data.datasource.remote.PexelsCatsRemoteDataSourceImpl
import com.manuelnunez.apps.core.data.mapper.toItems
import com.manuelnunez.apps.core.data.utils.mockPexelsSearchResponseDTO
import com.manuelnunez.apps.core.domain.model.Item
diff --git a/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/repository/HomeRepositoryTest.kt b/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/repository/HomeRepositoryTest.kt
index 7542b10..aba354f 100644
--- a/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/repository/HomeRepositoryTest.kt
+++ b/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/repository/HomeRepositoryTest.kt
@@ -4,7 +4,7 @@ import com.manuelnunez.apps.core.common.Either
import com.manuelnunez.apps.core.common.eitherError
import com.manuelnunez.apps.core.common.eitherSuccess
import com.manuelnunez.apps.core.common.fold
-import com.manuelnunez.apps.core.data.datasource.PexelsCatsRemoteDataSource
+import com.manuelnunez.apps.core.data.datasource.remote.PexelsCatsRemoteDataSource
import com.manuelnunez.apps.core.data.utils.mockPexelsSearchResponseDTO
import com.manuelnunez.apps.core.domain.model.ErrorModel
import com.manuelnunez.apps.core.services.executors.ServiceError
diff --git a/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/repository/SeeMoreRepositoryTest.kt b/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/repository/SeeMoreRepositoryTest.kt
index b5c0244..c7e2b07 100644
--- a/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/repository/SeeMoreRepositoryTest.kt
+++ b/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/repository/SeeMoreRepositoryTest.kt
@@ -1,7 +1,7 @@
package com.manuelnunez.apps.core.data.repository
import androidx.paging.PagingData
-import com.manuelnunez.apps.core.data.datasource.PexelsCatsRemoteDataSource
+import com.manuelnunez.apps.core.data.datasource.remote.PexelsCatsRemoteDataSource
import com.manuelnunez.apps.core.data.utils.mockItems
import com.manuelnunez.apps.features.seemore.domain.repository.SeeMoreRepository
import io.mockk.confirmVerified
diff --git a/core/datastore-proto/.gitignore b/core/datastore-proto/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/core/datastore-proto/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/core/datastore-proto/build.gradle.kts b/core/datastore-proto/build.gradle.kts
new file mode 100644
index 0000000..dc35c6c
--- /dev/null
+++ b/core/datastore-proto/build.gradle.kts
@@ -0,0 +1,42 @@
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.protobuf)
+ alias(libs.plugins.ksp)
+}
+
+android {
+ namespace = "com.manuelnunez.apps.core.datastore.proto"
+ compileSdk = 34
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+
+ defaultConfig { minSdk = 21 }
+
+ kotlinOptions { jvmTarget = JavaVersion.VERSION_17.toString() }
+}
+
+// Setup protobuf configuration, generating lite Java and Kotlin classes
+protobuf {
+ protoc { artifact = libs.protobuf.protoc.get().toString() }
+ generateProtoTasks {
+ all().forEach { task ->
+ task.builtins {
+ register("java") { option("lite") }
+ register("kotlin") { option("lite") }
+ }
+ }
+ }
+}
+
+dependencies {
+ implementation(libs.protobuf.kotlin.lite)
+ implementation(libs.androidx.dataStore.core)
+
+ // HILT
+ implementation(libs.hilt.android)
+ ksp(libs.hilt.compiler)
+}
diff --git a/core/datastore-proto/src/main/kotlin/com/manuelnunez/apps/core/datastore/proto/serializer/ItemListSerializer.kt b/core/datastore-proto/src/main/kotlin/com/manuelnunez/apps/core/datastore/proto/serializer/ItemListSerializer.kt
new file mode 100644
index 0000000..336095f
--- /dev/null
+++ b/core/datastore-proto/src/main/kotlin/com/manuelnunez/apps/core/datastore/proto/serializer/ItemListSerializer.kt
@@ -0,0 +1,21 @@
+package com.manuelnunez.apps.core.datastore.proto.serializer
+
+import androidx.datastore.core.Serializer
+import com.google.protobuf.InvalidProtocolBufferException
+import com.manuelnunez.apps.core.datastore.proto.ItemList
+import java.io.InputStream
+import java.io.OutputStream
+
+class ItemListSerializer : Serializer {
+ override val defaultValue: ItemList = ItemList.getDefaultInstance()
+
+ override suspend fun readFrom(input: InputStream): ItemList {
+ try {
+ return ItemList.parseFrom(input)
+ } catch (exception: InvalidProtocolBufferException) {
+ throw IllegalArgumentException("Error parsing ItemList", exception)
+ }
+ }
+
+ override suspend fun writeTo(t: ItemList, output: OutputStream) = t.writeTo(output)
+}
diff --git a/core/datastore-proto/src/main/kotlin/com/manuelnunez/apps/core/datastore/proto/serializer/di/DataStoreModule.kt b/core/datastore-proto/src/main/kotlin/com/manuelnunez/apps/core/datastore/proto/serializer/di/DataStoreModule.kt
new file mode 100644
index 0000000..27f7d39
--- /dev/null
+++ b/core/datastore-proto/src/main/kotlin/com/manuelnunez/apps/core/datastore/proto/serializer/di/DataStoreModule.kt
@@ -0,0 +1,32 @@
+package com.manuelnunez.apps.core.datastore.proto.serializer
+
+import android.content.Context
+import androidx.datastore.core.DataStore
+import androidx.datastore.core.DataStoreFactory
+import androidx.datastore.dataStoreFile
+import com.manuelnunez.apps.core.datastore.proto.ItemList
+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 DataStoreModule {
+
+ @Provides @Singleton internal fun providesItemListSerializer() = ItemListSerializer()
+
+ @Provides
+ @Singleton
+ internal fun providesItemListDataStore(
+ @ApplicationContext context: Context,
+ itemListSerializer: ItemListSerializer,
+ ): DataStore =
+ DataStoreFactory.create(
+ serializer = itemListSerializer,
+ ) {
+ context.dataStoreFile("favorites.pb")
+ }
+}
diff --git a/core/datastore-proto/src/main/proto/item.proto b/core/datastore-proto/src/main/proto/item.proto
new file mode 100644
index 0000000..5fb786d
--- /dev/null
+++ b/core/datastore-proto/src/main/proto/item.proto
@@ -0,0 +1,15 @@
+syntax = "proto3";
+
+option java_package = "com.manuelnunez.apps.core.datastore.proto";
+option java_multiple_files = true;
+
+message ItemList {
+ repeated Item items = 1;
+}
+
+message Item {
+ string photoId = 1;
+ string imageUrl = 2;
+ string thumbnailUrl = 3;
+ string description = 4;
+}
diff --git a/features/favorites/domain/src/main/kotlin/com/manuelnunez/apps/features/favorites/domain/repository/FavoritesRepository.kt b/features/favorites/domain/src/main/kotlin/com/manuelnunez/apps/features/favorites/domain/repository/FavoritesRepository.kt
new file mode 100644
index 0000000..f740942
--- /dev/null
+++ b/features/favorites/domain/src/main/kotlin/com/manuelnunez/apps/features/favorites/domain/repository/FavoritesRepository.kt
@@ -0,0 +1,12 @@
+package com.manuelnunez.apps.features.favorites.domain.repository
+
+import com.manuelnunez.apps.core.domain.model.Item
+import kotlinx.coroutines.flow.Flow
+
+interface FavoritesRepository {
+ fun getAllFavorites(): Flow>
+
+ suspend fun saveFavoriteItem(favoriteItem: Item)
+
+ suspend fun removeFavoriteItem(favoriteItem: Item)
+}
diff --git a/features/favorites/domain/src/main/kotlin/com/manuelnunez/apps/features/favorites/domain/usecase/GetFavoritesUseCase.kt b/features/favorites/domain/src/main/kotlin/com/manuelnunez/apps/features/favorites/domain/usecase/GetFavoritesUseCase.kt
new file mode 100644
index 0000000..c27b49a
--- /dev/null
+++ b/features/favorites/domain/src/main/kotlin/com/manuelnunez/apps/features/favorites/domain/usecase/GetFavoritesUseCase.kt
@@ -0,0 +1,18 @@
+package com.manuelnunez.apps.features.favorites.domain.usecase
+
+import com.manuelnunez.apps.core.common.dispatcher.CoroutineDispatcherProvider
+import com.manuelnunez.apps.core.domain.model.Item
+import com.manuelnunez.apps.core.domain.usecase.FlowUseCase
+import com.manuelnunez.apps.features.favorites.domain.repository.FavoritesRepository
+import kotlinx.coroutines.flow.Flow
+import javax.inject.Inject
+
+class GetFavoritesUseCase
+@Inject
+constructor(
+ private val favoritesRepository: FavoritesRepository,
+ coroutineDispatcherProvider: CoroutineDispatcherProvider
+) : FlowUseCase>(coroutineDispatcherProvider) {
+
+ override fun execute(input: Unit): Flow> = favoritesRepository.getAllFavorites()
+}
diff --git a/features/favorites/domain/src/main/kotlin/com/manuelnunez/apps/features/favorites/domain/usecase/RemoveFavoritesUseCase.kt b/features/favorites/domain/src/main/kotlin/com/manuelnunez/apps/features/favorites/domain/usecase/RemoveFavoritesUseCase.kt
new file mode 100644
index 0000000..42555cf
--- /dev/null
+++ b/features/favorites/domain/src/main/kotlin/com/manuelnunez/apps/features/favorites/domain/usecase/RemoveFavoritesUseCase.kt
@@ -0,0 +1,21 @@
+package com.manuelnunez.apps.features.favorites.domain.usecase
+
+import com.manuelnunez.apps.core.common.dispatcher.CoroutineDispatcherProvider
+import com.manuelnunez.apps.core.domain.model.Item
+import com.manuelnunez.apps.core.domain.usecase.FlowUseCase
+import com.manuelnunez.apps.features.favorites.domain.repository.FavoritesRepository
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+import javax.inject.Inject
+
+class RemoveFavoritesUseCase
+@Inject
+constructor(
+ private val favoritesRepository: FavoritesRepository,
+ coroutineDispatcherProvider: CoroutineDispatcherProvider
+) : FlowUseCase- (coroutineDispatcherProvider) {
+
+ override fun execute(input: Item): Flow = flow {
+ favoritesRepository.removeFavoriteItem(input)
+ }
+}
diff --git a/features/favorites/domain/src/main/kotlin/com/manuelnunez/apps/features/favorites/domain/usecase/SaveFavoritesUseCase.kt b/features/favorites/domain/src/main/kotlin/com/manuelnunez/apps/features/favorites/domain/usecase/SaveFavoritesUseCase.kt
new file mode 100644
index 0000000..10cd282
--- /dev/null
+++ b/features/favorites/domain/src/main/kotlin/com/manuelnunez/apps/features/favorites/domain/usecase/SaveFavoritesUseCase.kt
@@ -0,0 +1,21 @@
+package com.manuelnunez.apps.features.favorites.domain.usecase
+
+import com.manuelnunez.apps.core.common.dispatcher.CoroutineDispatcherProvider
+import com.manuelnunez.apps.core.domain.model.Item
+import com.manuelnunez.apps.core.domain.usecase.FlowUseCase
+import com.manuelnunez.apps.features.favorites.domain.repository.FavoritesRepository
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+import javax.inject.Inject
+
+class SaveFavoritesUseCase
+@Inject
+constructor(
+ private val favoritesRepository: FavoritesRepository,
+ coroutineDispatcherProvider: CoroutineDispatcherProvider
+) : FlowUseCase
- (coroutineDispatcherProvider) {
+
+ override fun execute(input: Item): Flow = flow {
+ favoritesRepository.saveFavoriteItem(input)
+ }
+}
diff --git a/features/favorites/ui/build.gradle.kts b/features/favorites/ui/build.gradle.kts
index 97b9097..34aff0b 100644
--- a/features/favorites/ui/build.gradle.kts
+++ b/features/favorites/ui/build.gradle.kts
@@ -35,6 +35,7 @@ dependencies {
implementation(projects.core.common)
implementation(projects.core.domain)
implementation(projects.core.ui)
+ implementation(projects.features.favorites.domain)
// Arch Components
implementation(libs.androidx.lifecycle.runtime.compose)
diff --git a/features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/feature/favorites/ui/FavoritesView.kt b/features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/feature/favorites/ui/FavoritesView.kt
index 9328950..dcf57ed 100644
--- a/features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/feature/favorites/ui/FavoritesView.kt
+++ b/features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/feature/favorites/ui/FavoritesView.kt
@@ -5,13 +5,48 @@ import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.safeContent
import androidx.compose.foundation.layout.windowInsetsPadding
+import androidx.compose.material3.Button
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.manuelnunez.apps.core.domain.model.Item
import com.manuelnunez.apps.core.ui.component.SurfaceText
+import java.text.SimpleDateFormat
+import java.util.Calendar
@Composable
-fun FavoritesView() {
+fun FavoritesView(viewModel: FavoritesViewModel = hiltViewModel()) {
+ val items by viewModel.favoriteItemsState.collectAsStateWithLifecycle()
Column(Modifier.fillMaxSize().windowInsetsPadding(WindowInsets.safeContent)) {
- SurfaceText(text = "Favoritos")
+ Button(
+ onClick = {
+ val currentDateTime = Calendar.getInstance().time
+
+ // Define a date-time formatter (optional)
+ val formatter = SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
+
+ // Format the current date and time using the formatter (optional)
+ val formattedDateTime = formatter.format(currentDateTime)
+ viewModel.saveFavorite(Item(formattedDateTime, "", "", ""))
+ }) {}
+
+ Button(
+ onClick = {
+ val currentDateTime = Calendar.getInstance().time
+
+ // Define a date-time formatter (optional)
+ val formatter = SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
+
+ // Format the current date and time using the formatter (optional)
+ val formattedDateTime = formatter.format(currentDateTime)
+ viewModel.removeFavorite(Item(formattedDateTime, "", "", ""))
+ }) {}
+ if (items is FavoritesViewModel.FavoriteItemsState.ShowList) {
+ (items as FavoritesViewModel.FavoriteItemsState.ShowList).items.forEach {
+ SurfaceText(text = it.photoId)
+ }
+ }
}
}
diff --git a/features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/feature/favorites/ui/FavoritesViewModel.kt b/features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/feature/favorites/ui/FavoritesViewModel.kt
new file mode 100644
index 0000000..61f140a
--- /dev/null
+++ b/features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/feature/favorites/ui/FavoritesViewModel.kt
@@ -0,0 +1,54 @@
+package com.manuelnunez.apps.feature.favorites.ui
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.manuelnunez.apps.core.domain.model.Item
+import com.manuelnunez.apps.features.favorites.domain.usecase.GetFavoritesUseCase
+import com.manuelnunez.apps.features.favorites.domain.usecase.RemoveFavoritesUseCase
+import com.manuelnunez.apps.features.favorites.domain.usecase.SaveFavoritesUseCase
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.onStart
+import javax.inject.Inject
+
+@HiltViewModel
+class FavoritesViewModel
+@Inject
+constructor(
+ private val getFavoritesUseCase: GetFavoritesUseCase,
+ private val saveFavoritesUseCase: SaveFavoritesUseCase,
+ private val removeFavoritesUseCase: RemoveFavoritesUseCase
+) : ViewModel() {
+
+ val favoriteItemsState = MutableStateFlow(FavoriteItemsState.Idle)
+
+ init {
+ getFavoritesUseCase
+ .prepare(Unit)
+ .onStart { favoriteItemsState.value = FavoriteItemsState.Loading }
+ .onEach { favoriteItemsState.value = FavoriteItemsState.ShowList(it) }
+ .catch { favoriteItemsState.value = FavoriteItemsState.Error }
+ .launchIn(viewModelScope)
+ }
+
+ fun saveFavorite(item: Item) {
+ saveFavoritesUseCase.prepare(item).catch {}.launchIn(viewModelScope)
+ }
+
+ fun removeFavorite(item: Item) {
+ removeFavoritesUseCase.prepare(item).catch {}.launchIn(viewModelScope)
+ }
+
+ sealed interface FavoriteItemsState {
+ data object Idle : FavoriteItemsState
+
+ data object Loading : FavoriteItemsState
+
+ data object Error : FavoriteItemsState
+
+ data class ShowList(val items: List
- ) : FavoriteItemsState
+ }
+}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 2798549..32e209a 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -9,6 +9,7 @@ androidxLifecycle = "2.7.0"
androidxNavigation = "2.7.7"
androidxTestExt = "1.1.5"
androidxTestRunner = "1.5.2"
+androidxDataStore = "1.1.0"
kotlin = "1.9.0"
coreKtx = "1.12.0"
junit = "5.10.0"
@@ -24,6 +25,8 @@ uiTooling = "1.6.5"
mockk = "1.13.10"
turbine = "1.0.0"
pagingCommonKtx = "3.2.1"
+protobufPlugin = "0.9.4"
+protobuf = "3.25.2"
[libraries]
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidxActivity" }
@@ -63,6 +66,9 @@ turbine = { group = "app.cash.turbine", name = "turbine", version.ref = "turbine
androidx-paging-common-ktx = { group = "androidx.paging", name = "paging-common-ktx", version.ref = "pagingCommonKtx" }
androidx-paging-runtime = { group = "androidx.paging", name = "paging-runtime", version.ref = "pagingCommonKtx" }
androidx-paging-compose = { group = "androidx.paging", name = "paging-compose", version.ref = "pagingCommonKtx" }
+protobuf-kotlin-lite = { group = "com.google.protobuf", name = "protobuf-kotlin-lite", version.ref = "protobuf" }
+protobuf-protoc = { group = "com.google.protobuf", name = "protoc", version.ref = "protobuf" }
+androidx-dataStore-core = { group = "androidx.datastore", name = "datastore", version.ref = "androidxDataStore" }
[plugins]
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
@@ -70,3 +76,4 @@ android-library = { id = "com.android.library", version.ref = "androidGradlePlug
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
hilt-gradle = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
+protobuf = { id = "com.google.protobuf", version.ref = "protobufPlugin" }
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 902094e..95086b8 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -49,3 +49,5 @@ include(":features:seemore:domain")
include(":features:favorites:ui")
include(":features:favorites:domain")
+
+include(":core:datastore-proto")
From 7805ba8e81e4bf6314caabbf224c700176ae8e4c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Manuel=20Nu=C3=B1ez?= <03.manu@gmail.com>
Date: Thu, 18 Apr 2024 20:17:07 -0400
Subject: [PATCH 6/8] Added favorite UI #47
---
.../kotlin/com/manuelnunez/apps/MainApp.kt | 120 ++++++++++++++----
.../com/manuelnunez/apps/MainDestination.kt | 51 --------
.../apps/navigation/MainNavigation.kt | 74 +++++++++--
core/data/build.gradle.kts | 1 +
.../datasource/local/FavoritesDataSource.kt | 3 +
.../apps/core/data/di/DataModule.kt | 6 +
.../data/repository/DetailRepositoryImpl.kt | 22 ++++
.../repository/FavoritesRepositoryImpl.kt | 8 --
.../proto/serializer/di/DataStoreModule.kt | 3 +-
core/domain/build.gradle.kts | 3 +-
.../apps/core/domain/model/Item.kt | 20 ++-
.../apps/core/ui/component/BackToolbar.kt | 36 ++++++
core/ui/src/main/res/values/strings.xml | 2 +-
features/detail/domain/.gitignore | 1 +
features/detail/domain/build.gradle.kts | 38 ++++++
.../domain/src/main/AndroidManifest.xml | 4 +
.../domain/repository/DetailRepository.kt | 12 ++
.../usecase/GetFavoriteStatusUseCase.kt | 17 +++
.../domain/usecase/RemoveFavoritesUseCase.kt | 8 +-
.../domain/usecase/SaveFavoritesUseCase.kt | 10 +-
features/detail/ui/build.gradle.kts | 2 +
.../apps/features/detail/ui/DetailViewTest.kt | 2 +-
.../apps/features/detail/ui/DetailRoute.kt | 17 +++
.../apps/features/detail/ui/DetailView.kt | 15 ---
.../features/detail/ui/DetailViewModel.kt | 86 +++++++++++++
.../detail/ui/components/DetailScreen.kt | 98 +++++++++-----
.../detail/ui/navigation/DetailNavigation.kt | 25 ++--
.../detail/ui/src/main/res/values/strings.xml | 2 +-
.../domain/repository/FavoritesRepository.kt | 4 -
.../feature/favorites/ui/FavoritesRoute.kt | 38 ++++++
.../feature/favorites/ui/FavoritesView.kt | 52 --------
.../favorites/ui/FavoritesViewModel.kt | 23 +---
.../ui/component/FavoritesErrorScreen.kt} | 11 +-
.../favorites/ui/component/FavoritesScreen.kt | 68 ++++++++++
.../ui/navigation/FavoritesNavigation.kt | 14 +-
.../ui/src/main/res/values/strings.xml | 4 +
.../apps/features/home/ui/HomeViewTest.kt | 26 ++--
.../home/ui/{HomeView.kt => HomeRoute.kt} | 12 +-
...omeScreenViewModel.kt => HomeViewModel.kt} | 2 +-
.../features/home/ui/components/HomeScreen.kt | 8 +-
.../home/ui/navigation/HomeNavigation.kt | 8 +-
...nViewModelTest.kt => HomeViewModelTest.kt} | 14 +-
.../ui/{SeeMoreView.kt => SeeMoreRoute.kt} | 2 +-
.../seemore/ui/components/SeeMoreScreen.kt | 26 +---
.../ui/navigation/SeeMoreNavigation.kt | 6 +-
gradle/libs.versions.toml | 2 +
settings.gradle.kts | 6 +-
47 files changed, 687 insertions(+), 325 deletions(-)
delete mode 100644 app/src/main/kotlin/com/manuelnunez/apps/MainDestination.kt
create mode 100644 core/data/src/main/kotlin/com/manuelnunez/apps/core/data/repository/DetailRepositoryImpl.kt
create mode 100644 core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/BackToolbar.kt
create mode 100644 features/detail/domain/.gitignore
create mode 100644 features/detail/domain/build.gradle.kts
create mode 100644 features/detail/domain/src/main/AndroidManifest.xml
create mode 100644 features/detail/domain/src/main/kotlin/com/manuelnunez/apps/features/detail/domain/repository/DetailRepository.kt
create mode 100644 features/detail/domain/src/main/kotlin/com/manuelnunez/apps/features/detail/domain/usecase/GetFavoriteStatusUseCase.kt
rename features/{favorites/domain/src/main/kotlin/com/manuelnunez/apps/features/favorites => detail/domain/src/main/kotlin/com/manuelnunez/apps/features/detail}/domain/usecase/RemoveFavoritesUseCase.kt (67%)
rename features/{favorites/domain/src/main/kotlin/com/manuelnunez/apps/features/favorites => detail/domain/src/main/kotlin/com/manuelnunez/apps/features/detail}/domain/usecase/SaveFavoritesUseCase.kt (59%)
create mode 100644 features/detail/ui/src/main/kotlin/com/manuelnunez/apps/features/detail/ui/DetailRoute.kt
delete mode 100644 features/detail/ui/src/main/kotlin/com/manuelnunez/apps/features/detail/ui/DetailView.kt
create mode 100644 features/detail/ui/src/main/kotlin/com/manuelnunez/apps/features/detail/ui/DetailViewModel.kt
create mode 100644 features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/feature/favorites/ui/FavoritesRoute.kt
delete mode 100644 features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/feature/favorites/ui/FavoritesView.kt
rename features/{detail/ui/src/main/kotlin/com/manuelnunez/apps/features/detail/ui/components/DetailErrorScreen.kt => favorites/ui/src/main/kotlin/com/manuelnunez/apps/feature/favorites/ui/component/FavoritesErrorScreen.kt} (60%)
create mode 100644 features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/feature/favorites/ui/component/FavoritesScreen.kt
create mode 100644 features/favorites/ui/src/main/res/values/strings.xml
rename features/home/ui/src/main/kotlin/com/manuelnunez/apps/features/home/ui/{HomeView.kt => HomeRoute.kt} (77%)
rename features/home/ui/src/main/kotlin/com/manuelnunez/apps/features/home/ui/{HomeScreenViewModel.kt => HomeViewModel.kt} (99%)
rename features/home/ui/src/test/kotlin/com/manuelnunez/apps/features/home/ui/viewmodel/{HomeScreenViewModelTest.kt => HomeViewModelTest.kt} (88%)
rename features/seemore/ui/src/main/kotlin/com/manuelnunez/apps/features/seemore/ui/{SeeMoreView.kt => SeeMoreRoute.kt} (98%)
diff --git a/app/src/main/kotlin/com/manuelnunez/apps/MainApp.kt b/app/src/main/kotlin/com/manuelnunez/apps/MainApp.kt
index 4e997f1..67299bf 100644
--- a/app/src/main/kotlin/com/manuelnunez/apps/MainApp.kt
+++ b/app/src/main/kotlin/com/manuelnunez/apps/MainApp.kt
@@ -1,5 +1,6 @@
package com.manuelnunez.apps
+import android.content.res.Configuration
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
@@ -13,47 +14,39 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.saveable.rememberSaveable
-import androidx.compose.runtime.setValue
+import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.stringResource
+import androidx.navigation.NavController
+import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.compose.rememberNavController
import com.manuelnunez.apps.core.ui.component.MainGradientBackground
import com.manuelnunez.apps.core.ui.component.MainNavigationBar
import com.manuelnunez.apps.core.ui.component.MainNavigationBarItem
+import com.manuelnunez.apps.core.ui.component.MainNavigationRail
+import com.manuelnunez.apps.core.ui.component.MainNavigationRailItem
import com.manuelnunez.apps.navigation.MainNavigation
+import com.manuelnunez.apps.navigation.RootScreen
+import com.manuelnunez.apps.navigation.mainDestinations
+import com.manuelnunez.apps.navigation.navigateToRootScreen
@Composable
fun MainApp() {
val navController = rememberNavController()
- var currentMainDestination by rememberSaveable { mutableStateOf(MainDestination.HOME) }
+ val currentSelectedScreen by navController.currentScreenAsState()
+ val shouldShowBottomBar =
+ LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT
Scaffold(
bottomBar = {
- MainNavigationBar {
- mainDestinations().forEach { destination ->
- MainNavigationBarItem(
- selected = currentMainDestination == destination,
- onClick = {
- currentMainDestination = destination
- navController.onNavigateToDestination(destination)
- },
- icon = {
- Icon(
- imageVector = destination.unselectedIcon,
- contentDescription = null,
- )
- },
- selectedIcon = {
- Icon(
- imageVector = destination.selectedIcon,
- contentDescription = null,
- )
- },
- label = { Text(stringResource(destination.iconTextId)) })
- }
+ if (shouldShowBottomBar) {
+ MainBottomNavBar(navController, currentSelectedScreen)
}
}) { paddingValues ->
Row(
@@ -66,7 +59,82 @@ fun MainApp() {
),
),
) {
- MainGradientBackground { MainNavigation(navController = navController) }
+ if (!shouldShowBottomBar) {
+ MainNavRail(navController, currentSelectedScreen)
+ }
+ MainGradientBackground { MainNavigation(navController) }
}
}
}
+
+@Composable
+private fun MainBottomNavBar(navController: NavController, currentSelectedScreen: RootScreen) {
+ MainNavigationBar {
+ mainDestinations().forEach { rootScreen ->
+ MainNavigationBarItem(
+ selected = currentSelectedScreen == rootScreen,
+ onClick = { navController.navigateToRootScreen(rootScreen) },
+ icon = {
+ Icon(
+ imageVector = rootScreen.unselectedIcon,
+ contentDescription = null,
+ )
+ },
+ selectedIcon = {
+ Icon(
+ imageVector = rootScreen.selectedIcon,
+ contentDescription = null,
+ )
+ },
+ label = { Text(stringResource(rootScreen.iconTextId)) })
+ }
+ }
+}
+
+@Composable
+private fun MainNavRail(navController: NavController, currentSelectedScreen: RootScreen) {
+ MainNavigationRail {
+ mainDestinations().forEach { rootScreen ->
+ MainNavigationRailItem(
+ selected = currentSelectedScreen == rootScreen,
+ onClick = { navController.navigateToRootScreen(rootScreen) },
+ icon = {
+ Icon(
+ imageVector = rootScreen.unselectedIcon,
+ contentDescription = null,
+ )
+ },
+ selectedIcon = {
+ Icon(
+ imageVector = rootScreen.selectedIcon,
+ contentDescription = null,
+ )
+ },
+ label = { Text(stringResource(rootScreen.iconTextId)) })
+ }
+ }
+}
+
+@Stable
+@Composable
+private fun NavController.currentScreenAsState(): State {
+ val selectedItem = remember { mutableStateOf(RootScreen.HOME) }
+ DisposableEffect(key1 = this) {
+ val listener =
+ NavController.OnDestinationChangedListener { _, destination, _ ->
+ when {
+ destination.hierarchy.any { it.route == RootScreen.HOME.route } -> {
+ selectedItem.value = RootScreen.HOME
+ }
+ destination.hierarchy.any { it.route == RootScreen.FAVORITES.route } -> {
+ selectedItem.value = RootScreen.FAVORITES
+ }
+ }
+ }
+
+ addOnDestinationChangedListener(listener)
+
+ onDispose { removeOnDestinationChangedListener(listener) }
+ }
+ return selectedItem
+}
diff --git a/app/src/main/kotlin/com/manuelnunez/apps/MainDestination.kt b/app/src/main/kotlin/com/manuelnunez/apps/MainDestination.kt
deleted file mode 100644
index 2a81196..0000000
--- a/app/src/main/kotlin/com/manuelnunez/apps/MainDestination.kt
+++ /dev/null
@@ -1,51 +0,0 @@
-package com.manuelnunez.apps
-
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Favorite
-import androidx.compose.material.icons.filled.Home
-import androidx.compose.material.icons.outlined.FavoriteBorder
-import androidx.compose.material.icons.outlined.Home
-import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.navigation.NavGraph.Companion.findStartDestination
-import androidx.navigation.NavHostController
-import androidx.navigation.navOptions
-import com.manuelnunez.apps.feature.favorites.ui.navigation.navigateToFavorites
-import com.manuelnunez.apps.features.home.ui.navigation.navigateToHome
-
-enum class MainDestination(
- val selectedIcon: ImageVector,
- val unselectedIcon: ImageVector,
- val iconTextId: Int,
- val titleTextId: Int,
-) {
- HOME(
- selectedIcon = Icons.Default.Home,
- unselectedIcon = Icons.Outlined.Home,
- iconTextId = R.string.home_destination_title,
- titleTextId = R.string.home_destination_title),
- FAVORITES(
- selectedIcon = Icons.Default.Favorite,
- unselectedIcon = Icons.Outlined.FavoriteBorder,
- iconTextId = R.string.favorites_destination_title,
- titleTextId = R.string.favorites_destination_title)
-}
-
-fun NavHostController.onNavigateToDestination(destination: MainDestination) {
- val topLevelNavOptions = navOptions {
- // Pop up to the start destination of the graph to
- // avoid building up a large stack of destinations
- // on the back stack as users select items
- popUpTo(graph.findStartDestination().id) { saveState = true }
- // Avoid multiple copies of the same destination when
- // reselecting the same item
- launchSingleTop = true
- // Restore state when reselecting a previously selected item
- restoreState = true
- }
- when (destination) {
- MainDestination.HOME -> navigateToHome(topLevelNavOptions)
- MainDestination.FAVORITES -> navigateToFavorites(topLevelNavOptions)
- }
-}
-
-fun mainDestinations() = listOf(MainDestination.HOME, MainDestination.FAVORITES)
diff --git a/app/src/main/kotlin/com/manuelnunez/apps/navigation/MainNavigation.kt b/app/src/main/kotlin/com/manuelnunez/apps/navigation/MainNavigation.kt
index 99cdf1e..cdb8ea0 100644
--- a/app/src/main/kotlin/com/manuelnunez/apps/navigation/MainNavigation.kt
+++ b/app/src/main/kotlin/com/manuelnunez/apps/navigation/MainNavigation.kt
@@ -1,24 +1,41 @@
package com.manuelnunez.apps.navigation
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.material.icons.filled.Home
+import androidx.compose.material.icons.outlined.FavoriteBorder
+import androidx.compose.material.icons.outlined.Home
import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.navigation.NavController
+import androidx.navigation.NavGraph.Companion.findStartDestination
+import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
+import androidx.navigation.navigation
+import com.manuelnunez.apps.R
+import com.manuelnunez.apps.feature.favorites.ui.navigation.FAVORITES_ROUTE
import com.manuelnunez.apps.feature.favorites.ui.navigation.favoritesScreen
+import com.manuelnunez.apps.features.detail.ui.navigation.detailFavScreen
import com.manuelnunez.apps.features.detail.ui.navigation.detailScreen
import com.manuelnunez.apps.features.detail.ui.navigation.navigateToDetail
+import com.manuelnunez.apps.features.detail.ui.navigation.navigateToDetailFav
import com.manuelnunez.apps.features.home.ui.navigation.HOME_ROUTE
import com.manuelnunez.apps.features.home.ui.navigation.homeScreen
import com.manuelnunez.apps.features.seemore.ui.navigation.navigateToSeeMore
import com.manuelnunez.apps.features.seemore.ui.navigation.seeMoreScreen
@Composable
-fun MainNavigation(
- modifier: Modifier = Modifier,
- navController: NavHostController,
- startDestination: String = HOME_ROUTE,
-) {
- NavHost(modifier = modifier, navController = navController, startDestination = startDestination) {
+fun MainNavigation(navController: NavHostController) {
+ NavHost(navController = navController, startDestination = RootScreen.HOME.route) {
+ addHomeRoute(navController)
+ addFavoriteRoute(navController)
+ }
+}
+
+// home navigation
+private fun NavGraphBuilder.addHomeRoute(navController: NavController) {
+ navigation(route = RootScreen.HOME.route, startDestination = HOME_ROUTE) {
homeScreen(
navigateToDetails = navController::navigateToDetail,
navigateToSeeMore = navController::navigateToSeeMore)
@@ -26,6 +43,47 @@ fun MainNavigation(
seeMoreScreen(
onBackClick = { navController.navigateUp() },
navigateToDetails = navController::navigateToDetail)
- favoritesScreen()
}
}
+
+// favorite navigation
+private fun NavGraphBuilder.addFavoriteRoute(navController: NavController) {
+ navigation(route = RootScreen.FAVORITES.route, startDestination = FAVORITES_ROUTE) {
+ favoritesScreen(
+ navigateToDetails = navController::navigateToDetailFav,
+ onBackClick = { navController.navigateUp() },
+ )
+ detailFavScreen(onBackClick = { navController.navigateUp() })
+ }
+}
+
+enum class RootScreen(
+ val route: String,
+ val selectedIcon: ImageVector,
+ val unselectedIcon: ImageVector,
+ val iconTextId: Int,
+ val titleTextId: Int,
+) {
+ HOME(
+ route = "home_root",
+ selectedIcon = Icons.Default.Home,
+ unselectedIcon = Icons.Outlined.Home,
+ iconTextId = R.string.home_destination_title,
+ titleTextId = R.string.home_destination_title),
+ FAVORITES(
+ route = "favorite_root",
+ selectedIcon = Icons.Default.Favorite,
+ unselectedIcon = Icons.Outlined.FavoriteBorder,
+ iconTextId = R.string.favorites_destination_title,
+ titleTextId = R.string.favorites_destination_title)
+}
+
+fun NavController.navigateToRootScreen(rootScreen: RootScreen) {
+ navigate(rootScreen.route) {
+ launchSingleTop = true
+ restoreState = true
+ popUpTo(graph.findStartDestination().id) { saveState = true }
+ }
+}
+
+fun mainDestinations() = listOf(RootScreen.HOME, RootScreen.FAVORITES)
diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts
index 718bf96..f9c612a 100644
--- a/core/data/build.gradle.kts
+++ b/core/data/build.gradle.kts
@@ -30,6 +30,7 @@ dependencies {
implementation(projects.features.home.domain)
implementation(projects.features.seemore.domain)
implementation(projects.features.favorites.domain)
+ implementation(projects.features.detail.domain)
implementation(libs.androidx.dataStore.core)
implementation(libs.protobuf.kotlin.lite)
diff --git a/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/datasource/local/FavoritesDataSource.kt b/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/datasource/local/FavoritesDataSource.kt
index 76c4d16..a12b54d 100644
--- a/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/datasource/local/FavoritesDataSource.kt
+++ b/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/datasource/local/FavoritesDataSource.kt
@@ -43,4 +43,7 @@ class FavoritesDataSource @Inject constructor(private val itemDataStore: DataSto
updatedItems
}
}
+
+ fun isItemFavorite(itemPhotoId: String): Flow =
+ favorites.map { it.any { item -> item.photoId == itemPhotoId } }
}
diff --git a/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/di/DataModule.kt b/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/di/DataModule.kt
index fd061d9..4f7a5c9 100644
--- a/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/di/DataModule.kt
+++ b/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/di/DataModule.kt
@@ -4,9 +4,11 @@ import com.manuelnunez.apps.core.data.datasource.remote.CataasCatsRemoteDataSour
import com.manuelnunez.apps.core.data.datasource.remote.CataasCatsRemoteDataSourceImpl
import com.manuelnunez.apps.core.data.datasource.remote.PexelsCatsRemoteDataSource
import com.manuelnunez.apps.core.data.datasource.remote.PexelsCatsRemoteDataSourceImpl
+import com.manuelnunez.apps.core.data.repository.DetailRepositoryImpl
import com.manuelnunez.apps.core.data.repository.FavoritesRepositoryImpl
import com.manuelnunez.apps.core.data.repository.HomeRepositoryImpl
import com.manuelnunez.apps.core.data.repository.SeeMoreRepositoryImpl
+import com.manuelnunez.apps.features.detail.domain.repository.DetailRepository
import com.manuelnunez.apps.features.favorites.domain.repository.FavoritesRepository
import com.manuelnunez.apps.features.home.domain.repository.HomeRepository
import com.manuelnunez.apps.features.seemore.domain.repository.SeeMoreRepository
@@ -28,6 +30,10 @@ abstract class DataModule {
@Binds
abstract fun bindsSeeMoreRepository(seeMoreRepository: SeeMoreRepositoryImpl): SeeMoreRepository
+ @Singleton
+ @Binds
+ abstract fun bindsDetailRepository(detailRepository: DetailRepositoryImpl): DetailRepository
+
@Singleton
@Binds
abstract fun bindsFavoritesRepository(
diff --git a/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/repository/DetailRepositoryImpl.kt b/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/repository/DetailRepositoryImpl.kt
new file mode 100644
index 0000000..4fb131e
--- /dev/null
+++ b/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/repository/DetailRepositoryImpl.kt
@@ -0,0 +1,22 @@
+package com.manuelnunez.apps.core.data.repository
+
+import com.manuelnunez.apps.core.data.datasource.local.FavoritesDataSource
+import com.manuelnunez.apps.core.domain.model.Item
+import com.manuelnunez.apps.features.detail.domain.repository.DetailRepository
+import kotlinx.coroutines.flow.Flow
+import javax.inject.Inject
+
+class DetailRepositoryImpl
+@Inject
+constructor(private val favoritesDataSource: FavoritesDataSource) : DetailRepository {
+ override suspend fun saveFavoriteItem(favoriteItem: Item) {
+ favoritesDataSource.addItemToFavorites(favoriteItem)
+ }
+
+ override suspend fun removeFavoriteItem(favoriteItem: Item) {
+ favoritesDataSource.removeItemFromFavorites(favoriteItem.photoId)
+ }
+
+ override fun isItemFavorite(itemPhotoId: String): Flow =
+ favoritesDataSource.isItemFavorite(itemPhotoId)
+}
diff --git a/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/repository/FavoritesRepositoryImpl.kt b/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/repository/FavoritesRepositoryImpl.kt
index fa9eb2e..d97ec20 100644
--- a/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/repository/FavoritesRepositoryImpl.kt
+++ b/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/repository/FavoritesRepositoryImpl.kt
@@ -10,12 +10,4 @@ class FavoritesRepositoryImpl
@Inject
constructor(private val favoritesDataSource: FavoritesDataSource) : FavoritesRepository {
override fun getAllFavorites(): Flow
> = favoritesDataSource.favorites
-
- override suspend fun saveFavoriteItem(favoriteItem: Item) {
- favoritesDataSource.addItemToFavorites(favoriteItem)
- }
-
- override suspend fun removeFavoriteItem(favoriteItem: Item) {
- favoritesDataSource.removeItemFromFavorites(favoriteItem.photoId)
- }
}
diff --git a/core/datastore-proto/src/main/kotlin/com/manuelnunez/apps/core/datastore/proto/serializer/di/DataStoreModule.kt b/core/datastore-proto/src/main/kotlin/com/manuelnunez/apps/core/datastore/proto/serializer/di/DataStoreModule.kt
index 27f7d39..2d84641 100644
--- a/core/datastore-proto/src/main/kotlin/com/manuelnunez/apps/core/datastore/proto/serializer/di/DataStoreModule.kt
+++ b/core/datastore-proto/src/main/kotlin/com/manuelnunez/apps/core/datastore/proto/serializer/di/DataStoreModule.kt
@@ -1,10 +1,11 @@
-package com.manuelnunez.apps.core.datastore.proto.serializer
+package com.manuelnunez.apps.core.datastore.proto.serializer.di
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.core.DataStoreFactory
import androidx.datastore.dataStoreFile
import com.manuelnunez.apps.core.datastore.proto.ItemList
+import com.manuelnunez.apps.core.datastore.proto.serializer.ItemListSerializer
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
diff --git a/core/domain/build.gradle.kts b/core/domain/build.gradle.kts
index d766e44..bea0b6b 100644
--- a/core/domain/build.gradle.kts
+++ b/core/domain/build.gradle.kts
@@ -2,7 +2,7 @@ plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.ksp)
- id("kotlin-parcelize")
+ kotlin("plugin.serialization") version "1.9.23"
}
android {
@@ -23,6 +23,7 @@ android {
dependencies {
implementation(projects.core.common)
+ implementation(libs.kotlinx.serializer)
// HILT
implementation(libs.hilt.android)
diff --git a/core/domain/src/main/kotlin/com/manuelnunez/apps/core/domain/model/Item.kt b/core/domain/src/main/kotlin/com/manuelnunez/apps/core/domain/model/Item.kt
index dee05f9..ffcce56 100644
--- a/core/domain/src/main/kotlin/com/manuelnunez/apps/core/domain/model/Item.kt
+++ b/core/domain/src/main/kotlin/com/manuelnunez/apps/core/domain/model/Item.kt
@@ -1,12 +1,24 @@
package com.manuelnunez.apps.core.domain.model
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.decodeFromString
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
+import java.net.URLDecoder
+import java.net.URLEncoder
-@Parcelize
+@Serializable
data class Item(
val photoId: String,
val imageUrl: String,
val thumbnailUrl: String,
val description: String
-) : Parcelable
+) {
+ companion object {
+ val empty = Item(photoId = "", imageUrl = "", thumbnailUrl = "", description = "")
+ }
+}
+
+fun Item.toEncodedString(): String = URLEncoder.encode(Json.encodeToString(this), "UTF-8")
+
+fun String.toDecodedItem(): Item = Json.decodeFromString- (URLDecoder.decode(this, "UTF-8"))
diff --git a/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/BackToolbar.kt b/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/BackToolbar.kt
new file mode 100644
index 0000000..1621298
--- /dev/null
+++ b/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/BackToolbar.kt
@@ -0,0 +1,36 @@
+package com.manuelnunez.apps.core.ui.component
+
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.rounded.ArrowBack
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import com.manuelnunez.apps.core.ui.R
+import com.manuelnunez.apps.core.ui.theme.MainTheme
+import com.manuelnunez.apps.core.ui.utils.ThemePreviews
+
+@Composable
+fun BackToolbar(title: String, onBackClick: () -> Unit) {
+ Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
+ IconButton(onClick = onBackClick) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
+ contentDescription = stringResource(id = R.string.button_back),
+ tint = MaterialTheme.colorScheme.onSurface)
+ }
+
+ SurfaceText(text = title)
+ }
+}
+
+@ThemePreviews
+@Composable
+fun BackToolbarPreview() {
+ MainTheme { BackToolbar(title = stringResource(id = R.string.section_popular), onBackClick = {}) }
+}
diff --git a/core/ui/src/main/res/values/strings.xml b/core/ui/src/main/res/values/strings.xml
index d474e51..748a23a 100644
--- a/core/ui/src/main/res/values/strings.xml
+++ b/core/ui/src/main/res/values/strings.xml
@@ -8,5 +8,5 @@
Retry
Confirm
Dismiss
-
+ An Error has occurred. Please go back and try again.
\ No newline at end of file
diff --git a/features/detail/domain/.gitignore b/features/detail/domain/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/features/detail/domain/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/features/detail/domain/build.gradle.kts b/features/detail/domain/build.gradle.kts
new file mode 100644
index 0000000..4a25a1b
--- /dev/null
+++ b/features/detail/domain/build.gradle.kts
@@ -0,0 +1,38 @@
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.ksp)
+}
+
+android {
+ namespace = "com.manuelnunez.apps.features.home.domain"
+ compileSdk = 34
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+
+ defaultConfig { minSdk = 21 }
+
+ kotlinOptions { jvmTarget = JavaVersion.VERSION_17.toString() }
+
+ tasks.withType { useJUnitPlatform() }
+
+ packaging { resources { excludes.add("META-INF/{LICENSE-notice.md,LICENSE.md}") } }
+}
+
+dependencies {
+ implementation(projects.core.common)
+ implementation(projects.core.domain)
+
+ implementation(libs.kotlinx.coroutines.android)
+
+ // HILT
+ implementation(libs.hilt.android)
+ ksp(libs.hilt.compiler)
+
+ testImplementation(libs.junit)
+ testImplementation(libs.mockk)
+ testImplementation(libs.turbine)
+}
diff --git a/features/detail/domain/src/main/AndroidManifest.xml b/features/detail/domain/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..44008a4
--- /dev/null
+++ b/features/detail/domain/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/features/detail/domain/src/main/kotlin/com/manuelnunez/apps/features/detail/domain/repository/DetailRepository.kt b/features/detail/domain/src/main/kotlin/com/manuelnunez/apps/features/detail/domain/repository/DetailRepository.kt
new file mode 100644
index 0000000..9063e8d
--- /dev/null
+++ b/features/detail/domain/src/main/kotlin/com/manuelnunez/apps/features/detail/domain/repository/DetailRepository.kt
@@ -0,0 +1,12 @@
+package com.manuelnunez.apps.features.detail.domain.repository
+
+import com.manuelnunez.apps.core.domain.model.Item
+import kotlinx.coroutines.flow.Flow
+
+interface DetailRepository {
+ suspend fun saveFavoriteItem(favoriteItem: Item)
+
+ suspend fun removeFavoriteItem(favoriteItem: Item)
+
+ fun isItemFavorite(itemPhotoId: String): Flow
+}
diff --git a/features/detail/domain/src/main/kotlin/com/manuelnunez/apps/features/detail/domain/usecase/GetFavoriteStatusUseCase.kt b/features/detail/domain/src/main/kotlin/com/manuelnunez/apps/features/detail/domain/usecase/GetFavoriteStatusUseCase.kt
new file mode 100644
index 0000000..2567849
--- /dev/null
+++ b/features/detail/domain/src/main/kotlin/com/manuelnunez/apps/features/detail/domain/usecase/GetFavoriteStatusUseCase.kt
@@ -0,0 +1,17 @@
+package com.manuelnunez.apps.features.detail.domain.usecase
+
+import com.manuelnunez.apps.core.common.dispatcher.CoroutineDispatcherProvider
+import com.manuelnunez.apps.core.domain.usecase.FlowUseCase
+import com.manuelnunez.apps.features.detail.domain.repository.DetailRepository
+import kotlinx.coroutines.flow.Flow
+import javax.inject.Inject
+
+class GetFavoriteStatusUseCase
+@Inject
+constructor(
+ private val favoritesRepository: DetailRepository,
+ coroutineDispatcherProvider: CoroutineDispatcherProvider
+) : FlowUseCase(coroutineDispatcherProvider) {
+
+ override fun execute(input: String): Flow = favoritesRepository.isItemFavorite(input)
+}
diff --git a/features/favorites/domain/src/main/kotlin/com/manuelnunez/apps/features/favorites/domain/usecase/RemoveFavoritesUseCase.kt b/features/detail/domain/src/main/kotlin/com/manuelnunez/apps/features/detail/domain/usecase/RemoveFavoritesUseCase.kt
similarity index 67%
rename from features/favorites/domain/src/main/kotlin/com/manuelnunez/apps/features/favorites/domain/usecase/RemoveFavoritesUseCase.kt
rename to features/detail/domain/src/main/kotlin/com/manuelnunez/apps/features/detail/domain/usecase/RemoveFavoritesUseCase.kt
index 42555cf..fa627c8 100644
--- a/features/favorites/domain/src/main/kotlin/com/manuelnunez/apps/features/favorites/domain/usecase/RemoveFavoritesUseCase.kt
+++ b/features/detail/domain/src/main/kotlin/com/manuelnunez/apps/features/detail/domain/usecase/RemoveFavoritesUseCase.kt
@@ -1,9 +1,9 @@
-package com.manuelnunez.apps.features.favorites.domain.usecase
+package com.manuelnunez.apps.features.detail.domain.usecase
import com.manuelnunez.apps.core.common.dispatcher.CoroutineDispatcherProvider
import com.manuelnunez.apps.core.domain.model.Item
import com.manuelnunez.apps.core.domain.usecase.FlowUseCase
-import com.manuelnunez.apps.features.favorites.domain.repository.FavoritesRepository
+import com.manuelnunez.apps.features.detail.domain.repository.DetailRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import javax.inject.Inject
@@ -11,11 +11,11 @@ import javax.inject.Inject
class RemoveFavoritesUseCase
@Inject
constructor(
- private val favoritesRepository: FavoritesRepository,
+ private val detailRepository: DetailRepository,
coroutineDispatcherProvider: CoroutineDispatcherProvider
) : FlowUseCase- (coroutineDispatcherProvider) {
override fun execute(input: Item): Flow = flow {
- favoritesRepository.removeFavoriteItem(input)
+ detailRepository.removeFavoriteItem(input)
}
}
diff --git a/features/favorites/domain/src/main/kotlin/com/manuelnunez/apps/features/favorites/domain/usecase/SaveFavoritesUseCase.kt b/features/detail/domain/src/main/kotlin/com/manuelnunez/apps/features/detail/domain/usecase/SaveFavoritesUseCase.kt
similarity index 59%
rename from features/favorites/domain/src/main/kotlin/com/manuelnunez/apps/features/favorites/domain/usecase/SaveFavoritesUseCase.kt
rename to features/detail/domain/src/main/kotlin/com/manuelnunez/apps/features/detail/domain/usecase/SaveFavoritesUseCase.kt
index 10cd282..ea0f2b4 100644
--- a/features/favorites/domain/src/main/kotlin/com/manuelnunez/apps/features/favorites/domain/usecase/SaveFavoritesUseCase.kt
+++ b/features/detail/domain/src/main/kotlin/com/manuelnunez/apps/features/detail/domain/usecase/SaveFavoritesUseCase.kt
@@ -1,9 +1,9 @@
-package com.manuelnunez.apps.features.favorites.domain.usecase
+package com.manuelnunez.apps.features.detail.domain.usecase
import com.manuelnunez.apps.core.common.dispatcher.CoroutineDispatcherProvider
import com.manuelnunez.apps.core.domain.model.Item
import com.manuelnunez.apps.core.domain.usecase.FlowUseCase
-import com.manuelnunez.apps.features.favorites.domain.repository.FavoritesRepository
+import com.manuelnunez.apps.features.detail.domain.repository.DetailRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import javax.inject.Inject
@@ -11,11 +11,9 @@ import javax.inject.Inject
class SaveFavoritesUseCase
@Inject
constructor(
- private val favoritesRepository: FavoritesRepository,
+ private val detailRepository: DetailRepository,
coroutineDispatcherProvider: CoroutineDispatcherProvider
) : FlowUseCase
- (coroutineDispatcherProvider) {
- override fun execute(input: Item): Flow = flow {
- favoritesRepository.saveFavoriteItem(input)
- }
+ override fun execute(input: Item): Flow = flow { detailRepository.saveFavoriteItem(input) }
}
diff --git a/features/detail/ui/build.gradle.kts b/features/detail/ui/build.gradle.kts
index b3c9141..2dbf4c7 100644
--- a/features/detail/ui/build.gradle.kts
+++ b/features/detail/ui/build.gradle.kts
@@ -35,6 +35,7 @@ dependencies {
implementation(projects.core.common)
implementation(projects.core.domain)
implementation(projects.core.ui)
+ implementation(projects.features.detail.domain)
// Arch Components
implementation(libs.androidx.lifecycle.runtime.compose)
@@ -50,6 +51,7 @@ dependencies {
implementation(libs.coil.kt)
implementation(libs.coil.kt.compose)
implementation(libs.coil.kt.gif)
+ implementation(libs.kotlinx.serializer)
// Compose
implementation(libs.androidx.compose.ui)
diff --git a/features/detail/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/detail/ui/DetailViewTest.kt b/features/detail/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/detail/ui/DetailViewTest.kt
index 33316cf..f082c4c 100644
--- a/features/detail/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/detail/ui/DetailViewTest.kt
+++ b/features/detail/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/detail/ui/DetailViewTest.kt
@@ -19,7 +19,7 @@ class DetailViewTest {
@Test
fun photo_whenScreenIsLoaded_showsPhotoShareAndDescription() {
- composeTestRule.setContent { DetailScreen(mockItem, onBackClick = {}) }
+ composeTestRule.setContent { DetailScreen(mockItem, onBackClick = {}, onFavoriteClicked = {}) }
// description
composeTestRule.onNodeWithText(mockItem.description).assertExists()
diff --git a/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/features/detail/ui/DetailRoute.kt b/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/features/detail/ui/DetailRoute.kt
new file mode 100644
index 0000000..7e21b70
--- /dev/null
+++ b/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/features/detail/ui/DetailRoute.kt
@@ -0,0 +1,17 @@
+package com.manuelnunez.apps.features.detail.ui
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.manuelnunez.apps.features.detail.ui.components.DetailScreen
+
+@Composable
+fun DetailRoute(detailViewModel: DetailViewModel = hiltViewModel(), onBackClick: () -> Unit) {
+ val item by detailViewModel.state.collectAsStateWithLifecycle()
+ DetailScreen(
+ item = item.item,
+ isFavorite = item.isFavorite,
+ onFavoriteClicked = detailViewModel::favorite,
+ onBackClick = onBackClick)
+}
diff --git a/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/features/detail/ui/DetailView.kt b/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/features/detail/ui/DetailView.kt
deleted file mode 100644
index 61e3747..0000000
--- a/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/features/detail/ui/DetailView.kt
+++ /dev/null
@@ -1,15 +0,0 @@
-package com.manuelnunez.apps.features.detail.ui
-
-import androidx.compose.runtime.Composable
-import com.manuelnunez.apps.core.domain.model.Item
-import com.manuelnunez.apps.features.detail.ui.components.DetailErrorScreen
-import com.manuelnunez.apps.features.detail.ui.components.DetailScreen
-
-@Composable
-fun DetailView(onBackClick: () -> Unit, item: Item?) {
- if (item == null) {
- DetailErrorScreen(onBackClick)
- } else {
- DetailScreen(item, onBackClick)
- }
-}
diff --git a/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/features/detail/ui/DetailViewModel.kt b/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/features/detail/ui/DetailViewModel.kt
new file mode 100644
index 0000000..c157a0e
--- /dev/null
+++ b/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/features/detail/ui/DetailViewModel.kt
@@ -0,0 +1,86 @@
+package com.manuelnunez.apps.features.detail.ui
+
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.manuelnunez.apps.core.domain.model.Item
+import com.manuelnunez.apps.core.domain.model.toDecodedItem
+import com.manuelnunez.apps.features.detail.domain.usecase.GetFavoriteStatusUseCase
+import com.manuelnunez.apps.features.detail.domain.usecase.RemoveFavoritesUseCase
+import com.manuelnunez.apps.features.detail.domain.usecase.SaveFavoritesUseCase
+import com.manuelnunez.apps.features.detail.ui.navigation.DETAIL_ITEM
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.stateIn
+import javax.inject.Inject
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@HiltViewModel
+class DetailViewModel
+@Inject
+constructor(
+ private val saveFavoritesUseCase: SaveFavoritesUseCase,
+ private val removeFavoritesUseCase: RemoveFavoritesUseCase,
+ private val getFavoriteStatusUseCase: GetFavoriteStatusUseCase,
+ savedStateHandle: SavedStateHandle
+) : ViewModel() {
+ private val selectedItemString: StateFlow =
+ savedStateHandle.getStateFlow(key = DETAIL_ITEM, initialValue = "")
+
+ private val selectedItem: StateFlow
- =
+ selectedItemString
+ .mapLatest { itemString -> itemString.toDecodedItem() }
+ .stateIn(
+ scope = viewModelScope, started = SharingStarted.Eagerly, initialValue = Item.empty)
+
+ private val isItemFavorite = MutableStateFlow(false)
+
+ val state =
+ combine(isItemFavorite, selectedItem) { isFavorite, item -> DetailUiState(item, isFavorite) }
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), DetailUiState.Empty)
+
+ init {
+ checkFavorite()
+ }
+
+ fun checkFavorite() {
+ getFavoriteStatusUseCase
+ .prepare(selectedItem.value.photoId)
+ .map { isItemFavorite.value = it }
+ .catch {}
+ .launchIn(viewModelScope)
+ }
+
+ fun favorite() {
+ if (isItemFavorite.value) {
+ removeFavorite(selectedItem.value)
+ } else {
+ saveFavorite(selectedItem.value)
+ }
+ }
+
+ fun saveFavorite(item: Item) {
+ saveFavoritesUseCase.prepare(item).catch {}.launchIn(viewModelScope)
+ }
+
+ fun removeFavorite(item: Item) {
+ removeFavoritesUseCase.prepare(item).catch {}.launchIn(viewModelScope)
+ }
+
+ data class DetailUiState(
+ val item: Item = Item.empty,
+ val isFavorite: Boolean = false,
+ ) {
+ companion object {
+ val Empty = DetailUiState()
+ }
+ }
+}
diff --git a/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/features/detail/ui/components/DetailScreen.kt b/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/features/detail/ui/components/DetailScreen.kt
index a5a46f9..745ff7b 100644
--- a/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/features/detail/ui/components/DetailScreen.kt
+++ b/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/features/detail/ui/components/DetailScreen.kt
@@ -9,13 +9,13 @@ import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeContent
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.rounded.ArrowBack
+import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.material.icons.filled.FavoriteBorder
import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@@ -34,43 +34,68 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import com.manuelnunez.apps.core.domain.model.Item
+import com.manuelnunez.apps.core.ui.component.BackToolbar
import com.manuelnunez.apps.core.ui.component.StatefulAsyncImage
import com.manuelnunez.apps.core.ui.component.SurfaceText
import com.manuelnunez.apps.core.ui.theme.MainTheme
import com.manuelnunez.apps.core.ui.utils.OrientationPreviews
import com.manuelnunez.apps.features.detail.ui.R
-import com.manuelnunez.apps.core.ui.R as RCU
@Composable
-fun DetailScreen(item: Item, onBackClick: () -> Unit) {
+fun DetailScreen(
+ item: Item,
+ isFavorite: Boolean,
+ onFavoriteClicked: () -> Unit,
+ onBackClick: () -> Unit
+) {
val orientation = LocalConfiguration.current.orientation
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
- DetailLandscape(item, onBackClick)
+ DetailLandscape(item, isFavorite, onFavoriteClicked, onBackClick)
} else {
- DetailPortrait(item, onBackClick)
+ DetailPortrait(item, isFavorite, onFavoriteClicked, onBackClick)
}
}
@Composable
-private fun DetailPortrait(item: Item, onBackClick: () -> Unit) {
- Column(Modifier.windowInsetsPadding(WindowInsets.safeContent)) {
+private fun DetailPortrait(
+ item: Item,
+ isFavorite: Boolean,
+ onFavoriteClicked: () -> Unit,
+ onBackClick: () -> Unit
+) {
+ Column(Modifier.fillMaxSize().windowInsetsPadding(WindowInsets.safeContent)) {
Spacer(modifier = Modifier.height(20.dp))
- DetailToolbar(onBackClick)
+ BackToolbar(
+ title = stringResource(id = R.string.section_details_title), onBackClick = onBackClick)
Spacer(modifier = Modifier.height(10.dp))
Column(
- modifier = Modifier.weight(1f).wrapContentSize().heightIn(min = 100.dp),
+ modifier = Modifier.weight(1f).wrapContentSize(),
horizontalAlignment = Alignment.CenterHorizontally) {
StatefulAsyncImage(
- modifier = Modifier.fillMaxWidth().padding(horizontal = 6.dp),
+ modifier = Modifier.fillMaxWidth().padding(horizontal = 20.dp),
imageUrl = item.imageUrl,
contentDescription = item.photoId,
contentScale = ContentScale.Fit)
- ShareImage(url = item.imageUrl)
+ Row {
+ ShareImage(url = item.imageUrl)
+
+ IconButton(onClick = onFavoriteClicked) {
+ Icon(
+ imageVector =
+ if (isFavorite) {
+ Icons.Filled.Favorite
+ } else {
+ Icons.Filled.FavoriteBorder
+ },
+ contentDescription = stringResource(id = R.string.button_share),
+ tint = MaterialTheme.colorScheme.onSurface)
+ }
+ }
SurfaceText(
modifier = Modifier.padding(top = 10.dp).padding(horizontal = 40.dp),
@@ -78,31 +103,21 @@ private fun DetailPortrait(item: Item, onBackClick: () -> Unit) {
text = item.description,
style = MaterialTheme.typography.titleSmall)
}
-
- Spacer(modifier = Modifier.height(20.dp))
- }
-}
-
-@Composable
-private fun DetailToolbar(onBackClick: () -> Unit) {
- Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
- IconButton(onClick = onBackClick) {
- Icon(
- imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
- contentDescription = stringResource(id = RCU.string.button_back),
- tint = MaterialTheme.colorScheme.onSurface)
- }
-
- SurfaceText(text = "Details")
}
}
@Composable
-private fun DetailLandscape(item: Item, onBackClick: () -> Unit) {
+private fun DetailLandscape(
+ item: Item,
+ isFavorite: Boolean,
+ onFavoriteClicked: () -> Unit,
+ onBackClick: () -> Unit
+) {
Column(modifier = Modifier.fillMaxSize().windowInsetsPadding(WindowInsets.safeContent)) {
Spacer(modifier = Modifier.height(20.dp))
- DetailToolbar(onBackClick)
+ BackToolbar(
+ title = stringResource(id = R.string.section_details_title), onBackClick = onBackClick)
Spacer(modifier = Modifier.height(10.dp))
@@ -113,7 +128,21 @@ private fun DetailLandscape(item: Item, onBackClick: () -> Unit) {
contentDescription = item.photoId,
contentScale = ContentScale.Fit)
- ShareImage(modifier = Modifier.weight(0.1f), url = item.imageUrl)
+ Column(Modifier.weight(0.1f)) {
+ ShareImage(url = item.imageUrl)
+
+ IconButton(onClick = onFavoriteClicked) {
+ Icon(
+ imageVector =
+ if (isFavorite) {
+ Icons.Filled.Favorite
+ } else {
+ Icons.Filled.FavoriteBorder
+ },
+ contentDescription = stringResource(id = R.string.button_share),
+ tint = MaterialTheme.colorScheme.onSurface)
+ }
+ }
VerticalDivider()
@@ -155,7 +184,12 @@ private fun ShareImage(modifier: Modifier = Modifier, url: String) {
@Composable
fun DetailScreenPreview() {
MainTheme {
+ val item = Item("", "", "", "Description Photo: Selected Photo 4564")
DetailScreen(
- onBackClick = {}, item = Item("", "", "", "Description Photo: Selected Photo 4564"))
+ item = item,
+ isFavorite = false,
+ onFavoriteClicked = {},
+ onBackClick = {},
+ )
}
}
diff --git a/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/features/detail/ui/navigation/DetailNavigation.kt b/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/features/detail/ui/navigation/DetailNavigation.kt
index 2213ae3..2ef31f3 100644
--- a/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/features/detail/ui/navigation/DetailNavigation.kt
+++ b/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/features/detail/ui/navigation/DetailNavigation.kt
@@ -1,26 +1,29 @@
package com.manuelnunez.apps.features.detail.ui.navigation
-import android.os.Bundle
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptionsBuilder
import androidx.navigation.compose.composable
-import androidx.navigation.navOptions
-import com.manuelnunez.apps.core.common.navigation.navigate
import com.manuelnunez.apps.core.domain.model.Item
-import com.manuelnunez.apps.features.detail.ui.DetailView
+import com.manuelnunez.apps.core.domain.model.toEncodedString
+import com.manuelnunez.apps.features.detail.ui.DetailRoute
const val DETAIL_ITEM = "myItem"
-const val DETAIL_ROUTE = "detail"
+const val DETAIL_ROUTE_FAV = "detail_route_fav"
+const val DETAIL_ROUTE = "detail_route"
fun NavController.navigateToDetail(item: Item, navOptions: NavOptionsBuilder.() -> Unit = {}) {
- val bundle = Bundle().apply { putParcelable(DETAIL_ITEM, item) }
- navigate(route = DETAIL_ROUTE, args = bundle, navOptions = navOptions(navOptions))
+ navigate("$DETAIL_ROUTE/${item.toEncodedString()}", navOptions)
}
fun NavGraphBuilder.detailScreen(onBackClick: () -> Unit) {
- composable(DETAIL_ROUTE) { backStackEntry ->
- val myItem = backStackEntry.arguments?.getParcelable
- (DETAIL_ITEM)
- DetailView(onBackClick = onBackClick, item = myItem)
- }
+ composable("$DETAIL_ROUTE/{$DETAIL_ITEM}") { DetailRoute(onBackClick = onBackClick) }
+}
+
+fun NavController.navigateToDetailFav(item: Item, navOptions: NavOptionsBuilder.() -> Unit = {}) {
+ navigate("$DETAIL_ROUTE_FAV/${item.toEncodedString()}", navOptions)
+}
+
+fun NavGraphBuilder.detailFavScreen(onBackClick: () -> Unit) {
+ composable("$DETAIL_ROUTE_FAV/{$DETAIL_ITEM}") { DetailRoute(onBackClick = onBackClick) }
}
diff --git a/features/detail/ui/src/main/res/values/strings.xml b/features/detail/ui/src/main/res/values/strings.xml
index 5e67771..1a1ef95 100644
--- a/features/detail/ui/src/main/res/values/strings.xml
+++ b/features/detail/ui/src/main/res/values/strings.xml
@@ -1,6 +1,6 @@
+ Detail
Share
Checkout this image from Purrfect Pics!: %1$s
Share Image
- An Error has occurred. Please go back and try again.
\ No newline at end of file
diff --git a/features/favorites/domain/src/main/kotlin/com/manuelnunez/apps/features/favorites/domain/repository/FavoritesRepository.kt b/features/favorites/domain/src/main/kotlin/com/manuelnunez/apps/features/favorites/domain/repository/FavoritesRepository.kt
index f740942..bd4f982 100644
--- a/features/favorites/domain/src/main/kotlin/com/manuelnunez/apps/features/favorites/domain/repository/FavoritesRepository.kt
+++ b/features/favorites/domain/src/main/kotlin/com/manuelnunez/apps/features/favorites/domain/repository/FavoritesRepository.kt
@@ -5,8 +5,4 @@ import kotlinx.coroutines.flow.Flow
interface FavoritesRepository {
fun getAllFavorites(): Flow
>
-
- suspend fun saveFavoriteItem(favoriteItem: Item)
-
- suspend fun removeFavoriteItem(favoriteItem: Item)
}
diff --git a/features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/feature/favorites/ui/FavoritesRoute.kt b/features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/feature/favorites/ui/FavoritesRoute.kt
new file mode 100644
index 0000000..d78635f
--- /dev/null
+++ b/features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/feature/favorites/ui/FavoritesRoute.kt
@@ -0,0 +1,38 @@
+package com.manuelnunez.apps.feature.favorites.ui
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.manuelnunez.apps.core.domain.model.Item
+import com.manuelnunez.apps.feature.favorites.ui.FavoritesViewModel.FavoriteItemsState
+import com.manuelnunez.apps.feature.favorites.ui.component.FavoritesErrorScreen
+import com.manuelnunez.apps.feature.favorites.ui.component.FavoritesScreen
+
+@Composable
+fun FavoritesRoute(
+ viewModel: FavoritesViewModel = hiltViewModel(),
+ onFavoriteClicked: (Item) -> Unit,
+ onBackClick: () -> Unit
+) {
+ val items by viewModel.favoriteItemsState.collectAsStateWithLifecycle()
+
+ when (items) {
+ is FavoriteItemsState.Loading ->
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ CircularProgressIndicator()
+ }
+ is FavoriteItemsState.ShowList ->
+ FavoritesScreen(
+ items = (items as FavoriteItemsState.ShowList).items,
+ navigateToDetails = onFavoriteClicked,
+ onBackClick = onBackClick)
+ is FavoriteItemsState.Error -> FavoritesErrorScreen(onBackClick = onBackClick)
+ else -> {}
+ }
+}
diff --git a/features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/feature/favorites/ui/FavoritesView.kt b/features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/feature/favorites/ui/FavoritesView.kt
deleted file mode 100644
index dcf57ed..0000000
--- a/features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/feature/favorites/ui/FavoritesView.kt
+++ /dev/null
@@ -1,52 +0,0 @@
-package com.manuelnunez.apps.feature.favorites.ui
-
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.WindowInsets
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.safeContent
-import androidx.compose.foundation.layout.windowInsetsPadding
-import androidx.compose.material3.Button
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.ui.Modifier
-import androidx.hilt.navigation.compose.hiltViewModel
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import com.manuelnunez.apps.core.domain.model.Item
-import com.manuelnunez.apps.core.ui.component.SurfaceText
-import java.text.SimpleDateFormat
-import java.util.Calendar
-
-@Composable
-fun FavoritesView(viewModel: FavoritesViewModel = hiltViewModel()) {
- val items by viewModel.favoriteItemsState.collectAsStateWithLifecycle()
- Column(Modifier.fillMaxSize().windowInsetsPadding(WindowInsets.safeContent)) {
- Button(
- onClick = {
- val currentDateTime = Calendar.getInstance().time
-
- // Define a date-time formatter (optional)
- val formatter = SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
-
- // Format the current date and time using the formatter (optional)
- val formattedDateTime = formatter.format(currentDateTime)
- viewModel.saveFavorite(Item(formattedDateTime, "", "", ""))
- }) {}
-
- Button(
- onClick = {
- val currentDateTime = Calendar.getInstance().time
-
- // Define a date-time formatter (optional)
- val formatter = SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
-
- // Format the current date and time using the formatter (optional)
- val formattedDateTime = formatter.format(currentDateTime)
- viewModel.removeFavorite(Item(formattedDateTime, "", "", ""))
- }) {}
- if (items is FavoritesViewModel.FavoriteItemsState.ShowList) {
- (items as FavoritesViewModel.FavoriteItemsState.ShowList).items.forEach {
- SurfaceText(text = it.photoId)
- }
- }
- }
-}
diff --git a/features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/feature/favorites/ui/FavoritesViewModel.kt b/features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/feature/favorites/ui/FavoritesViewModel.kt
index 61f140a..be7132f 100644
--- a/features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/feature/favorites/ui/FavoritesViewModel.kt
+++ b/features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/feature/favorites/ui/FavoritesViewModel.kt
@@ -4,8 +4,6 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.manuelnunez.apps.core.domain.model.Item
import com.manuelnunez.apps.features.favorites.domain.usecase.GetFavoritesUseCase
-import com.manuelnunez.apps.features.favorites.domain.usecase.RemoveFavoritesUseCase
-import com.manuelnunez.apps.features.favorites.domain.usecase.SaveFavoritesUseCase
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.catch
@@ -15,17 +13,16 @@ import kotlinx.coroutines.flow.onStart
import javax.inject.Inject
@HiltViewModel
-class FavoritesViewModel
-@Inject
-constructor(
- private val getFavoritesUseCase: GetFavoritesUseCase,
- private val saveFavoritesUseCase: SaveFavoritesUseCase,
- private val removeFavoritesUseCase: RemoveFavoritesUseCase
-) : ViewModel() {
+class FavoritesViewModel @Inject constructor(private val getFavoritesUseCase: GetFavoritesUseCase) :
+ ViewModel() {
val favoriteItemsState = MutableStateFlow(FavoriteItemsState.Idle)
init {
+ getFavorites()
+ }
+
+ private fun getFavorites() {
getFavoritesUseCase
.prepare(Unit)
.onStart { favoriteItemsState.value = FavoriteItemsState.Loading }
@@ -34,14 +31,6 @@ constructor(
.launchIn(viewModelScope)
}
- fun saveFavorite(item: Item) {
- saveFavoritesUseCase.prepare(item).catch {}.launchIn(viewModelScope)
- }
-
- fun removeFavorite(item: Item) {
- removeFavoritesUseCase.prepare(item).catch {}.launchIn(viewModelScope)
- }
-
sealed interface FavoriteItemsState {
data object Idle : FavoriteItemsState
diff --git a/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/features/detail/ui/components/DetailErrorScreen.kt b/features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/feature/favorites/ui/component/FavoritesErrorScreen.kt
similarity index 60%
rename from features/detail/ui/src/main/kotlin/com/manuelnunez/apps/features/detail/ui/components/DetailErrorScreen.kt
rename to features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/feature/favorites/ui/component/FavoritesErrorScreen.kt
index 436709f..3cf396e 100644
--- a/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/features/detail/ui/components/DetailErrorScreen.kt
+++ b/features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/feature/favorites/ui/component/FavoritesErrorScreen.kt
@@ -1,23 +1,22 @@
-package com.manuelnunez.apps.features.detail.ui.components
+package com.manuelnunez.apps.feature.favorites.ui.component
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.manuelnunez.apps.core.ui.component.ErrorDialog
import com.manuelnunez.apps.core.ui.theme.MainTheme
import com.manuelnunez.apps.core.ui.utils.OrientationPreviews
-import com.manuelnunez.apps.features.detail.ui.R
import com.manuelnunez.apps.core.ui.R as RCU
@Composable
-fun DetailErrorScreen(onBackClick: () -> Unit) {
+fun FavoritesErrorScreen(onBackClick: () -> Unit) {
ErrorDialog(
onConfirmClick = onBackClick,
dialogTitle = stringResource(id = RCU.string.alert_error_title),
- dialogText = stringResource(id = R.string.alert_error_try_again_back))
+ dialogText = stringResource(id = RCU.string.alert_error_try_again_back))
}
@OrientationPreviews
@Composable
-fun DetailErrorScreenPreview() {
- MainTheme { DetailErrorScreen(onBackClick = {}) }
+fun FavoritesErrorScreenPreview() {
+ MainTheme { FavoritesErrorScreen(onBackClick = {}) }
}
diff --git a/features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/feature/favorites/ui/component/FavoritesScreen.kt b/features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/feature/favorites/ui/component/FavoritesScreen.kt
new file mode 100644
index 0000000..2bb9b8f
--- /dev/null
+++ b/features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/feature/favorites/ui/component/FavoritesScreen.kt
@@ -0,0 +1,68 @@
+package com.manuelnunez.apps.feature.favorites.ui.component
+
+import android.content.res.Configuration
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.safeContent
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.windowInsetsPadding
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.grid.items
+import androidx.compose.foundation.lazy.grid.rememberLazyGridState
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import com.manuelnunez.apps.core.domain.model.Item
+import com.manuelnunez.apps.core.ui.component.ImageCard
+import com.manuelnunez.apps.core.ui.component.SurfaceText
+import com.manuelnunez.apps.features.favorites.ui.R
+
+@Composable
+fun FavoritesScreen(items: List- , navigateToDetails: (Item) -> Unit, onBackClick: () -> Unit) {
+ Column(Modifier.fillMaxSize().windowInsetsPadding(WindowInsets.safeContent)) {
+ Spacer(modifier = Modifier.height(20.dp))
+
+ SurfaceText(
+ modifier = Modifier.padding(vertical = 6.dp, horizontal = 20.dp),
+ text = stringResource(id = R.string.section_favorites))
+
+ FavoriteItems(navigateToDetails = navigateToDetails, favoriteItems = items)
+
+ Spacer(modifier = Modifier.height(20.dp))
+ }
+}
+
+@Composable
+private fun FavoriteItems(navigateToDetails: (Item) -> Unit, favoriteItems: List
- ) {
+ val gridState = rememberLazyGridState()
+
+ val cellConfiguration =
+ if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE) {
+ GridCells.Adaptive(minSize = 100.dp)
+ } else GridCells.Fixed(3)
+
+ LazyVerticalGrid(
+ columns = cellConfiguration,
+ modifier = Modifier.fillMaxSize(),
+ state = gridState,
+ contentPadding = PaddingValues(start = 20.dp, end = 20.dp, top = 10.dp, bottom = 20.dp),
+ verticalArrangement = Arrangement.spacedBy(10.dp),
+ horizontalArrangement = Arrangement.spacedBy(10.dp)) {
+ items(items = favoriteItems) { item ->
+ ImageCard(
+ modifier = Modifier.size(width = 100.dp, height = 200.dp),
+ imageUrl = item.imageUrl,
+ cardContentDescription = item.description,
+ onClick = { navigateToDetails.invoke(item) })
+ }
+ }
+}
diff --git a/features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/feature/favorites/ui/navigation/FavoritesNavigation.kt b/features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/feature/favorites/ui/navigation/FavoritesNavigation.kt
index d5b40d1..c2554c7 100644
--- a/features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/feature/favorites/ui/navigation/FavoritesNavigation.kt
+++ b/features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/feature/favorites/ui/navigation/FavoritesNavigation.kt
@@ -1,16 +1,14 @@
package com.manuelnunez.apps.feature.favorites.ui.navigation
-import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
-import androidx.navigation.NavOptions
import androidx.navigation.compose.composable
-import com.manuelnunez.apps.feature.favorites.ui.FavoritesView
+import com.manuelnunez.apps.core.domain.model.Item
+import com.manuelnunez.apps.feature.favorites.ui.FavoritesRoute
const val FAVORITES_ROUTE = "favorites_route"
-fun NavController.navigateToFavorites(navOptions: NavOptions) =
- navigate(FAVORITES_ROUTE, navOptions)
-
-fun NavGraphBuilder.favoritesScreen() {
- composable(route = FAVORITES_ROUTE) { FavoritesView() }
+fun NavGraphBuilder.favoritesScreen(navigateToDetails: (Item) -> Unit, onBackClick: () -> Unit) {
+ composable(route = FAVORITES_ROUTE) {
+ FavoritesRoute(onFavoriteClicked = navigateToDetails, onBackClick = onBackClick)
+ }
}
diff --git a/features/favorites/ui/src/main/res/values/strings.xml b/features/favorites/ui/src/main/res/values/strings.xml
new file mode 100644
index 0000000..6e330c5
--- /dev/null
+++ b/features/favorites/ui/src/main/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+
+ Favorites
+
\ No newline at end of file
diff --git a/features/home/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/home/ui/HomeViewTest.kt b/features/home/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/home/ui/HomeViewTest.kt
index 6b22b4d..b2a05f8 100644
--- a/features/home/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/home/ui/HomeViewTest.kt
+++ b/features/home/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/home/ui/HomeViewTest.kt
@@ -21,9 +21,9 @@ class HomeViewTest {
composeTestRule.setContent {
HomeScreen(
items =
- HomeScreenViewModel.HomeUiState(
- popularItemsState = HomeScreenViewModel.PopularItemsState.Loading,
- featuredItemsState = HomeScreenViewModel.FeaturedItemsState.Loading),
+ HomeViewModel.HomeUiState(
+ popularItemsState = HomeViewModel.PopularItemsState.Loading,
+ featuredItemsState = HomeViewModel.FeaturedItemsState.Loading),
navigateToDetails = {},
navigateToSeeMore = {})
}
@@ -47,11 +47,10 @@ class HomeViewTest {
composeTestRule.setContent {
HomeScreen(
items =
- HomeScreenViewModel.HomeUiState(
- popularItemsState =
- HomeScreenViewModel.PopularItemsState.ShowList(mockPopularPhotos),
+ HomeViewModel.HomeUiState(
+ popularItemsState = HomeViewModel.PopularItemsState.ShowList(mockPopularPhotos),
featuredItemsState =
- HomeScreenViewModel.FeaturedItemsState.ShowList(mockFeaturedPhotos)),
+ HomeViewModel.FeaturedItemsState.ShowList(mockFeaturedPhotos)),
navigateToDetails = {},
navigateToSeeMore = {})
}
@@ -106,10 +105,9 @@ class HomeViewTest {
composeTestRule.setContent {
HomeScreen(
items =
- HomeScreenViewModel.HomeUiState(
- popularItemsState =
- HomeScreenViewModel.PopularItemsState.ShowList(mockPopularPhotos),
- featuredItemsState = HomeScreenViewModel.FeaturedItemsState.Error),
+ HomeViewModel.HomeUiState(
+ popularItemsState = HomeViewModel.PopularItemsState.ShowList(mockPopularPhotos),
+ featuredItemsState = HomeViewModel.FeaturedItemsState.Error),
navigateToDetails = {},
navigateToSeeMore = {})
}
@@ -127,10 +125,10 @@ class HomeViewTest {
composeTestRule.setContent {
HomeScreen(
items =
- HomeScreenViewModel.HomeUiState(
- popularItemsState = HomeScreenViewModel.PopularItemsState.Error,
+ HomeViewModel.HomeUiState(
+ popularItemsState = HomeViewModel.PopularItemsState.Error,
featuredItemsState =
- HomeScreenViewModel.FeaturedItemsState.ShowList(mockFeaturedPhotos)),
+ HomeViewModel.FeaturedItemsState.ShowList(mockFeaturedPhotos)),
navigateToDetails = {},
navigateToSeeMore = {})
}
diff --git a/features/home/ui/src/main/kotlin/com/manuelnunez/apps/features/home/ui/HomeView.kt b/features/home/ui/src/main/kotlin/com/manuelnunez/apps/features/home/ui/HomeRoute.kt
similarity index 77%
rename from features/home/ui/src/main/kotlin/com/manuelnunez/apps/features/home/ui/HomeView.kt
rename to features/home/ui/src/main/kotlin/com/manuelnunez/apps/features/home/ui/HomeRoute.kt
index d99b80f..394c70b 100644
--- a/features/home/ui/src/main/kotlin/com/manuelnunez/apps/features/home/ui/HomeView.kt
+++ b/features/home/ui/src/main/kotlin/com/manuelnunez/apps/features/home/ui/HomeRoute.kt
@@ -5,23 +5,23 @@ import androidx.compose.runtime.getValue
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.manuelnunez.apps.core.domain.model.Item
-import com.manuelnunez.apps.features.home.ui.HomeScreenViewModel.FeaturedItemsState
-import com.manuelnunez.apps.features.home.ui.HomeScreenViewModel.PopularItemsState
+import com.manuelnunez.apps.features.home.ui.HomeViewModel.FeaturedItemsState
+import com.manuelnunez.apps.features.home.ui.HomeViewModel.PopularItemsState
import com.manuelnunez.apps.features.home.ui.components.HomeErrorScreen
import com.manuelnunez.apps.features.home.ui.components.HomeScreen
@Composable
-fun HomeView(
- viewModel: HomeScreenViewModel = hiltViewModel(),
+fun HomeRoute(
+ viewModel: HomeViewModel = hiltViewModel(),
navigateToDetails: (Item) -> Unit,
navigateToSeeMore: () -> Unit
) {
val items by viewModel.state.collectAsStateWithLifecycle()
+ HomeScreen(items, navigateToDetails, navigateToSeeMore)
+
if (items.popularItemsState is PopularItemsState.Error ||
items.featuredItemsState is FeaturedItemsState.Error) {
HomeErrorScreen { viewModel.getItems() }
}
-
- HomeScreen(items, navigateToDetails, navigateToSeeMore)
}
diff --git a/features/home/ui/src/main/kotlin/com/manuelnunez/apps/features/home/ui/HomeScreenViewModel.kt b/features/home/ui/src/main/kotlin/com/manuelnunez/apps/features/home/ui/HomeViewModel.kt
similarity index 99%
rename from features/home/ui/src/main/kotlin/com/manuelnunez/apps/features/home/ui/HomeScreenViewModel.kt
rename to features/home/ui/src/main/kotlin/com/manuelnunez/apps/features/home/ui/HomeViewModel.kt
index a00b29b..52e4eca 100644
--- a/features/home/ui/src/main/kotlin/com/manuelnunez/apps/features/home/ui/HomeScreenViewModel.kt
+++ b/features/home/ui/src/main/kotlin/com/manuelnunez/apps/features/home/ui/HomeViewModel.kt
@@ -18,7 +18,7 @@ import kotlinx.coroutines.flow.stateIn
import javax.inject.Inject
@HiltViewModel
-class HomeScreenViewModel
+class HomeViewModel
@Inject
constructor(
private val getFeaturedItemsUseCase: GetFeaturedItemsUseCase,
diff --git a/features/home/ui/src/main/kotlin/com/manuelnunez/apps/features/home/ui/components/HomeScreen.kt b/features/home/ui/src/main/kotlin/com/manuelnunez/apps/features/home/ui/components/HomeScreen.kt
index 6f52fb5..c8bb5e2 100644
--- a/features/home/ui/src/main/kotlin/com/manuelnunez/apps/features/home/ui/components/HomeScreen.kt
+++ b/features/home/ui/src/main/kotlin/com/manuelnunez/apps/features/home/ui/components/HomeScreen.kt
@@ -33,9 +33,9 @@ import com.manuelnunez.apps.core.ui.component.TextCard
import com.manuelnunez.apps.core.ui.theme.MainTheme
import com.manuelnunez.apps.core.ui.utils.FontScalingPreviews
import com.manuelnunez.apps.core.ui.utils.ThemePreviews
-import com.manuelnunez.apps.features.home.ui.HomeScreenViewModel.FeaturedItemsState
-import com.manuelnunez.apps.features.home.ui.HomeScreenViewModel.HomeUiState
-import com.manuelnunez.apps.features.home.ui.HomeScreenViewModel.PopularItemsState
+import com.manuelnunez.apps.features.home.ui.HomeViewModel.FeaturedItemsState
+import com.manuelnunez.apps.features.home.ui.HomeViewModel.HomeUiState
+import com.manuelnunez.apps.features.home.ui.HomeViewModel.PopularItemsState
import com.manuelnunez.apps.features.home.ui.R
import com.manuelnunez.apps.core.ui.R as RCU
@@ -165,7 +165,7 @@ private fun LoadingIndicator(loaderContentDescription: String) {
@Composable
private fun HomeScreenPreview() {
MainTheme {
- val items = List(5) { Item("", "", description = "", thumbnailUrl = "") }
+ val items = List(5) { Item.empty }
HomeScreen(
items =
diff --git a/features/home/ui/src/main/kotlin/com/manuelnunez/apps/features/home/ui/navigation/HomeNavigation.kt b/features/home/ui/src/main/kotlin/com/manuelnunez/apps/features/home/ui/navigation/HomeNavigation.kt
index a15bdd8..114801f 100644
--- a/features/home/ui/src/main/kotlin/com/manuelnunez/apps/features/home/ui/navigation/HomeNavigation.kt
+++ b/features/home/ui/src/main/kotlin/com/manuelnunez/apps/features/home/ui/navigation/HomeNavigation.kt
@@ -1,18 +1,14 @@
package com.manuelnunez.apps.features.home.ui.navigation
-import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
-import androidx.navigation.NavOptions
import androidx.navigation.compose.composable
import com.manuelnunez.apps.core.domain.model.Item
-import com.manuelnunez.apps.features.home.ui.HomeView
+import com.manuelnunez.apps.features.home.ui.HomeRoute
const val HOME_ROUTE = "home_route"
-fun NavController.navigateToHome(navOptions: NavOptions) = navigate(HOME_ROUTE, navOptions)
-
fun NavGraphBuilder.homeScreen(navigateToDetails: (Item) -> Unit, navigateToSeeMore: () -> Unit) {
composable(route = HOME_ROUTE) {
- HomeView(navigateToDetails = navigateToDetails, navigateToSeeMore = navigateToSeeMore)
+ HomeRoute(navigateToDetails = navigateToDetails, navigateToSeeMore = navigateToSeeMore)
}
}
diff --git a/features/home/ui/src/test/kotlin/com/manuelnunez/apps/features/home/ui/viewmodel/HomeScreenViewModelTest.kt b/features/home/ui/src/test/kotlin/com/manuelnunez/apps/features/home/ui/viewmodel/HomeViewModelTest.kt
similarity index 88%
rename from features/home/ui/src/test/kotlin/com/manuelnunez/apps/features/home/ui/viewmodel/HomeScreenViewModelTest.kt
rename to features/home/ui/src/test/kotlin/com/manuelnunez/apps/features/home/ui/viewmodel/HomeViewModelTest.kt
index 4d8c002..a0c77f2 100644
--- a/features/home/ui/src/test/kotlin/com/manuelnunez/apps/features/home/ui/viewmodel/HomeScreenViewModelTest.kt
+++ b/features/home/ui/src/test/kotlin/com/manuelnunez/apps/features/home/ui/viewmodel/HomeViewModelTest.kt
@@ -9,9 +9,9 @@ import com.manuelnunez.apps.core.domain.model.ErrorModel
import com.manuelnunez.apps.core.domain.model.Item
import com.manuelnunez.apps.features.home.domain.usecase.GetFeaturedItemsUseCase
import com.manuelnunez.apps.features.home.domain.usecase.GetPopularItemsUseCase
-import com.manuelnunez.apps.features.home.ui.HomeScreenViewModel
-import com.manuelnunez.apps.features.home.ui.HomeScreenViewModel.FeaturedItemsState
-import com.manuelnunez.apps.features.home.ui.HomeScreenViewModel.PopularItemsState
+import com.manuelnunez.apps.features.home.ui.HomeViewModel
+import com.manuelnunez.apps.features.home.ui.HomeViewModel.FeaturedItemsState
+import com.manuelnunez.apps.features.home.ui.HomeViewModel.PopularItemsState
import io.mockk.confirmVerified
import io.mockk.every
import io.mockk.mockk
@@ -25,14 +25,14 @@ import org.junit.jupiter.api.extension.RegisterExtension
import kotlin.properties.Delegates
@OptIn(ExperimentalCoroutinesApi::class)
-class HomeScreenViewModelTest {
+class HomeViewModelTest {
@RegisterExtension private val mockkAllExtension = MockkAllRule()
@RegisterExtension private val unMockkAllExtension = UnMockkAllRule()
private val getFeaturedItemsUseCase = mockk()
private val getPopularItemsUseCase = mockk()
- private var viewModel: HomeScreenViewModel by Delegates.notNull()
+ private var viewModel: HomeViewModel by Delegates.notNull()
@Test
fun `GIVEN viewmodel init, WHEN onSuccess, THEN set state with popular and featured items`() =
@@ -42,7 +42,7 @@ class HomeScreenViewModelTest {
every { getPopularItemsUseCase.prepare(Unit) } returns
flow { emit(eitherSuccess(mockPhotos)) }
- viewModel = HomeScreenViewModel(getFeaturedItemsUseCase, getPopularItemsUseCase)
+ viewModel = HomeViewModel(getFeaturedItemsUseCase, getPopularItemsUseCase)
viewModel.state.test {
// GIVEN viewModel INIT
@@ -76,7 +76,7 @@ class HomeScreenViewModelTest {
every { getPopularItemsUseCase.prepare(Unit) } returns
flow { emit(eitherError(ErrorModel.ServiceError)) }
- viewModel = HomeScreenViewModel(getFeaturedItemsUseCase, getPopularItemsUseCase)
+ viewModel = HomeViewModel(getFeaturedItemsUseCase, getPopularItemsUseCase)
viewModel.state.test {
// GIVEN viewModel INIT
diff --git a/features/seemore/ui/src/main/kotlin/com/manuelnunez/apps/features/seemore/ui/SeeMoreView.kt b/features/seemore/ui/src/main/kotlin/com/manuelnunez/apps/features/seemore/ui/SeeMoreRoute.kt
similarity index 98%
rename from features/seemore/ui/src/main/kotlin/com/manuelnunez/apps/features/seemore/ui/SeeMoreView.kt
rename to features/seemore/ui/src/main/kotlin/com/manuelnunez/apps/features/seemore/ui/SeeMoreRoute.kt
index 4d569b6..5b9b9f6 100644
--- a/features/seemore/ui/src/main/kotlin/com/manuelnunez/apps/features/seemore/ui/SeeMoreView.kt
+++ b/features/seemore/ui/src/main/kotlin/com/manuelnunez/apps/features/seemore/ui/SeeMoreRoute.kt
@@ -14,7 +14,7 @@ import com.manuelnunez.apps.features.seemore.ui.components.SeeMoreErrorScreen
import com.manuelnunez.apps.features.seemore.ui.components.SeeMoreScreen
@Composable
-fun SeeMoreView(
+fun SeeMoreRoute(
viewModel: SeeMoreViewModel = hiltViewModel(),
onBackClick: () -> Unit,
navigateToDetails: (Item) -> Unit
diff --git a/features/seemore/ui/src/main/kotlin/com/manuelnunez/apps/features/seemore/ui/components/SeeMoreScreen.kt b/features/seemore/ui/src/main/kotlin/com/manuelnunez/apps/features/seemore/ui/components/SeeMoreScreen.kt
index 6b9fde0..17f0002 100644
--- a/features/seemore/ui/src/main/kotlin/com/manuelnunez/apps/features/seemore/ui/components/SeeMoreScreen.kt
+++ b/features/seemore/ui/src/main/kotlin/com/manuelnunez/apps/features/seemore/ui/components/SeeMoreScreen.kt
@@ -4,11 +4,9 @@ import android.content.res.Configuration
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeContent
@@ -17,14 +15,8 @@ import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material3.CircularProgressIndicator
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.stringResource
@@ -35,8 +27,8 @@ import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.itemKey
import com.manuelnunez.apps.core.domain.model.Item
+import com.manuelnunez.apps.core.ui.component.BackToolbar
import com.manuelnunez.apps.core.ui.component.ImageCard
-import com.manuelnunez.apps.core.ui.component.SurfaceText
import com.manuelnunez.apps.core.ui.theme.MainTheme
import com.manuelnunez.apps.core.ui.utils.FontScalingPreviews
import com.manuelnunez.apps.core.ui.utils.ThemePreviews
@@ -52,7 +44,7 @@ fun SeeMoreScreen(
Column(Modifier.fillMaxSize().windowInsetsPadding(WindowInsets.safeContent)) {
Spacer(modifier = Modifier.height(20.dp))
- SeeMoreToolbar(onBackClick)
+ BackToolbar(title = stringResource(id = RCU.string.section_popular), onBackClick = onBackClick)
PopularItems(navigateToDetails = navigateToDetails, itemsPagingItems = items)
@@ -98,20 +90,6 @@ fun PopularItems(itemsPagingItems: LazyPagingItems
- , navigateToDetails: (It
}
}
-@Composable
-private fun SeeMoreToolbar(onBackClick: () -> Unit) {
- Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
- IconButton(onClick = onBackClick) {
- Icon(
- imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
- contentDescription = stringResource(id = RCU.string.button_back),
- tint = MaterialTheme.colorScheme.onSurface)
- }
-
- SurfaceText(text = stringResource(id = RCU.string.section_popular))
- }
-}
-
@FontScalingPreviews
@ThemePreviews
@Composable
diff --git a/features/seemore/ui/src/main/kotlin/com/manuelnunez/apps/features/seemore/ui/navigation/SeeMoreNavigation.kt b/features/seemore/ui/src/main/kotlin/com/manuelnunez/apps/features/seemore/ui/navigation/SeeMoreNavigation.kt
index 85b420a..28e5941 100644
--- a/features/seemore/ui/src/main/kotlin/com/manuelnunez/apps/features/seemore/ui/navigation/SeeMoreNavigation.kt
+++ b/features/seemore/ui/src/main/kotlin/com/manuelnunez/apps/features/seemore/ui/navigation/SeeMoreNavigation.kt
@@ -5,9 +5,9 @@ import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptionsBuilder
import androidx.navigation.compose.composable
import com.manuelnunez.apps.core.domain.model.Item
-import com.manuelnunez.apps.features.seemore.ui.SeeMoreView
+import com.manuelnunez.apps.features.seemore.ui.SeeMoreRoute
-const val SEE_MORE_ROUTE = "see_more"
+const val SEE_MORE_ROUTE = "see_more_route"
fun NavController.navigateToSeeMore(navOptions: NavOptionsBuilder.() -> Unit = {}) {
navigate(SEE_MORE_ROUTE, navOptions)
@@ -15,6 +15,6 @@ fun NavController.navigateToSeeMore(navOptions: NavOptionsBuilder.() -> Unit = {
fun NavGraphBuilder.seeMoreScreen(onBackClick: () -> Unit, navigateToDetails: (Item) -> Unit) {
composable(SEE_MORE_ROUTE) {
- SeeMoreView(onBackClick = onBackClick, navigateToDetails = navigateToDetails)
+ SeeMoreRoute(onBackClick = onBackClick, navigateToDetails = navigateToDetails)
}
}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 32e209a..7d89c41 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -21,6 +21,7 @@ retrofit = "2.9.0"
okhttp = "4.12.0"
coil = "2.6.0"
coroutines = "1.8.0"
+kotlinSerializer = "1.3.0"
uiTooling = "1.6.5"
mockk = "1.13.10"
turbine = "1.0.0"
@@ -50,6 +51,7 @@ androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", vers
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
+kotlinx-serializer = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinSerializer" }
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }
hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt" }
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 95086b8..7fce6e2 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -30,6 +30,8 @@ include(":core:common")
include(":core:data")
+include(":core:datastore-proto")
+
include(":core:domain")
include(":core:services")
@@ -40,6 +42,8 @@ include(":features:home:domain")
include(":features:home:ui")
+include(":features:detail:domain")
+
include(":features:detail:ui")
include(":features:seemore:ui")
@@ -49,5 +53,3 @@ include(":features:seemore:domain")
include(":features:favorites:ui")
include(":features:favorites:domain")
-
-include(":core:datastore-proto")
From 1fdcff0f702fcec978186d19354ed43941084790 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Manuel=20Nu=C3=B1ez?= <03.manu@gmail.com>
Date: Thu, 18 Apr 2024 23:49:27 -0400
Subject: [PATCH 7/8] Added favorite tests (I)#49
---
.../apps/navigation/MainNavigation.kt | 4 +-
.../data/repository/DetailRepositoryTest.kt | 56 +++++++++++++
.../repository/FavoritesRepositoryTest.kt | 33 ++++++++
.../usecase/GetFavoriteStatusUseCaseTest.kt | 41 ++++++++++
.../detail/domain/usecase/MockUtils.kt | 10 +++
.../usecase/RemoveFavoritesUseCaseTest.kt | 37 +++++++++
.../usecase/SaveFavoritesUseCaseTest.kt | 37 +++++++++
...{DetailViewTest.kt => DetailScreenTest.kt} | 36 +++------
.../detail/ui/components/DetailScreen.kt | 39 ++++-----
.../detail/ui/src/main/res/values/strings.xml | 1 +
.../domain/usecase/GetFavoritesUseCaseTest.kt | 50 ++++++++++++
.../favorites/ui/FavoritesScreenTest.kt | 70 ++++++++++++++++
.../favorites/ui/FavoritesRoute.kt | 8 +-
.../favorites/ui/FavoritesViewModel.kt | 2 +-
.../ui/component/FavoritesErrorScreen.kt | 2 +-
.../favorites/ui/component/FavoritesScreen.kt | 2 +-
.../ui/navigation/FavoritesNavigation.kt | 4 +-
.../favorites/ui/FavoritesViewModelTest.kt | 81 +++++++++++++++++++
.../ui/{HomeViewTest.kt => HomeScreenTest.kt} | 31 +++----
.../apps/features/home/ui/HomeViewModel.kt | 2 +-
.../home/ui/viewmodel/HomeViewModelTest.kt | 31 +++++++
...eeMoreViewTest.kt => SeeMoreScreenTest.kt} | 6 +-
22 files changed, 508 insertions(+), 75 deletions(-)
create mode 100644 core/data/src/test/kotlin/com/manuelnunez/apps/core/data/repository/DetailRepositoryTest.kt
create mode 100644 core/data/src/test/kotlin/com/manuelnunez/apps/core/data/repository/FavoritesRepositoryTest.kt
create mode 100644 features/detail/domain/src/test/kotlin/com/manuelnunez/apps/features/detail/domain/usecase/GetFavoriteStatusUseCaseTest.kt
create mode 100644 features/detail/domain/src/test/kotlin/com/manuelnunez/apps/features/detail/domain/usecase/MockUtils.kt
create mode 100644 features/detail/domain/src/test/kotlin/com/manuelnunez/apps/features/detail/domain/usecase/RemoveFavoritesUseCaseTest.kt
create mode 100644 features/detail/domain/src/test/kotlin/com/manuelnunez/apps/features/detail/domain/usecase/SaveFavoritesUseCaseTest.kt
rename features/detail/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/detail/ui/{DetailViewTest.kt => DetailScreenTest.kt} (71%)
create mode 100644 features/favorites/domain/src/test/kotlin/com/manuelnunez/apps/features/favorites/domain/usecase/GetFavoritesUseCaseTest.kt
create mode 100644 features/favorites/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/favorites/ui/FavoritesScreenTest.kt
rename features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/{feature => features}/favorites/ui/FavoritesRoute.kt (80%)
rename features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/{feature => features}/favorites/ui/FavoritesViewModel.kt (96%)
rename features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/{feature => features}/favorites/ui/component/FavoritesErrorScreen.kt (92%)
rename features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/{feature => features}/favorites/ui/component/FavoritesScreen.kt (97%)
rename features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/{feature => features}/favorites/ui/navigation/FavoritesNavigation.kt (76%)
create mode 100644 features/favorites/ui/src/test/kotlin/com/manuelnunez/apps/features/favorites/ui/FavoritesViewModelTest.kt
rename features/home/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/home/ui/{HomeViewTest.kt => HomeScreenTest.kt} (81%)
rename features/seemore/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/seemore/ui/{SeeMoreViewTest.kt => SeeMoreScreenTest.kt} (94%)
diff --git a/app/src/main/kotlin/com/manuelnunez/apps/navigation/MainNavigation.kt b/app/src/main/kotlin/com/manuelnunez/apps/navigation/MainNavigation.kt
index cdb8ea0..f3765aa 100644
--- a/app/src/main/kotlin/com/manuelnunez/apps/navigation/MainNavigation.kt
+++ b/app/src/main/kotlin/com/manuelnunez/apps/navigation/MainNavigation.kt
@@ -14,12 +14,12 @@ import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.navigation
import com.manuelnunez.apps.R
-import com.manuelnunez.apps.feature.favorites.ui.navigation.FAVORITES_ROUTE
-import com.manuelnunez.apps.feature.favorites.ui.navigation.favoritesScreen
import com.manuelnunez.apps.features.detail.ui.navigation.detailFavScreen
import com.manuelnunez.apps.features.detail.ui.navigation.detailScreen
import com.manuelnunez.apps.features.detail.ui.navigation.navigateToDetail
import com.manuelnunez.apps.features.detail.ui.navigation.navigateToDetailFav
+import com.manuelnunez.apps.features.favorites.ui.navigation.FAVORITES_ROUTE
+import com.manuelnunez.apps.features.favorites.ui.navigation.favoritesScreen
import com.manuelnunez.apps.features.home.ui.navigation.HOME_ROUTE
import com.manuelnunez.apps.features.home.ui.navigation.homeScreen
import com.manuelnunez.apps.features.seemore.ui.navigation.navigateToSeeMore
diff --git a/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/repository/DetailRepositoryTest.kt b/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/repository/DetailRepositoryTest.kt
new file mode 100644
index 0000000..0fd6eb2
--- /dev/null
+++ b/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/repository/DetailRepositoryTest.kt
@@ -0,0 +1,56 @@
+package com.manuelnunez.apps.core.data.repository
+
+import com.manuelnunez.apps.core.data.datasource.local.FavoritesDataSource
+import com.manuelnunez.apps.core.data.utils.mockItems
+import com.manuelnunez.apps.features.detail.domain.repository.DetailRepository
+import io.mockk.coJustRun
+import io.mockk.coVerify
+import io.mockk.confirmVerified
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.verify
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.test.runTest
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+
+class DetailRepositoryTest {
+ private lateinit var repository: DetailRepository
+
+ private val remoteDataSource = mockk()
+
+ @BeforeEach
+ fun setUp() {
+ repository = DetailRepositoryImpl(remoteDataSource)
+ }
+
+ @Test
+ fun `call to saveFavoriteItem invokes addItemToFavorites from datasource`() = runTest {
+ coJustRun { remoteDataSource.addItemToFavorites(mockItems[0]) }
+
+ repository.saveFavoriteItem(mockItems[0])
+
+ coVerify(exactly = 1) { remoteDataSource.addItemToFavorites(mockItems[0]) }
+ confirmVerified(remoteDataSource)
+ }
+
+ @Test
+ fun `call to removeFavoriteItem invokes removeItemFromFavorites from datasource`() = runTest {
+ coJustRun { remoteDataSource.removeItemFromFavorites(mockItems[0].photoId) }
+
+ repository.removeFavoriteItem(mockItems[0])
+
+ coVerify(exactly = 1) { remoteDataSource.removeItemFromFavorites(mockItems[0].photoId) }
+ confirmVerified(remoteDataSource)
+ }
+
+ @Test
+ fun `call to isItemFavorite invokes isItemFavorite from datasource`() {
+ every { remoteDataSource.isItemFavorite(mockItems[0].photoId) } returns flow { emit(true) }
+
+ repository.isItemFavorite(mockItems[0].photoId)
+
+ verify(exactly = 1) { remoteDataSource.isItemFavorite(mockItems[0].photoId) }
+ confirmVerified(remoteDataSource)
+ }
+}
diff --git a/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/repository/FavoritesRepositoryTest.kt b/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/repository/FavoritesRepositoryTest.kt
new file mode 100644
index 0000000..c044591
--- /dev/null
+++ b/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/repository/FavoritesRepositoryTest.kt
@@ -0,0 +1,33 @@
+package com.manuelnunez.apps.core.data.repository
+
+import com.manuelnunez.apps.core.data.datasource.local.FavoritesDataSource
+import com.manuelnunez.apps.core.data.utils.mockItems
+import com.manuelnunez.apps.features.favorites.domain.repository.FavoritesRepository
+import io.mockk.confirmVerified
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.verify
+import kotlinx.coroutines.flow.flow
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+
+class FavoritesRepositoryTest {
+ private lateinit var repository: FavoritesRepository
+
+ private val remoteDataSource = mockk()
+
+ @BeforeEach
+ fun setUp() {
+ repository = FavoritesRepositoryImpl(remoteDataSource)
+ }
+
+ @Test
+ fun `call to getAllFavorites returns item data from datasource`() {
+ every { remoteDataSource.favorites } returns flow { emit(mockItems) }
+
+ repository.getAllFavorites()
+
+ verify(exactly = 1) { remoteDataSource.favorites }
+ confirmVerified(remoteDataSource)
+ }
+}
diff --git a/features/detail/domain/src/test/kotlin/com/manuelnunez/apps/features/detail/domain/usecase/GetFavoriteStatusUseCaseTest.kt b/features/detail/domain/src/test/kotlin/com/manuelnunez/apps/features/detail/domain/usecase/GetFavoriteStatusUseCaseTest.kt
new file mode 100644
index 0000000..aceecd1
--- /dev/null
+++ b/features/detail/domain/src/test/kotlin/com/manuelnunez/apps/features/detail/domain/usecase/GetFavoriteStatusUseCaseTest.kt
@@ -0,0 +1,41 @@
+package com.manuelnunez.apps.features.detail.domain.usecase
+
+import app.cash.turbine.test
+import com.manuelnunez.apps.core.common.test.MockkAllRule
+import com.manuelnunez.apps.core.common.test.UnMockkAllRule
+import com.manuelnunez.apps.features.detail.domain.repository.DetailRepository
+import io.mockk.coVerify
+import io.mockk.confirmVerified
+import io.mockk.every
+import io.mockk.mockk
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.flow
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.RegisterExtension
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class GetFavoriteStatusUseCaseTest {
+ @RegisterExtension private val mockkAllExtension = MockkAllRule()
+ @RegisterExtension private val unMockkAllExtension = UnMockkAllRule()
+
+ private val detailRepository = mockk()
+ private lateinit var useCase: GetFavoriteStatusUseCase
+
+ @BeforeEach
+ fun setUp() {
+ useCase =
+ GetFavoriteStatusUseCase(
+ detailRepository, mockkAllExtension.testCoroutineDispatcherProvider)
+ }
+
+ @Test
+ fun `call GetFavoriteStatusUseCase invokes isItemFavorite from repository`() =
+ mockkAllExtension.runTest {
+ every { detailRepository.isItemFavorite(mockItem.photoId) } returns flow { emit(false) }
+ useCase.prepare(mockItem.photoId).test {}
+
+ coVerify(exactly = 1) { detailRepository.isItemFavorite(mockItem.photoId) }
+ confirmVerified(detailRepository)
+ }
+}
diff --git a/features/detail/domain/src/test/kotlin/com/manuelnunez/apps/features/detail/domain/usecase/MockUtils.kt b/features/detail/domain/src/test/kotlin/com/manuelnunez/apps/features/detail/domain/usecase/MockUtils.kt
new file mode 100644
index 0000000..860e1ab
--- /dev/null
+++ b/features/detail/domain/src/test/kotlin/com/manuelnunez/apps/features/detail/domain/usecase/MockUtils.kt
@@ -0,0 +1,10 @@
+package com.manuelnunez.apps.features.detail.domain.usecase
+
+import com.manuelnunez.apps.core.domain.model.Item
+
+val mockItem =
+ Item(
+ "1324",
+ "https://example.com/1324",
+ description = "description: 1324",
+ thumbnailUrl = "https://example.com/1324")
diff --git a/features/detail/domain/src/test/kotlin/com/manuelnunez/apps/features/detail/domain/usecase/RemoveFavoritesUseCaseTest.kt b/features/detail/domain/src/test/kotlin/com/manuelnunez/apps/features/detail/domain/usecase/RemoveFavoritesUseCaseTest.kt
new file mode 100644
index 0000000..d3689d7
--- /dev/null
+++ b/features/detail/domain/src/test/kotlin/com/manuelnunez/apps/features/detail/domain/usecase/RemoveFavoritesUseCaseTest.kt
@@ -0,0 +1,37 @@
+package com.manuelnunez.apps.features.detail.domain.usecase
+
+import app.cash.turbine.test
+import com.manuelnunez.apps.core.common.test.MockkAllRule
+import com.manuelnunez.apps.core.common.test.UnMockkAllRule
+import com.manuelnunez.apps.features.detail.domain.repository.DetailRepository
+import io.mockk.coVerify
+import io.mockk.confirmVerified
+import io.mockk.mockk
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.RegisterExtension
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class RemoveFavoritesUseCaseTest {
+ @RegisterExtension private val mockkAllExtension = MockkAllRule()
+ @RegisterExtension private val unMockkAllExtension = UnMockkAllRule()
+
+ private val detailRepository = mockk()
+ private lateinit var useCase: RemoveFavoritesUseCase
+
+ @BeforeEach
+ fun setUp() {
+ useCase =
+ RemoveFavoritesUseCase(detailRepository, mockkAllExtension.testCoroutineDispatcherProvider)
+ }
+
+ @Test
+ fun `call RemoveFavoritesUseCase invokes removeFavoriteItem from repository`() =
+ mockkAllExtension.runTest {
+ useCase.prepare(mockItem).test {}
+
+ coVerify(exactly = 1) { detailRepository.removeFavoriteItem(mockItem) }
+ confirmVerified(detailRepository)
+ }
+}
diff --git a/features/detail/domain/src/test/kotlin/com/manuelnunez/apps/features/detail/domain/usecase/SaveFavoritesUseCaseTest.kt b/features/detail/domain/src/test/kotlin/com/manuelnunez/apps/features/detail/domain/usecase/SaveFavoritesUseCaseTest.kt
new file mode 100644
index 0000000..65ba9ec
--- /dev/null
+++ b/features/detail/domain/src/test/kotlin/com/manuelnunez/apps/features/detail/domain/usecase/SaveFavoritesUseCaseTest.kt
@@ -0,0 +1,37 @@
+package com.manuelnunez.apps.features.detail.domain.usecase
+
+import app.cash.turbine.test
+import com.manuelnunez.apps.core.common.test.MockkAllRule
+import com.manuelnunez.apps.core.common.test.UnMockkAllRule
+import com.manuelnunez.apps.features.detail.domain.repository.DetailRepository
+import io.mockk.coVerify
+import io.mockk.confirmVerified
+import io.mockk.mockk
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.RegisterExtension
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class SaveFavoritesUseCaseTest {
+ @RegisterExtension private val mockkAllExtension = MockkAllRule()
+ @RegisterExtension private val unMockkAllExtension = UnMockkAllRule()
+
+ private val detailRepository = mockk()
+ private lateinit var useCase: SaveFavoritesUseCase
+
+ @BeforeEach
+ fun setUp() {
+ useCase =
+ SaveFavoritesUseCase(detailRepository, mockkAllExtension.testCoroutineDispatcherProvider)
+ }
+
+ @Test
+ fun `call SaveFavoritesUseCase invokes saveFavoriteItem from repository`() =
+ mockkAllExtension.runTest {
+ useCase.prepare(mockItem).test {}
+
+ coVerify(exactly = 1) { detailRepository.saveFavoriteItem(mockItem) }
+ confirmVerified(detailRepository)
+ }
+}
diff --git a/features/detail/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/detail/ui/DetailViewTest.kt b/features/detail/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/detail/ui/DetailScreenTest.kt
similarity index 71%
rename from features/detail/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/detail/ui/DetailViewTest.kt
rename to features/detail/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/detail/ui/DetailScreenTest.kt
index f082c4c..a7fb1f3 100644
--- a/features/detail/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/detail/ui/DetailViewTest.kt
+++ b/features/detail/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/detail/ui/DetailScreenTest.kt
@@ -7,19 +7,27 @@ import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import com.manuelnunez.apps.core.domain.model.Item
-import com.manuelnunez.apps.features.detail.ui.components.DetailErrorScreen
import com.manuelnunez.apps.features.detail.ui.components.DetailScreen
import org.junit.Rule
import org.junit.Test
-import com.manuelnunez.apps.core.ui.R as RCU
-class DetailViewTest {
+class DetailScreenTest {
@get:Rule(order = 0) val composeTestRule = createAndroidComposeRule()
@Test
fun photo_whenScreenIsLoaded_showsPhotoShareAndDescription() {
- composeTestRule.setContent { DetailScreen(mockItem, onBackClick = {}, onFavoriteClicked = {}) }
+ composeTestRule.setContent {
+ DetailScreen(item = mockItem, isFavorite = false, onBackClick = {}, onFavoriteClicked = {})
+ }
+
+ // Detail title
+ composeTestRule
+ .onNodeWithText(
+ composeTestRule.activity.resources.getString(R.string.section_details_title),
+ substring = true,
+ )
+ .assertExists()
// description
composeTestRule.onNodeWithText(mockItem.description).assertExists()
@@ -40,26 +48,6 @@ class DetailViewTest {
.assertHasNoClickAction()
}
- @Test
- fun error_whenError_showsTextAndButtonForGoBack() {
- composeTestRule.setContent { DetailErrorScreen(onBackClick = {}) }
-
- composeTestRule
- .onNodeWithText(
- composeTestRule.activity.resources.getString(R.string.alert_error_try_again_back),
- substring = true,
- )
- .assertExists()
-
- composeTestRule
- .onNodeWithContentDescription(
- composeTestRule.activity.resources.getString(RCU.string.alert_dialog_confirm_button),
- substring = true,
- )
- .assertExists()
- .assertHasClickAction()
- }
-
private val mockItem =
Item(
photoId = "14gf",
diff --git a/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/features/detail/ui/components/DetailScreen.kt b/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/features/detail/ui/components/DetailScreen.kt
index 745ff7b..4c02095 100644
--- a/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/features/detail/ui/components/DetailScreen.kt
+++ b/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/features/detail/ui/components/DetailScreen.kt
@@ -84,17 +84,7 @@ private fun DetailPortrait(
Row {
ShareImage(url = item.imageUrl)
- IconButton(onClick = onFavoriteClicked) {
- Icon(
- imageVector =
- if (isFavorite) {
- Icons.Filled.Favorite
- } else {
- Icons.Filled.FavoriteBorder
- },
- contentDescription = stringResource(id = R.string.button_share),
- tint = MaterialTheme.colorScheme.onSurface)
- }
+ FavoriteButton(onFavoriteClicked, isFavorite)
}
SurfaceText(
@@ -106,6 +96,21 @@ private fun DetailPortrait(
}
}
+@Composable
+private fun FavoriteButton(onFavoriteClicked: () -> Unit, isFavorite: Boolean) {
+ IconButton(onClick = onFavoriteClicked) {
+ Icon(
+ imageVector =
+ if (isFavorite) {
+ Icons.Filled.Favorite
+ } else {
+ Icons.Filled.FavoriteBorder
+ },
+ contentDescription = stringResource(id = R.string.button_favorite),
+ tint = MaterialTheme.colorScheme.onSurface)
+ }
+}
+
@Composable
private fun DetailLandscape(
item: Item,
@@ -131,17 +136,7 @@ private fun DetailLandscape(
Column(Modifier.weight(0.1f)) {
ShareImage(url = item.imageUrl)
- IconButton(onClick = onFavoriteClicked) {
- Icon(
- imageVector =
- if (isFavorite) {
- Icons.Filled.Favorite
- } else {
- Icons.Filled.FavoriteBorder
- },
- contentDescription = stringResource(id = R.string.button_share),
- tint = MaterialTheme.colorScheme.onSurface)
- }
+ FavoriteButton(onFavoriteClicked = onFavoriteClicked, isFavorite = isFavorite)
}
VerticalDivider()
diff --git a/features/detail/ui/src/main/res/values/strings.xml b/features/detail/ui/src/main/res/values/strings.xml
index 1a1ef95..a3f3c6c 100644
--- a/features/detail/ui/src/main/res/values/strings.xml
+++ b/features/detail/ui/src/main/res/values/strings.xml
@@ -1,6 +1,7 @@
Detail
Share
+ Favorite
Checkout this image from Purrfect Pics!: %1$s
Share Image
\ No newline at end of file
diff --git a/features/favorites/domain/src/test/kotlin/com/manuelnunez/apps/features/favorites/domain/usecase/GetFavoritesUseCaseTest.kt b/features/favorites/domain/src/test/kotlin/com/manuelnunez/apps/features/favorites/domain/usecase/GetFavoritesUseCaseTest.kt
new file mode 100644
index 0000000..c9e2d17
--- /dev/null
+++ b/features/favorites/domain/src/test/kotlin/com/manuelnunez/apps/features/favorites/domain/usecase/GetFavoritesUseCaseTest.kt
@@ -0,0 +1,50 @@
+package com.manuelnunez.apps.features.favorites.domain.usecase
+
+import app.cash.turbine.test
+import com.manuelnunez.apps.core.common.test.MockkAllRule
+import com.manuelnunez.apps.core.common.test.UnMockkAllRule
+import com.manuelnunez.apps.core.domain.model.Item
+import com.manuelnunez.apps.features.favorites.domain.repository.FavoritesRepository
+import io.mockk.confirmVerified
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.verify
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.flow
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.RegisterExtension
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class GetFavoritesUseCaseTest {
+ @RegisterExtension private val mockkAllExtension = MockkAllRule()
+ @RegisterExtension private val unMockkAllExtension = UnMockkAllRule()
+
+ private val favoritesRepository = mockk()
+ private lateinit var useCase: GetFavoritesUseCase
+
+ @BeforeEach
+ fun setUp() {
+ useCase =
+ GetFavoritesUseCase(favoritesRepository, mockkAllExtension.testCoroutineDispatcherProvider)
+ }
+
+ @Test
+ fun `call GetFavoritesUseCase invokes getAllFavorites from repository`() =
+ mockkAllExtension.runTest {
+ every { favoritesRepository.getAllFavorites() } returns flow { mockItems }
+ useCase.prepare(Unit).test {}
+
+ verify(exactly = 1) { favoritesRepository.getAllFavorites() }
+ confirmVerified(favoritesRepository)
+ }
+
+ private val mockItems =
+ List(5) { index ->
+ Item(
+ "$index",
+ "https://example.com/$index",
+ description = "description: $index",
+ thumbnailUrl = "https://example.com/$index")
+ }
+}
diff --git a/features/favorites/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/favorites/ui/FavoritesScreenTest.kt b/features/favorites/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/favorites/ui/FavoritesScreenTest.kt
new file mode 100644
index 0000000..3ba5df6
--- /dev/null
+++ b/features/favorites/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/favorites/ui/FavoritesScreenTest.kt
@@ -0,0 +1,70 @@
+package com.manuelnunez.apps.features.favorites.ui
+
+import androidx.activity.ComponentActivity
+import androidx.compose.ui.test.assertHasClickAction
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onNodeWithContentDescription
+import androidx.compose.ui.test.onNodeWithText
+import com.manuelnunez.apps.core.domain.model.Item
+import com.manuelnunez.apps.features.favorites.ui.component.FavoritesErrorScreen
+import com.manuelnunez.apps.features.favorites.ui.component.FavoritesScreen
+import org.junit.Rule
+import org.junit.Test
+import com.manuelnunez.apps.core.ui.R as RCU
+
+class FavoritesScreenTest {
+ @get:Rule(order = 0) val composeTestRule = createAndroidComposeRule()
+
+ @Test
+ fun photo_whenScreenIsLoaded_showsListOfPhotos() {
+ composeTestRule.setContent {
+ FavoritesScreen(items = mockItems, navigateToDetails = {}, onBackClick = {})
+ }
+
+ // Title
+ composeTestRule
+ .onNodeWithText(
+ composeTestRule.activity.resources.getString(R.string.section_favorites),
+ substring = true,
+ )
+ .assertExists()
+
+ // Content
+ composeTestRule
+ .onNodeWithContentDescription(
+ mockItems[0].description,
+ substring = true,
+ )
+ .assertExists()
+ .assertHasClickAction()
+ }
+
+ @Test
+ fun error_whenError_showsTextAndButtonForGoBack() {
+ composeTestRule.setContent { FavoritesErrorScreen(onBackClick = {}) }
+
+ composeTestRule
+ .onNodeWithText(
+ composeTestRule.activity.resources.getString(RCU.string.alert_error_try_again_back),
+ substring = true,
+ )
+ .assertExists()
+
+ composeTestRule
+ .onNodeWithContentDescription(
+ composeTestRule.activity.resources.getString(RCU.string.alert_dialog_confirm_button),
+ substring = true,
+ )
+ .assertExists()
+ .assertHasClickAction()
+ }
+
+ private val mockItems =
+ List(5) { index ->
+ Item(
+ "$index",
+ "https://example.com/$index",
+ description = "description: $index",
+ thumbnailUrl = "https://example.com/$index")
+ }
+}
diff --git a/features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/feature/favorites/ui/FavoritesRoute.kt b/features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/features/favorites/ui/FavoritesRoute.kt
similarity index 80%
rename from features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/feature/favorites/ui/FavoritesRoute.kt
rename to features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/features/favorites/ui/FavoritesRoute.kt
index d78635f..44bc16a 100644
--- a/features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/feature/favorites/ui/FavoritesRoute.kt
+++ b/features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/features/favorites/ui/FavoritesRoute.kt
@@ -1,4 +1,4 @@
-package com.manuelnunez.apps.feature.favorites.ui
+package com.manuelnunez.apps.features.favorites.ui
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
@@ -10,9 +10,9 @@ import androidx.compose.ui.Modifier
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.manuelnunez.apps.core.domain.model.Item
-import com.manuelnunez.apps.feature.favorites.ui.FavoritesViewModel.FavoriteItemsState
-import com.manuelnunez.apps.feature.favorites.ui.component.FavoritesErrorScreen
-import com.manuelnunez.apps.feature.favorites.ui.component.FavoritesScreen
+import com.manuelnunez.apps.features.favorites.ui.FavoritesViewModel.FavoriteItemsState
+import com.manuelnunez.apps.features.favorites.ui.component.FavoritesErrorScreen
+import com.manuelnunez.apps.features.favorites.ui.component.FavoritesScreen
@Composable
fun FavoritesRoute(
diff --git a/features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/feature/favorites/ui/FavoritesViewModel.kt b/features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/features/favorites/ui/FavoritesViewModel.kt
similarity index 96%
rename from features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/feature/favorites/ui/FavoritesViewModel.kt
rename to features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/features/favorites/ui/FavoritesViewModel.kt
index be7132f..75e9c6d 100644
--- a/features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/feature/favorites/ui/FavoritesViewModel.kt
+++ b/features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/features/favorites/ui/FavoritesViewModel.kt
@@ -1,4 +1,4 @@
-package com.manuelnunez.apps.feature.favorites.ui
+package com.manuelnunez.apps.features.favorites.ui
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
diff --git a/features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/feature/favorites/ui/component/FavoritesErrorScreen.kt b/features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/features/favorites/ui/component/FavoritesErrorScreen.kt
similarity index 92%
rename from features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/feature/favorites/ui/component/FavoritesErrorScreen.kt
rename to features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/features/favorites/ui/component/FavoritesErrorScreen.kt
index 3cf396e..d336255 100644
--- a/features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/feature/favorites/ui/component/FavoritesErrorScreen.kt
+++ b/features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/features/favorites/ui/component/FavoritesErrorScreen.kt
@@ -1,4 +1,4 @@
-package com.manuelnunez.apps.feature.favorites.ui.component
+package com.manuelnunez.apps.features.favorites.ui.component
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
diff --git a/features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/feature/favorites/ui/component/FavoritesScreen.kt b/features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/features/favorites/ui/component/FavoritesScreen.kt
similarity index 97%
rename from features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/feature/favorites/ui/component/FavoritesScreen.kt
rename to features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/features/favorites/ui/component/FavoritesScreen.kt
index 2bb9b8f..2ee9067 100644
--- a/features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/feature/favorites/ui/component/FavoritesScreen.kt
+++ b/features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/features/favorites/ui/component/FavoritesScreen.kt
@@ -1,4 +1,4 @@
-package com.manuelnunez.apps.feature.favorites.ui.component
+package com.manuelnunez.apps.features.favorites.ui.component
import android.content.res.Configuration
import androidx.compose.foundation.layout.Arrangement
diff --git a/features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/feature/favorites/ui/navigation/FavoritesNavigation.kt b/features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/features/favorites/ui/navigation/FavoritesNavigation.kt
similarity index 76%
rename from features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/feature/favorites/ui/navigation/FavoritesNavigation.kt
rename to features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/features/favorites/ui/navigation/FavoritesNavigation.kt
index c2554c7..984bf91 100644
--- a/features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/feature/favorites/ui/navigation/FavoritesNavigation.kt
+++ b/features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/features/favorites/ui/navigation/FavoritesNavigation.kt
@@ -1,9 +1,9 @@
-package com.manuelnunez.apps.feature.favorites.ui.navigation
+package com.manuelnunez.apps.features.favorites.ui.navigation
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import com.manuelnunez.apps.core.domain.model.Item
-import com.manuelnunez.apps.feature.favorites.ui.FavoritesRoute
+import com.manuelnunez.apps.features.favorites.ui.FavoritesRoute
const val FAVORITES_ROUTE = "favorites_route"
diff --git a/features/favorites/ui/src/test/kotlin/com/manuelnunez/apps/features/favorites/ui/FavoritesViewModelTest.kt b/features/favorites/ui/src/test/kotlin/com/manuelnunez/apps/features/favorites/ui/FavoritesViewModelTest.kt
new file mode 100644
index 0000000..1e3013b
--- /dev/null
+++ b/features/favorites/ui/src/test/kotlin/com/manuelnunez/apps/features/favorites/ui/FavoritesViewModelTest.kt
@@ -0,0 +1,81 @@
+package com.manuelnunez.apps.features.favorites.ui
+
+import app.cash.turbine.test
+import com.manuelnunez.apps.core.common.test.MockkAllRule
+import com.manuelnunez.apps.core.common.test.UnMockkAllRule
+import com.manuelnunez.apps.core.domain.model.Item
+import com.manuelnunez.apps.features.favorites.domain.usecase.GetFavoritesUseCase
+import com.manuelnunez.apps.features.favorites.ui.FavoritesViewModel.FavoriteItemsState
+import io.mockk.confirmVerified
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.verify
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.flow
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assertions.assertTrue
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.RegisterExtension
+import kotlin.properties.Delegates
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class FavoritesViewModelTest {
+ @RegisterExtension private val mockkAllExtension = MockkAllRule()
+ @RegisterExtension private val unMockkAllExtension = UnMockkAllRule()
+
+ private val getFavoritesUseCase = mockk()
+
+ private var viewModel: FavoritesViewModel by Delegates.notNull()
+
+ @Test
+ fun `GIVEN viewmodel init, WHEN onSuccess, THEN set state with favorite items`() =
+ mockkAllExtension.runTest {
+ every { getFavoritesUseCase.prepare(Unit) } returns flow { emit(mockPhotos) }
+
+ viewModel = FavoritesViewModel(getFavoritesUseCase)
+
+ viewModel.favoriteItemsState.test {
+ // GIVEN viewModel INIT
+
+ assertTrue(awaitItem() is FavoriteItemsState.Idle)
+ assertTrue(awaitItem() is FavoriteItemsState.Loading)
+ assertEquals(FavoriteItemsState.ShowList(mockPhotos), awaitItem())
+
+ cancelAndIgnoreRemainingEvents()
+ }
+
+ verify(exactly = 1) { getFavoritesUseCase.prepare(Unit) }
+ confirmVerified(getFavoritesUseCase)
+ }
+
+ @Test
+ fun `GIVEN viewmodel init, WHEN exception catch, THEN set state with error`() =
+ mockkAllExtension.runTest {
+ every { getFavoritesUseCase.prepare(Unit) } returns flow { throw Exception() }
+
+ viewModel = FavoritesViewModel(getFavoritesUseCase)
+
+ viewModel.favoriteItemsState.test {
+ // GIVEN viewModel INIT
+
+ assertTrue(awaitItem() is FavoriteItemsState.Idle)
+ assertTrue(awaitItem() is FavoriteItemsState.Loading)
+ assertEquals(FavoriteItemsState.Error, awaitItem())
+
+ cancelAndIgnoreRemainingEvents()
+ }
+
+ verify(exactly = 1) { getFavoritesUseCase.prepare(Unit) }
+ confirmVerified(getFavoritesUseCase)
+ }
+
+ private val mockPhotos: List
- =
+ List(20) { index ->
+ val id = (index + 1).toString()
+ Item(
+ photoId = id,
+ imageUrl = "https://example.com/photo$id",
+ thumbnailUrl = "https://example.com/photo$id/small",
+ description = "This is a description for item $id")
+ }
+}
diff --git a/features/home/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/home/ui/HomeViewTest.kt b/features/home/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/home/ui/HomeScreenTest.kt
similarity index 81%
rename from features/home/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/home/ui/HomeViewTest.kt
rename to features/home/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/home/ui/HomeScreenTest.kt
index b2a05f8..d3f107e 100644
--- a/features/home/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/home/ui/HomeViewTest.kt
+++ b/features/home/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/home/ui/HomeScreenTest.kt
@@ -6,13 +6,16 @@ import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import com.manuelnunez.apps.core.domain.model.Item
+import com.manuelnunez.apps.features.home.ui.HomeViewModel.FeaturedItemsState
+import com.manuelnunez.apps.features.home.ui.HomeViewModel.HomeUiState
+import com.manuelnunez.apps.features.home.ui.HomeViewModel.PopularItemsState
import com.manuelnunez.apps.features.home.ui.components.HomeErrorScreen
import com.manuelnunez.apps.features.home.ui.components.HomeScreen
import org.junit.Rule
import org.junit.Test
import com.manuelnunez.apps.core.ui.R as RCU
-class HomeViewTest {
+class HomeScreenTest {
@get:Rule(order = 0) val composeTestRule = createAndroidComposeRule()
@@ -21,9 +24,9 @@ class HomeViewTest {
composeTestRule.setContent {
HomeScreen(
items =
- HomeViewModel.HomeUiState(
- popularItemsState = HomeViewModel.PopularItemsState.Loading,
- featuredItemsState = HomeViewModel.FeaturedItemsState.Loading),
+ HomeUiState(
+ popularItemsState = PopularItemsState.Loading,
+ featuredItemsState = FeaturedItemsState.Loading),
navigateToDetails = {},
navigateToSeeMore = {})
}
@@ -47,10 +50,9 @@ class HomeViewTest {
composeTestRule.setContent {
HomeScreen(
items =
- HomeViewModel.HomeUiState(
- popularItemsState = HomeViewModel.PopularItemsState.ShowList(mockPopularPhotos),
- featuredItemsState =
- HomeViewModel.FeaturedItemsState.ShowList(mockFeaturedPhotos)),
+ HomeUiState(
+ popularItemsState = PopularItemsState.ShowList(mockPopularPhotos),
+ featuredItemsState = FeaturedItemsState.ShowList(mockFeaturedPhotos)),
navigateToDetails = {},
navigateToSeeMore = {})
}
@@ -105,9 +107,9 @@ class HomeViewTest {
composeTestRule.setContent {
HomeScreen(
items =
- HomeViewModel.HomeUiState(
- popularItemsState = HomeViewModel.PopularItemsState.ShowList(mockPopularPhotos),
- featuredItemsState = HomeViewModel.FeaturedItemsState.Error),
+ HomeUiState(
+ popularItemsState = PopularItemsState.ShowList(mockPopularPhotos),
+ featuredItemsState = FeaturedItemsState.Error),
navigateToDetails = {},
navigateToSeeMore = {})
}
@@ -125,10 +127,9 @@ class HomeViewTest {
composeTestRule.setContent {
HomeScreen(
items =
- HomeViewModel.HomeUiState(
- popularItemsState = HomeViewModel.PopularItemsState.Error,
- featuredItemsState =
- HomeViewModel.FeaturedItemsState.ShowList(mockFeaturedPhotos)),
+ HomeUiState(
+ popularItemsState = PopularItemsState.Error,
+ featuredItemsState = FeaturedItemsState.ShowList(mockFeaturedPhotos)),
navigateToDetails = {},
navigateToSeeMore = {})
}
diff --git a/features/home/ui/src/main/kotlin/com/manuelnunez/apps/features/home/ui/HomeViewModel.kt b/features/home/ui/src/main/kotlin/com/manuelnunez/apps/features/home/ui/HomeViewModel.kt
index 52e4eca..926181b 100644
--- a/features/home/ui/src/main/kotlin/com/manuelnunez/apps/features/home/ui/HomeViewModel.kt
+++ b/features/home/ui/src/main/kotlin/com/manuelnunez/apps/features/home/ui/HomeViewModel.kt
@@ -65,7 +65,7 @@ constructor(
success = { featuredItemsState.value = FeaturedItemsState.ShowList(it) },
error = { featuredItemsState.value = FeaturedItemsState.Error })
}
- .catch { featuredItemsState.value = FeaturedItemsState.ShowList(emptyList()) }
+ .catch { featuredItemsState.value = FeaturedItemsState.Error }
.launchIn(viewModelScope)
}
diff --git a/features/home/ui/src/test/kotlin/com/manuelnunez/apps/features/home/ui/viewmodel/HomeViewModelTest.kt b/features/home/ui/src/test/kotlin/com/manuelnunez/apps/features/home/ui/viewmodel/HomeViewModelTest.kt
index a0c77f2..d42c4f8 100644
--- a/features/home/ui/src/test/kotlin/com/manuelnunez/apps/features/home/ui/viewmodel/HomeViewModelTest.kt
+++ b/features/home/ui/src/test/kotlin/com/manuelnunez/apps/features/home/ui/viewmodel/HomeViewModelTest.kt
@@ -101,6 +101,37 @@ class HomeViewModelTest {
confirmVerified(getFeaturedItemsUseCase, getPopularItemsUseCase)
}
+ @Test
+ fun `GIVEN viewmodel init, WHEN exception catch, THEN set state with error`() =
+ mockkAllExtension.runTest {
+ every { getFeaturedItemsUseCase.prepare(Unit) } returns flow { throw Exception() }
+ every { getPopularItemsUseCase.prepare(Unit) } returns flow { throw Exception() }
+
+ viewModel = HomeViewModel(getFeaturedItemsUseCase, getPopularItemsUseCase)
+
+ viewModel.state.test {
+ // GIVEN viewModel INIT
+
+ awaitItem().apply {
+ assertTrue(featuredItemsState is FeaturedItemsState.Idle)
+ assertTrue(popularItemsState is PopularItemsState.Idle)
+ }
+
+ awaitItem().apply {
+ assertEquals(FeaturedItemsState.Error, featuredItemsState)
+ assertEquals(PopularItemsState.Error, popularItemsState)
+ }
+
+ cancelAndIgnoreRemainingEvents()
+ }
+
+ verify(exactly = 1) {
+ getFeaturedItemsUseCase.prepare(Unit)
+ getPopularItemsUseCase.prepare(Unit)
+ }
+ confirmVerified(getFeaturedItemsUseCase, getPopularItemsUseCase)
+ }
+
private val mockPhotos: List
- =
List(20) { index ->
val id = (index + 1).toString()
diff --git a/features/seemore/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/seemore/ui/SeeMoreViewTest.kt b/features/seemore/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/seemore/ui/SeeMoreScreenTest.kt
similarity index 94%
rename from features/seemore/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/seemore/ui/SeeMoreViewTest.kt
rename to features/seemore/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/seemore/ui/SeeMoreScreenTest.kt
index 76cfbfa..267a48f 100644
--- a/features/seemore/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/seemore/ui/SeeMoreViewTest.kt
+++ b/features/seemore/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/seemore/ui/SeeMoreScreenTest.kt
@@ -15,12 +15,12 @@ import org.junit.Rule
import org.junit.Test
import com.manuelnunez.apps.core.ui.R as RCU
-class SeeMoreViewTest {
+class SeeMoreScreenTest {
@get:Rule(order = 0) val composeTestRule = createAndroidComposeRule()
@Test
- fun photo_whenScreenIsLoaded_showsPhotoShareAndDescription() {
+ fun photo_whenScreenIsLoaded_showsListOfPhotos() {
composeTestRule.setContent {
SeeMoreScreen(
items = flowOf(PagingData.from(mockItems)).collectAsLazyPagingItems(),
@@ -28,6 +28,7 @@ class SeeMoreViewTest {
navigateToDetails = {})
}
+ // Title
composeTestRule
.onNodeWithText(
composeTestRule.activity.resources.getString(RCU.string.section_popular),
@@ -35,6 +36,7 @@ class SeeMoreViewTest {
)
.assertExists()
+ // Content
composeTestRule
.onNodeWithContentDescription(
mockItems[0].description,
From 5ce3b9ed170057145f005d34102682a180ffe31a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Manuel=20Nu=C3=B1ez?= <03.manu@gmail.com>
Date: Fri, 19 Apr 2024 00:26:22 -0400
Subject: [PATCH 8/8] Updated README
---
README.md | 20 ++++++++------------
1 file changed, 8 insertions(+), 12 deletions(-)
diff --git a/README.md b/README.md
index 9766e76..3eb84e6 100644
--- a/README.md
+++ b/README.md
@@ -9,30 +9,26 @@ heart.
- **Discover:** Explore a vast collection of random cat images sourced from the web.
- **Share:** Share delightful cat images with friends, family, and fellow cat enthusiasts with just
a tap.
-- **Save Favorites: (Coming soon)** Save your favorite cat images to easily revisit them later and
+- **Save Favorites:** Save your favorite cat images to easily revisit them later and
create your personalized collection.
## Tools/Libraries
-### Android Libraries
-
- **UI Components:** [AndroidX Core KTX](https://developer.android.com/jetpack/androidx/releases/core) | [Material Components for Android](https://github.com/material-components/material-components-android) | [Compose UI](https://developer.android.com/jetpack/androidx/releases/compose-ui)
+- **Compose UI:** [Compose UI](https://developer.android.com/jetpack/androidx/releases/compose-ui)
- **Testing:** [JUnit](https://junit.org/junit5/) | [MockK](https://mockk.io/) | [Turbine](https://github.com/cashapp/turbine)
- **Dependency Injection:** [Hilt](https://developer.android.com/training/dependency-injection/hilt-android)
- **Coroutines:** [Kotlin Coroutines](https://kotlinlang.org/docs/coroutines-overview.html)
+- **DataStore Proto:** [DataStore Proto](https://developer.android.com/topic/libraries/architecture/datastore#prefs-vs-proto)
+- **Navigation:** [Navigation Compose](https://developer.android.com/jetpack/androidx/releases/navigation) | [Hilt Navigation Compose](https://developer.android.com/training/dependency-injection/hilt-android#navigation-compose)
+- **Material Design:** [Material3](https://developer.android.com/jetpack/androidx/releases/compose-material3) [Material Components for Android](https://github.com/material-components/material-components-android)
- **Networking:** [Retrofit](https://square.github.io/retrofit/)
- **Image Loading:** [Coil](https://coil-kt.github.io/coil/)
-### Compose Libraries
-
-- **UI Components:** [Material3](https://developer.android.com/jetpack/androidx/releases/compose-material3) | [Compose UI](https://developer.android.com/jetpack/androidx/releases/compose-ui)
-- **Navigation:** [Navigation Compose](https://developer.android.com/jetpack/androidx/releases/navigation) | [Hilt Navigation Compose](https://developer.android.com/training/dependency-injection/hilt-android#navigation-compose)
-- **Material Design:** [Material Components for Android](https://github.com/material-components/material-components-android)
-
## Screenshots
-| ![Screenshot 2024-04-16 at 10 26 30 AM](https://github.com/manununhez/purrfect-pics/assets/5048531/c08f645e-a362-4d2e-bfff-c99d8966e2b8) | ![Screenshot 2024-04-15 at 6 34 16 PM](https://github.com/manununhez/purrfect-pics/assets/5048531/a8024c0c-e31f-4189-b268-1167048658ad) | ![Screenshot 2024-04-15 at 6 35 33 PM](https://github.com/manununhez/purrfect-pics/assets/5048531/1a72c5ea-ee22-4470-8e98-6215ebb86924) | ![Screenshot 2024-04-15 at 6 36 08 PM](https://github.com/manununhez/purrfect-pics/assets/5048531/c31637d7-ddfe-436a-8e16-d27244d638ea) |
-|------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------|
+| ![Screenshot 2024-04-16 at 10 26 30 AM](https://github.com/manununhez/purrfect-pics/assets/5048531/c08f645e-a362-4d2e-bfff-c99d8966e2b8) | ![Screenshot 2024-04-19 at 12 06 41 AM](https://github.com/manununhez/purrfect-pics/assets/5048531/f7c9873c-8ab8-4215-b4f3-ed801dd8f6c9) | ![Screenshot 2024-04-19 at 12 17 14 AM](https://github.com/manununhez/purrfect-pics/assets/5048531/534f2f34-5f9d-4945-83bf-0076f667bc19)| ![Screenshot 2024-04-19 at 12 10 40 AM](https://github.com/manununhez/purrfect-pics/assets/5048531/69042702-71aa-4eae-b508-871d9c630ff8) | ![Screenshot 2024-04-19 at 12 16 02 AM](https://github.com/manununhez/purrfect-pics/assets/5048531/e55d1bae-6aa4-4401-a6b6-8754b0756efd)|
+|----------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------|
## Considerations
@@ -40,7 +36,7 @@ heart.
and [Pexels](https://www.pexels.com/). In particular, Pexels API
is [rate-limited](https://www.pexels.com/api/documentation/#guidelines), in case of errors for
this, a new API key will be necessary to be generated.
-- Not persisted in DB or preferences because this API has dynamic and frequently updated data. On
+- Localdata persistence of favorite cats only. Responses from API are not cached because these are dynamic and frequently updated data. On
the other hand, Coil image lib does use disk and memory cache to smooth image loading.
- KtfmtFormat plugin applied for code formatting.
- There is a known issue with the splash screen not showing on Android 12. A temporary