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