From a01ec1ef4da824877aa304f956f27710a02bdd99 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Manuel=20Nu=C3=B1ez?= <03.manu@gmail.com>
Date: Tue, 16 Apr 2024 08:59:35 -0400
Subject: [PATCH 1/3] Renamed module #42
---
app/build.gradle.kts | 2 +-
core/{common-ui => ui}/.gitignore | 0
core/{common-ui => ui}/build.gradle.kts | 0
core/{common-ui => ui}/src/main/AndroidManifest.xml | 0
.../apps/core/ui/component/AdaptableVerticalGrid.kt | 0
.../apps/core/ui/component/AlertDialog.kt | 0
.../apps/core/ui/component/Background.kt | 0
.../apps/core/ui/component/DynamicAsyncImage.kt | 0
.../manuelnunez/apps/core/ui/component/ImageCard.kt | 0
.../manuelnunez/apps/core/ui/component/TextCard.kt | 0
.../manuelnunez/apps/core/ui/component/TitleText.kt | 0
.../manuelnunez/apps/core/ui/theme/Background.kt | 0
.../com/manuelnunez/apps/core/ui/theme/Color.kt | 0
.../com/manuelnunez/apps/core/ui/theme/Gradient.kt | 0
.../com/manuelnunez/apps/core/ui/theme/Theme.kt | 0
.../com/manuelnunez/apps/core/ui/theme/Type.kt | 0
.../com/manuelnunez/apps/core/ui/utils/Preview.kt | 0
.../src/main/res/drawable/ic_broken_image.xml | 0
.../src/main/res/font/nunito_sans.ttf | Bin
.../src/main/res/font/nunito_sans_bold.ttf | Bin
.../src/main/res/font/nunito_sans_light.ttf | Bin
.../src/main/res/values/strings.xml | 0
features/detail/ui/build.gradle.kts | 2 +-
features/home/ui/build.gradle.kts | 2 +-
features/seemore/ui/build.gradle.kts | 2 +-
settings.gradle.kts | 2 +-
26 files changed, 5 insertions(+), 5 deletions(-)
rename core/{common-ui => ui}/.gitignore (100%)
rename core/{common-ui => ui}/build.gradle.kts (100%)
rename core/{common-ui => ui}/src/main/AndroidManifest.xml (100%)
rename core/{common-ui => ui}/src/main/kotlin/com/manuelnunez/apps/core/ui/component/AdaptableVerticalGrid.kt (100%)
rename core/{common-ui => ui}/src/main/kotlin/com/manuelnunez/apps/core/ui/component/AlertDialog.kt (100%)
rename core/{common-ui => ui}/src/main/kotlin/com/manuelnunez/apps/core/ui/component/Background.kt (100%)
rename core/{common-ui => ui}/src/main/kotlin/com/manuelnunez/apps/core/ui/component/DynamicAsyncImage.kt (100%)
rename core/{common-ui => ui}/src/main/kotlin/com/manuelnunez/apps/core/ui/component/ImageCard.kt (100%)
rename core/{common-ui => ui}/src/main/kotlin/com/manuelnunez/apps/core/ui/component/TextCard.kt (100%)
rename core/{common-ui => ui}/src/main/kotlin/com/manuelnunez/apps/core/ui/component/TitleText.kt (100%)
rename core/{common-ui => ui}/src/main/kotlin/com/manuelnunez/apps/core/ui/theme/Background.kt (100%)
rename core/{common-ui => ui}/src/main/kotlin/com/manuelnunez/apps/core/ui/theme/Color.kt (100%)
rename core/{common-ui => ui}/src/main/kotlin/com/manuelnunez/apps/core/ui/theme/Gradient.kt (100%)
rename core/{common-ui => ui}/src/main/kotlin/com/manuelnunez/apps/core/ui/theme/Theme.kt (100%)
rename core/{common-ui => ui}/src/main/kotlin/com/manuelnunez/apps/core/ui/theme/Type.kt (100%)
rename core/{common-ui => ui}/src/main/kotlin/com/manuelnunez/apps/core/ui/utils/Preview.kt (100%)
rename core/{common-ui => ui}/src/main/res/drawable/ic_broken_image.xml (100%)
rename core/{common-ui => ui}/src/main/res/font/nunito_sans.ttf (100%)
rename core/{common-ui => ui}/src/main/res/font/nunito_sans_bold.ttf (100%)
rename core/{common-ui => ui}/src/main/res/font/nunito_sans_light.ttf (100%)
rename core/{common-ui => ui}/src/main/res/values/strings.xml (100%)
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index a25818e..9b65ec8 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -69,7 +69,7 @@ android {
}
dependencies {
- implementation(projects.core.commonUi)
+ implementation(projects.core.ui)
implementation(projects.core.data)
implementation(projects.core.domain)
implementation(projects.features.home.ui)
diff --git a/core/common-ui/.gitignore b/core/ui/.gitignore
similarity index 100%
rename from core/common-ui/.gitignore
rename to core/ui/.gitignore
diff --git a/core/common-ui/build.gradle.kts b/core/ui/build.gradle.kts
similarity index 100%
rename from core/common-ui/build.gradle.kts
rename to core/ui/build.gradle.kts
diff --git a/core/common-ui/src/main/AndroidManifest.xml b/core/ui/src/main/AndroidManifest.xml
similarity index 100%
rename from core/common-ui/src/main/AndroidManifest.xml
rename to core/ui/src/main/AndroidManifest.xml
diff --git a/core/common-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
similarity index 100%
rename from core/common-ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/AdaptableVerticalGrid.kt
rename to core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/AdaptableVerticalGrid.kt
diff --git a/core/common-ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/AlertDialog.kt b/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/AlertDialog.kt
similarity index 100%
rename from core/common-ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/AlertDialog.kt
rename to core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/AlertDialog.kt
diff --git a/core/common-ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/Background.kt b/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/Background.kt
similarity index 100%
rename from core/common-ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/Background.kt
rename to core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/Background.kt
diff --git a/core/common-ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/DynamicAsyncImage.kt b/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/DynamicAsyncImage.kt
similarity index 100%
rename from core/common-ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/DynamicAsyncImage.kt
rename to core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/DynamicAsyncImage.kt
diff --git a/core/common-ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/ImageCard.kt b/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/ImageCard.kt
similarity index 100%
rename from core/common-ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/ImageCard.kt
rename to core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/ImageCard.kt
diff --git a/core/common-ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/TextCard.kt b/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/TextCard.kt
similarity index 100%
rename from core/common-ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/TextCard.kt
rename to core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/TextCard.kt
diff --git a/core/common-ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/TitleText.kt b/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/TitleText.kt
similarity index 100%
rename from core/common-ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/TitleText.kt
rename to core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/TitleText.kt
diff --git a/core/common-ui/src/main/kotlin/com/manuelnunez/apps/core/ui/theme/Background.kt b/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/theme/Background.kt
similarity index 100%
rename from core/common-ui/src/main/kotlin/com/manuelnunez/apps/core/ui/theme/Background.kt
rename to core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/theme/Background.kt
diff --git a/core/common-ui/src/main/kotlin/com/manuelnunez/apps/core/ui/theme/Color.kt b/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/theme/Color.kt
similarity index 100%
rename from core/common-ui/src/main/kotlin/com/manuelnunez/apps/core/ui/theme/Color.kt
rename to core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/theme/Color.kt
diff --git a/core/common-ui/src/main/kotlin/com/manuelnunez/apps/core/ui/theme/Gradient.kt b/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/theme/Gradient.kt
similarity index 100%
rename from core/common-ui/src/main/kotlin/com/manuelnunez/apps/core/ui/theme/Gradient.kt
rename to core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/theme/Gradient.kt
diff --git a/core/common-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
similarity index 100%
rename from core/common-ui/src/main/kotlin/com/manuelnunez/apps/core/ui/theme/Theme.kt
rename to core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/theme/Theme.kt
diff --git a/core/common-ui/src/main/kotlin/com/manuelnunez/apps/core/ui/theme/Type.kt b/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/theme/Type.kt
similarity index 100%
rename from core/common-ui/src/main/kotlin/com/manuelnunez/apps/core/ui/theme/Type.kt
rename to core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/theme/Type.kt
diff --git a/core/common-ui/src/main/kotlin/com/manuelnunez/apps/core/ui/utils/Preview.kt b/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/utils/Preview.kt
similarity index 100%
rename from core/common-ui/src/main/kotlin/com/manuelnunez/apps/core/ui/utils/Preview.kt
rename to core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/utils/Preview.kt
diff --git a/core/common-ui/src/main/res/drawable/ic_broken_image.xml b/core/ui/src/main/res/drawable/ic_broken_image.xml
similarity index 100%
rename from core/common-ui/src/main/res/drawable/ic_broken_image.xml
rename to core/ui/src/main/res/drawable/ic_broken_image.xml
diff --git a/core/common-ui/src/main/res/font/nunito_sans.ttf b/core/ui/src/main/res/font/nunito_sans.ttf
similarity index 100%
rename from core/common-ui/src/main/res/font/nunito_sans.ttf
rename to core/ui/src/main/res/font/nunito_sans.ttf
diff --git a/core/common-ui/src/main/res/font/nunito_sans_bold.ttf b/core/ui/src/main/res/font/nunito_sans_bold.ttf
similarity index 100%
rename from core/common-ui/src/main/res/font/nunito_sans_bold.ttf
rename to core/ui/src/main/res/font/nunito_sans_bold.ttf
diff --git a/core/common-ui/src/main/res/font/nunito_sans_light.ttf b/core/ui/src/main/res/font/nunito_sans_light.ttf
similarity index 100%
rename from core/common-ui/src/main/res/font/nunito_sans_light.ttf
rename to core/ui/src/main/res/font/nunito_sans_light.ttf
diff --git a/core/common-ui/src/main/res/values/strings.xml b/core/ui/src/main/res/values/strings.xml
similarity index 100%
rename from core/common-ui/src/main/res/values/strings.xml
rename to core/ui/src/main/res/values/strings.xml
diff --git a/features/detail/ui/build.gradle.kts b/features/detail/ui/build.gradle.kts
index daa113e..b3c9141 100644
--- a/features/detail/ui/build.gradle.kts
+++ b/features/detail/ui/build.gradle.kts
@@ -34,7 +34,7 @@ android {
dependencies {
implementation(projects.core.common)
implementation(projects.core.domain)
- implementation(projects.core.commonUi)
+ implementation(projects.core.ui)
// Arch Components
implementation(libs.androidx.lifecycle.runtime.compose)
diff --git a/features/home/ui/build.gradle.kts b/features/home/ui/build.gradle.kts
index 14eb1ab..b7a8b4d 100644
--- a/features/home/ui/build.gradle.kts
+++ b/features/home/ui/build.gradle.kts
@@ -34,7 +34,7 @@ android {
dependencies {
implementation(projects.core.common)
implementation(projects.core.domain)
- implementation(projects.core.commonUi)
+ implementation(projects.core.ui)
implementation(projects.features.home.domain)
// Arch Components
diff --git a/features/seemore/ui/build.gradle.kts b/features/seemore/ui/build.gradle.kts
index 39e3ff9..ce805dc 100644
--- a/features/seemore/ui/build.gradle.kts
+++ b/features/seemore/ui/build.gradle.kts
@@ -35,7 +35,7 @@ android {
dependencies {
implementation(projects.core.common)
implementation(projects.core.domain)
- implementation(projects.core.commonUi)
+ implementation(projects.core.ui)
implementation(projects.features.seemore.domain)
// Arch Components
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 5547f45..96ffefe 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -34,7 +34,7 @@ include(":core:domain")
include(":core:services")
-include(":core:common-ui")
+include(":core:ui")
include(":features:home:domain")
From fc855d112a498aae8a78575587d3e5ef7971cb3b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Manuel=20Nu=C3=B1ez?= <03.manu@gmail.com>
Date: Tue, 16 Apr 2024 10:31:50 -0400
Subject: [PATCH 2/3] Update UI Components #43
---
.../apps/core/ui/component/AlertDialog.kt | 61 ------------
.../apps/core/ui/component/CustomCard.kt | 95 +++++++++++++++++++
.../apps/core/ui/component/ErrorDialog.kt | 73 ++++++++++++++
.../apps/core/ui/component/ImageCard.kt | 48 ----------
...micAsyncImage.kt => StatefulAsyncImage.kt} | 4 +-
.../apps/core/ui/component/SurfaceText.kt | 33 +++++++
.../apps/core/ui/component/TextCard.kt | 40 --------
.../apps/core/ui/component/TitleText.kt | 41 --------
core/ui/src/main/res/values/strings.xml | 3 +
.../detail/ui/components/DetailErrorScreen.kt | 2 +-
.../detail/ui/components/DetailScreen.kt | 18 ++--
.../home/ui/components/HomeErrorScreen.kt | 21 ++--
.../features/home/ui/components/HomeScreen.kt | 16 ++--
.../ui/components/SeeMoreErrorScreen.kt | 2 +-
.../seemore/ui/components/SeeMoreScreen.kt | 4 +-
15 files changed, 234 insertions(+), 227 deletions(-)
delete mode 100644 core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/AlertDialog.kt
create mode 100644 core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/CustomCard.kt
create mode 100644 core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/ErrorDialog.kt
delete mode 100644 core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/ImageCard.kt
rename core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/{DynamicAsyncImage.kt => StatefulAsyncImage.kt} (93%)
create mode 100644 core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/SurfaceText.kt
delete mode 100644 core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/TextCard.kt
delete mode 100644 core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/TitleText.kt
diff --git a/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/AlertDialog.kt b/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/AlertDialog.kt
deleted file mode 100644
index 9b55bdd..0000000
--- a/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/AlertDialog.kt
+++ /dev/null
@@ -1,61 +0,0 @@
-package com.manuelnunez.apps.core.ui.component
-
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Info
-import androidx.compose.material3.AlertDialog
-import androidx.compose.material3.Icon
-import androidx.compose.material3.Text
-import androidx.compose.material3.TextButton
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.ui.graphics.vector.ImageVector
-import com.manuelnunez.apps.core.ui.theme.MainTheme
-import com.manuelnunez.apps.core.ui.utils.ThemePreviews
-
-@Composable
-fun ErrorDialog(dialogTitle: String, dialogText: String, onConfirmation: () -> Unit) {
- val openAlertDialog = remember { mutableStateOf(true) }
-
- when {
- openAlertDialog.value -> {
- ErrorAlertDialog(
- onDismissRequest = { openAlertDialog.value = false },
- onConfirmation = {
- openAlertDialog.value = false
- onConfirmation.invoke()
- },
- dialogTitle = dialogTitle,
- dialogText = dialogText,
- icon = Icons.Default.Info)
- }
- }
-}
-
-@Composable
-fun ErrorAlertDialog(
- onDismissRequest: () -> Unit,
- onConfirmation: () -> Unit,
- dialogTitle: String,
- dialogText: String,
- icon: ImageVector,
-) {
- AlertDialog(
- icon = { Icon(icon, contentDescription = "Example Icon") },
- title = { Text(text = dialogTitle) },
- text = { Text(text = dialogText) },
- onDismissRequest = { onDismissRequest() },
- confirmButton = { TextButton(onClick = { onConfirmation() }) { Text("Confirm") } },
- dismissButton = { TextButton(onClick = { onDismissRequest() }) { Text("Dismiss") } })
-}
-
-@ThemePreviews
-@Composable
-fun ErrorDialogPreview() {
- MainTheme {
- ErrorDialog(
- onConfirmation = {},
- dialogTitle = "Title",
- dialogText = "https://picsum.photos/id/237/200/300")
- }
-}
diff --git a/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/CustomCard.kt b/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/CustomCard.kt
new file mode 100644
index 0000000..9d0c7db
--- /dev/null
+++ b/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/CustomCard.kt
@@ -0,0 +1,95 @@
+package com.manuelnunez.apps.core.ui.component
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.CardElevation
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.text.style.TextAlign
+import com.manuelnunez.apps.core.ui.component.CustomCardAutomation.IMAGE_CARD_PREFIX
+import com.manuelnunez.apps.core.ui.component.CustomCardAutomation.TEXT_CARD_PREFIX
+import com.manuelnunez.apps.core.ui.theme.MainTheme
+import com.manuelnunez.apps.core.ui.utils.ThemePreviews
+
+object CustomCardAutomation {
+ const val IMAGE_CARD_PREFIX = "ImageCard"
+ const val TEXT_CARD_PREFIX = "TextCard"
+}
+
+@Composable
+fun ImageCard(
+ modifier: Modifier = Modifier,
+ imageUrl: String,
+ cardContentDescription: String,
+ elevation: CardElevation = CardDefaults.cardElevation(),
+ contentScale: ContentScale = ContentScale.Crop,
+ testTag: String = IMAGE_CARD_PREFIX,
+ onClick: (() -> Unit)? = null
+) {
+ Card(
+ modifier =
+ modifier
+ .clickable(onClick = { onClick?.invoke() })
+ .semantics { contentDescription = cardContentDescription }
+ .testTag(testTag),
+ elevation = elevation) {
+ StatefulAsyncImage(
+ modifier = Modifier.fillMaxSize(),
+ imageUrl = imageUrl,
+ contentDescription = "",
+ contentScale = contentScale)
+ }
+}
+
+@Composable
+fun TextCard(
+ modifier: Modifier = Modifier,
+ text: String,
+ testTag: String = TEXT_CARD_PREFIX,
+ onClick: (() -> Unit)? = null
+) {
+ Card(
+ colors =
+ CardDefaults.cardColors().copy(containerColor = MaterialTheme.colorScheme.onBackground),
+ modifier =
+ modifier
+ .clickable(onClick = { onClick?.invoke() })
+ .semantics { contentDescription = text }
+ .testTag(testTag)) {
+ Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
+ Text(
+ modifier = Modifier.fillMaxWidth(),
+ textAlign = TextAlign.Center,
+ text = text,
+ color = MaterialTheme.colorScheme.background)
+ }
+ }
+}
+
+@ThemePreviews
+@Composable
+fun ImageCardPreview() {
+ MainTheme {
+ ImageCard(
+ onClick = {},
+ cardContentDescription = "",
+ imageUrl = "https://picsum.photos/id/237/200/300")
+ }
+}
+
+@ThemePreviews
+@Composable
+fun TextCardPreview() {
+ MainTheme { TextCard(onClick = {}, text = "See more") }
+}
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
new file mode 100644
index 0000000..e12188f
--- /dev/null
+++ b/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/ErrorDialog.kt
@@ -0,0 +1,73 @@
+package com.manuelnunez.apps.core.ui.component
+
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Warning
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
+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 ErrorDialog(
+ dialogTitle: String,
+ dialogText: String,
+ onConfirmClick: () -> Unit,
+ icon: ImageVector = Icons.Default.Warning,
+ confirmButtonText: String = stringResource(id = R.string.alert_dialog_confirm_button),
+ dismissButtonText: String = stringResource(id = R.string.alert_dialog_dismiss_button)
+) {
+ val openAlertDialog = remember { mutableStateOf(true) }
+
+ val onDismissRequest = { openAlertDialog.value = false }
+
+ val onConfirmation = {
+ openAlertDialog.value = false
+ onConfirmClick.invoke()
+ }
+
+ when {
+ openAlertDialog.value -> {
+ AlertDialog(
+ icon = { Icon(icon, contentDescription = "Warning Icon") },
+ title = { Text(text = dialogTitle) },
+ text = { Text(text = dialogText) },
+ onDismissRequest = { onDismissRequest() },
+ confirmButton = {
+ TextButton(
+ modifier = Modifier.semantics { contentDescription = confirmButtonText },
+ onClick = { onConfirmation() }) {
+ Text(confirmButtonText)
+ }
+ },
+ dismissButton = {
+ TextButton(
+ modifier = Modifier.semantics { contentDescription = dismissButtonText },
+ onClick = { onDismissRequest() }) {
+ Text(dismissButtonText)
+ }
+ })
+ }
+ }
+}
+
+@ThemePreviews
+@Composable
+fun ErrorDialogPreview() {
+ MainTheme {
+ ErrorDialog(
+ onConfirmClick = {},
+ dialogTitle = "Title",
+ dialogText = "https://picsum.photos/id/237/200/300")
+ }
+}
diff --git a/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/ImageCard.kt b/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/ImageCard.kt
deleted file mode 100644
index 294e470..0000000
--- a/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/ImageCard.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-package com.manuelnunez.apps.core.ui.component
-
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.material3.Card
-import androidx.compose.material3.CardDefaults
-import androidx.compose.material3.CardElevation
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.semantics.contentDescription
-import androidx.compose.ui.semantics.semantics
-import com.manuelnunez.apps.core.ui.theme.MainTheme
-import com.manuelnunez.apps.core.ui.utils.ThemePreviews
-
-@Composable
-fun ImageCard(
- modifier: Modifier = Modifier,
- imageUrl: String,
- cardContentDescription: String,
- elevation: CardElevation = CardDefaults.cardElevation(),
- contentScale: ContentScale = ContentScale.Crop,
- onClick: (() -> Unit)? = null,
-) {
- Card(
- modifier =
- modifier.clickable(onClick = { onClick?.invoke() }).semantics {
- contentDescription = cardContentDescription
- },
- elevation = elevation) {
- DynamicAsyncImage(
- modifier = Modifier.fillMaxSize(),
- imageUrl = imageUrl,
- contentDescription = "",
- contentScale = contentScale)
- }
-}
-
-@ThemePreviews
-@Composable
-fun ImageCardPreview() {
- MainTheme {
- ImageCard(
- onClick = {},
- cardContentDescription = "",
- imageUrl = "https://picsum.photos/id/237/200/300")
- }
-}
diff --git a/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/DynamicAsyncImage.kt b/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/StatefulAsyncImage.kt
similarity index 93%
rename from core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/DynamicAsyncImage.kt
rename to core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/StatefulAsyncImage.kt
index b68071b..41b9e39 100644
--- a/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/DynamicAsyncImage.kt
+++ b/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/StatefulAsyncImage.kt
@@ -17,15 +17,13 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
-import coil.compose.AsyncImage
import coil.compose.AsyncImagePainter.State.Error
import coil.compose.AsyncImagePainter.State.Loading
import coil.compose.rememberAsyncImagePainter
import com.manuelnunez.apps.core.ui.R
-/** A wrapper around [AsyncImage] which determines the colorFilter based on the theme */
@Composable
-fun DynamicAsyncImage(
+fun StatefulAsyncImage(
modifier: Modifier = Modifier,
imageUrl: String,
contentDescription: String,
diff --git a/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/SurfaceText.kt b/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/SurfaceText.kt
new file mode 100644
index 0000000..c2eafeb
--- /dev/null
+++ b/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/SurfaceText.kt
@@ -0,0 +1,33 @@
+package com.manuelnunez.apps.core.ui.component
+
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.style.TextAlign
+import com.manuelnunez.apps.core.ui.theme.MainTheme
+import com.manuelnunez.apps.core.ui.utils.ThemePreviews
+
+@Composable
+fun SurfaceText(
+ modifier: Modifier = Modifier,
+ text: String,
+ textAlign: TextAlign? = null,
+ style: TextStyle = MaterialTheme.typography.titleLarge
+) {
+ Text(
+ modifier = modifier.semantics { contentDescription = text },
+ text = text,
+ textAlign = textAlign,
+ color = MaterialTheme.colorScheme.onSurface,
+ style = style)
+}
+
+@ThemePreviews
+@Composable
+fun TitleTextPreview() {
+ MainTheme { SurfaceText(text = "See more") }
+}
diff --git a/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/TextCard.kt b/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/TextCard.kt
deleted file mode 100644
index 24baa60..0000000
--- a/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/TextCard.kt
+++ /dev/null
@@ -1,40 +0,0 @@
-package com.manuelnunez.apps.core.ui.component
-
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.material3.Card
-import androidx.compose.material3.CardDefaults
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.semantics.contentDescription
-import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.text.style.TextAlign
-import com.manuelnunez.apps.core.ui.theme.MainTheme
-import com.manuelnunez.apps.core.ui.utils.ThemePreviews
-
-@Composable
-fun TextCard(modifier: Modifier = Modifier, text: String, onClick: () -> Unit) {
- Card(
- colors =
- CardDefaults.cardColors().copy(containerColor = MaterialTheme.colorScheme.onBackground),
- modifier = modifier.clickable(onClick = onClick).semantics { contentDescription = text }) {
- Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
- Text(
- modifier = Modifier.fillMaxWidth(),
- textAlign = TextAlign.Center,
- text = text,
- color = MaterialTheme.colorScheme.background)
- }
- }
-}
-
-@ThemePreviews
-@Composable
-fun TextCardPreview() {
- MainTheme { TextCard(onClick = {}, text = "See more") }
-}
diff --git a/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/TitleText.kt b/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/TitleText.kt
deleted file mode 100644
index a653a0e..0000000
--- a/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/TitleText.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-package com.manuelnunez.apps.core.ui.component
-
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.text.style.TextAlign
-import com.manuelnunez.apps.core.ui.theme.MainTheme
-import com.manuelnunez.apps.core.ui.utils.ThemePreviews
-
-@Composable
-fun TitleText(modifier: Modifier = Modifier, title: String, textAlign: TextAlign? = null) {
- Text(
- modifier = modifier,
- text = title,
- textAlign = textAlign,
- color = MaterialTheme.colorScheme.onSurface,
- style = MaterialTheme.typography.titleLarge)
-}
-
-@Composable
-fun ErrorText(modifier: Modifier = Modifier, title: String, textAlign: TextAlign? = null) {
- Text(
- modifier = modifier,
- text = title,
- textAlign = textAlign,
- color = MaterialTheme.colorScheme.onSurface,
- style = MaterialTheme.typography.titleSmall)
-}
-
-@ThemePreviews
-@Composable
-fun TitleTextPreview() {
- MainTheme { TitleText(title = "See more") }
-}
-
-@ThemePreviews
-@Composable
-fun ErrorTextPreview() {
- MainTheme { ErrorText(title = "See more") }
-}
diff --git a/core/ui/src/main/res/values/strings.xml b/core/ui/src/main/res/values/strings.xml
index 427cdee..d4bf228 100644
--- a/core/ui/src/main/res/values/strings.xml
+++ b/core/ui/src/main/res/values/strings.xml
@@ -10,4 +10,7 @@
"Ups!"
Retry
An Error has occurred. Please go back and try again.
+ Confirm
+ Dismiss
+
\ No newline at end of file
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 fbaa683..a0153e5 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
@@ -10,7 +10,7 @@ import com.manuelnunez.apps.core.ui.R as RCU
@Composable
fun DetailErrorScreen(onBackClick: () -> Unit) {
ErrorDialog(
- onConfirmation = onBackClick,
+ onConfirmClick = onBackClick,
dialogTitle = stringResource(id = RCU.string.alert_error_title),
dialogText = stringResource(id = RCU.string.alert_error_try_again_back))
}
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/feature/detail/ui/components/DetailScreen.kt
index cf35c5f..f200c72 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/feature/detail/ui/components/DetailScreen.kt
@@ -34,8 +34,8 @@ 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.DynamicAsyncImage
-import com.manuelnunez.apps.core.ui.component.TitleText
+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
@@ -44,6 +44,7 @@ import com.manuelnunez.apps.core.ui.R as RCU
@Composable
fun DetailScreen(item: Item, onBackClick: () -> Unit) {
val orientation = LocalConfiguration.current.orientation
+
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
DetailLandscape(item, onBackClick)
} else {
@@ -61,9 +62,9 @@ private fun DetailPortrait(item: Item, onBackClick: () -> Unit) {
Spacer(modifier = Modifier.height(10.dp))
Column(
- modifier = Modifier.weight(1f).wrapContentSize().heightIn(100.dp),
+ modifier = Modifier.weight(1f).wrapContentSize().heightIn(min = 100.dp),
horizontalAlignment = Alignment.CenterHorizontally) {
- DynamicAsyncImage(
+ StatefulAsyncImage(
modifier = Modifier.fillMaxWidth().padding(horizontal = 6.dp),
imageUrl = item.imageUrl,
contentDescription = item.photoId,
@@ -71,12 +72,11 @@ private fun DetailPortrait(item: Item, onBackClick: () -> Unit) {
ShareImage(url = item.imageUrl)
- Text(
+ SurfaceText(
modifier = Modifier.padding(top = 10.dp).padding(horizontal = 40.dp),
textAlign = TextAlign.Center,
text = item.description,
- style = MaterialTheme.typography.titleSmall,
- color = MaterialTheme.colorScheme.onSurface)
+ style = MaterialTheme.typography.titleSmall)
}
Spacer(modifier = Modifier.height(20.dp))
@@ -93,7 +93,7 @@ private fun DetailToolbar(onBackClick: () -> Unit) {
tint = MaterialTheme.colorScheme.onSurface)
}
- TitleText(title = "Details")
+ SurfaceText(text = "Details")
}
}
@@ -107,7 +107,7 @@ private fun DetailLandscape(item: Item, onBackClick: () -> Unit) {
Spacer(modifier = Modifier.height(10.dp))
Row(verticalAlignment = Alignment.CenterVertically) {
- DynamicAsyncImage(
+ StatefulAsyncImage(
modifier = Modifier.fillMaxWidth().padding(vertical = 6.dp).weight(0.7f),
imageUrl = item.imageUrl,
contentDescription = item.photoId,
diff --git a/features/home/ui/src/main/kotlin/com/manuelnunez/apps/features/home/ui/components/HomeErrorScreen.kt b/features/home/ui/src/main/kotlin/com/manuelnunez/apps/features/home/ui/components/HomeErrorScreen.kt
index d094fea..7004a84 100644
--- a/features/home/ui/src/main/kotlin/com/manuelnunez/apps/features/home/ui/components/HomeErrorScreen.kt
+++ b/features/home/ui/src/main/kotlin/com/manuelnunez/apps/features/home/ui/components/HomeErrorScreen.kt
@@ -2,41 +2,34 @@ package com.manuelnunez.apps.features.home.ui.components
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.manuelnunez.apps.core.ui.component.ErrorDialog
-import com.manuelnunez.apps.core.ui.component.ErrorText
+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
-import com.manuelnunez.apps.features.home.ui.R
import com.manuelnunez.apps.core.ui.R as RCU
@Composable
fun HomeErrorScreen(retry: () -> Unit) {
ErrorDialog(
- onConfirmation = retry,
+ onConfirmClick = retry,
dialogTitle = stringResource(id = RCU.string.alert_error_title),
dialogText = stringResource(id = RCU.string.alert_error_try_again))
}
@Composable
-fun FeatureError() {
- ErrorText(
+fun ItemError(textError: String) {
+ SurfaceText(
modifier = Modifier.padding(vertical = 6.dp, horizontal = 20.dp).fillMaxWidth(),
textAlign = TextAlign.Center,
- title = stringResource(id = R.string.alert_error_feature))
-}
-
-@Composable
-fun PopularError() {
- ErrorText(
- modifier = Modifier.padding(vertical = 6.dp, horizontal = 20.dp).fillMaxWidth(),
- textAlign = TextAlign.Center,
- title = stringResource(id = R.string.alert_error_popular))
+ text = textError,
+ style = MaterialTheme.typography.titleSmall)
}
@FontScalingPreviews
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 33f75c5..0284e53 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
@@ -28,8 +28,8 @@ import com.manuelnunez.apps.core.domain.model.Item
import com.manuelnunez.apps.core.ui.component.AdaptableVerticalGrid
import com.manuelnunez.apps.core.ui.component.AdaptableVerticalGridDecoration
import com.manuelnunez.apps.core.ui.component.ImageCard
+import com.manuelnunez.apps.core.ui.component.SurfaceText
import com.manuelnunez.apps.core.ui.component.TextCard
-import com.manuelnunez.apps.core.ui.component.TitleText
import com.manuelnunez.apps.core.ui.theme.MainTheme
import com.manuelnunez.apps.core.ui.utils.FontScalingPreviews
import com.manuelnunez.apps.core.ui.utils.ThemePreviews
@@ -56,7 +56,8 @@ fun HomeScreen(
LoadingIndicator(
loaderContentDescription = stringResource(id = RCU.string.section_feature))
}
- FeaturedItemsState.Error -> item { FeatureError() }
+ FeaturedItemsState.Error ->
+ item { ItemError(stringResource(id = R.string.alert_error_feature)) }
else -> {}
}
@@ -70,7 +71,8 @@ fun HomeScreen(
LoadingIndicator(
loaderContentDescription = stringResource(id = RCU.string.section_popular))
}
- PopularItemsState.Error -> item { PopularError() }
+ PopularItemsState.Error ->
+ item { ItemError(stringResource(id = R.string.alert_error_popular)) }
else -> {}
}
}
@@ -79,9 +81,9 @@ fun HomeScreen(
@Composable
private fun FeaturedItem(items: List- , navigateToDetails: (Item) -> Unit) {
Column {
- TitleText(
+ SurfaceText(
modifier = Modifier.padding(vertical = 6.dp, horizontal = 20.dp),
- title = stringResource(id = RCU.string.section_feature))
+ text = stringResource(id = RCU.string.section_feature))
Spacer(modifier = Modifier.height(10.dp))
@@ -115,9 +117,9 @@ private fun PopularItem(
val gridPadding = 20.dp - horizontalMarginItem
Column {
- TitleText(
+ SurfaceText(
modifier = Modifier.padding(vertical = 6.dp, horizontal = 20.dp),
- title = stringResource(id = RCU.string.section_popular))
+ text = stringResource(id = RCU.string.section_popular))
Spacer(modifier = Modifier.height(10.dp))
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/feature/seemore/ui/components/SeeMoreErrorScreen.kt
index 55fab69..ea9f228 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/feature/seemore/ui/components/SeeMoreErrorScreen.kt
@@ -11,7 +11,7 @@ import com.manuelnunez.apps.core.ui.R as RCU
@Composable
fun SeeMoreErrorScreen(retry: () -> Unit) {
ErrorDialog(
- onConfirmation = retry,
+ onConfirmClick = retry,
dialogTitle = stringResource(id = RCU.string.alert_error_title),
dialogText = stringResource(id = RCU.string.alert_error_try_again))
}
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/feature/seemore/ui/components/SeeMoreScreen.kt
index 9d9a582..bc18564 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/feature/seemore/ui/components/SeeMoreScreen.kt
@@ -36,7 +36,7 @@ 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.ImageCard
-import com.manuelnunez.apps.core.ui.component.TitleText
+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
@@ -108,7 +108,7 @@ private fun SeeMoreToolbar(onBackClick: () -> Unit) {
tint = MaterialTheme.colorScheme.onSurface)
}
- TitleText(title = stringResource(id = RCU.string.section_popular))
+ SurfaceText(text = stringResource(id = RCU.string.section_popular))
}
}
From 6bf703156cdf83fa46eecbde0025f1a125867624 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Manuel=20Nu=C3=B1ez?= <03.manu@gmail.com>
Date: Tue, 16 Apr 2024 13:35:16 -0400
Subject: [PATCH 3/3] Added test and removed result model #41
---
README.md | 29 +--
.../apps/core/common/EitherTest.kt | 27 +++
.../datasource/CataasCatsRemoteDataSource.kt | 12 +-
.../datasource/PexeelsCatsRemoteDataSource.kt | 12 +-
.../CataasCatsRemoteDataSourceTest.kt | 96 ++++++++++
.../PexelsCatsRemoteDataSourceTest.kt | 97 ++++++++++
core/services/build.gradle.kts | 4 +
.../executors/ServiceExecutorRetrofitImpl.kt | 20 ++-
.../services/executors/ServicesExecutor.kt | 16 +-
.../apps/core/services/util/Result.kt | 9 -
core/ui/build.gradle.kts | 9 +-
.../com/manuelnunez/apps/core/ui/ThemeTest.kt | 167 ++++++++++++++++++
.../manuelnunez/apps/core/ui/theme/Theme.kt | 4 +-
.../apps/feature/detail/ui/DetailViewTest.kt | 17 +-
.../apps/features/home/ui/HomeViewTest.kt | 27 ++-
.../apps/features/home/ui/utils/MockUtils.kt | 27 ---
16 files changed, 488 insertions(+), 85 deletions(-)
create mode 100644 core/common/src/test/kotlin/com/manuelnunez/apps/core/common/EitherTest.kt
create mode 100644 core/data/src/test/kotlin/com/manuelnunez/apps/core/data/datasource/CataasCatsRemoteDataSourceTest.kt
create mode 100644 core/data/src/test/kotlin/com/manuelnunez/apps/core/data/datasource/PexelsCatsRemoteDataSourceTest.kt
delete mode 100644 core/services/src/main/kotlin/com/manuelnunez/apps/core/services/util/Result.kt
create mode 100644 core/ui/src/androidTest/kotlin/com/manuelnunez/apps/core/ui/ThemeTest.kt
delete mode 100644 features/home/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/home/ui/utils/MockUtils.kt
diff --git a/README.md b/README.md
index 475aa1e..d85bc37 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# PurrfectPics
+# PurrfectPics ![ic_launcher_round](https://github.com/manununhez/purrfect-pics/assets/5048531/1fab47b6-03fb-4901-b6c9-0fe60cbaecd1)
PurrfectPics is your ultimate companion for discovering, customizing, and sharing adorable cat
images on Android. With a wide range of features, PurrfectPics brings joy to every cat lover's
@@ -16,23 +16,29 @@ heart.
### 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)
-- **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)
+- **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)
+- **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)
- **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)
+- **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-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-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) |
+|------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------|
## Considerations
@@ -44,7 +50,8 @@ heart.
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
- solution is to **open the app from the app tray**, as indicated [here](https://stackoverflow.com/questions/69812590/android-12-splash-screen-icon-not-displaying))
+ solution is to **open the app from the app tray**, as
+ indicated [here](https://stackoverflow.com/questions/69812590/android-12-splash-screen-icon-not-displaying))
## Getting Started
@@ -82,4 +89,4 @@ PurrfectPics is licensed under the [MIT License](LICENSE).
## About
PurrfectPics is developed and maintained by [Manuel Nuñez](mailto:manuel.nunhez90@gmail.com). For
-inquiries, please contact [manuel.nunhez90@gmail.com].
+inquiries, please contact [manuel.nunhez90@gmail.com].
\ No newline at end of file
diff --git a/core/common/src/test/kotlin/com/manuelnunez/apps/core/common/EitherTest.kt b/core/common/src/test/kotlin/com/manuelnunez/apps/core/common/EitherTest.kt
new file mode 100644
index 0000000..469aec0
--- /dev/null
+++ b/core/common/src/test/kotlin/com/manuelnunez/apps/core/common/EitherTest.kt
@@ -0,0 +1,27 @@
+package com.manuelnunez.apps.core.common
+
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Test
+
+class EitherTest {
+
+ @Test
+ fun `test fold with success`() {
+ val successData = 42
+ val successValue: Either = eitherSuccess(successData)
+ val result = successValue.fold(success = { "Success: $it" }, error = { "Error: $it" })
+
+ assertEquals(Either.Success(successData), successValue)
+ assertEquals("Success: $successData", result)
+ }
+
+ @Test
+ fun `test fold with error`() {
+ val messageError = "An error occurred"
+ val errorValue: Either = eitherError(messageError)
+ val result = errorValue.fold(success = { "Success: $it" }, error = { "Error: $it" })
+
+ assertEquals(Either.Error(messageError), errorValue)
+ assertEquals(messageError, result)
+ }
+}
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 e4545fc..de9d737 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
@@ -6,6 +6,7 @@ import androidx.paging.PagingData
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.PAGE_SIZE
import com.manuelnunez.apps.core.data.PREFETCH_DISTANCE
import com.manuelnunez.apps.core.data.datasource.paging.CataasCatsPagingSource
@@ -15,7 +16,6 @@ import com.manuelnunez.apps.core.services.executors.RetrofitServiceRequest
import com.manuelnunez.apps.core.services.executors.ServiceError
import com.manuelnunez.apps.core.services.executors.ServicesExecutor
import com.manuelnunez.apps.core.services.service.CataasService
-import com.manuelnunez.apps.core.services.util.Result
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
@@ -33,8 +33,14 @@ constructor(private val servicesExecutor: ServicesExecutor, private val apiServi
override fun getItems(): Either
, ServiceError> {
val response = servicesExecutor.execute(RetrofitServiceRequest(apiService.searchCats()))
- return if (response is Result.Success) eitherSuccess(response.data.data)
- else eitherError((response as Result.Error).exception)
+ 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 0888244..2ca0ca6 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
@@ -6,6 +6,7 @@ import androidx.paging.PagingData
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.PAGE_SIZE
import com.manuelnunez.apps.core.data.PREFETCH_DISTANCE
import com.manuelnunez.apps.core.data.datasource.paging.PexeelsCatsPagingSource
@@ -15,7 +16,6 @@ import com.manuelnunez.apps.core.services.executors.RetrofitServiceRequest
import com.manuelnunez.apps.core.services.executors.ServiceError
import com.manuelnunez.apps.core.services.executors.ServicesExecutor
import com.manuelnunez.apps.core.services.service.PexelsService
-import com.manuelnunez.apps.core.services.util.Result
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
@@ -33,8 +33,14 @@ constructor(private val servicesExecutor: ServicesExecutor, private val apiServi
override fun getItems(): Either {
val response = servicesExecutor.execute(RetrofitServiceRequest(apiService.searchCats()))
- return if (response is Result.Success) eitherSuccess(response.data.data)
- else eitherError((response as Result.Error).exception)
+ return response.fold(
+ success = {
+ eitherSuccess(it.data)
+ },
+ error = {
+ eitherError(it)
+ }
+ )
}
override fun getAllItems(): Flow> =
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
new file mode 100644
index 0000000..62f7130
--- /dev/null
+++ b/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/datasource/CataasCatsRemoteDataSourceTest.kt
@@ -0,0 +1,96 @@
+package com.manuelnunez.apps.core.data.datasource
+
+import androidx.paging.PagingData
+import androidx.paging.map
+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.mapper.toItems
+import com.manuelnunez.apps.core.data.utils.mockCataasResponseDTOS
+import com.manuelnunez.apps.core.domain.model.Item
+import com.manuelnunez.apps.core.services.dto.CataasResponseDTO
+import com.manuelnunez.apps.core.services.executors.RetrofitServiceRequest
+import com.manuelnunez.apps.core.services.executors.ServiceError
+import com.manuelnunez.apps.core.services.executors.ServicesExecutor
+import com.manuelnunez.apps.core.services.executors.toServiceResponse
+import com.manuelnunez.apps.core.services.service.CataasService
+import io.mockk.coEvery
+import io.mockk.confirmVerified
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.verify
+import kotlinx.coroutines.test.runTest
+import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import retrofit2.Response
+
+class CataasCatsRemoteDataSourceTest {
+ private val servicesExecutor: ServicesExecutor = mockk()
+ private val apiService: CataasService = mockk()
+ private val mockServiceError = mockk()
+
+ private lateinit var remoteDataSource: CataasCatsRemoteDataSource
+
+ @BeforeEach
+ fun setup() {
+ remoteDataSource = CataasCatsRemoteDataSourceImpl(servicesExecutor, apiService)
+ }
+
+ @Test
+ fun `when getItems is called successfully, then returns PexelsSearchResponseDTO`() {
+ val responseSuccess = Response.success(mockCataasResponseDTOS)
+ val resultSuccess = eitherSuccess(responseSuccess.toServiceResponse())
+
+ every { apiService.searchCats().execute() } returns responseSuccess
+ every {
+ servicesExecutor.execute(any>>())
+ } returns resultSuccess
+
+ val result = remoteDataSource.getItems()
+
+ Assertions.assertTrue(result is Either.Success)
+ Assertions.assertEquals(eitherSuccess(mockCataasResponseDTOS), result)
+ verify(exactly = 1) {
+ apiService.searchCats()
+ servicesExecutor.execute(any>>())
+ }
+ confirmVerified(apiService, servicesExecutor)
+ }
+
+ @Test
+ fun `when getItems call fails, then returns ServiceError`() {
+ val responseSuccess = Response.success(mockCataasResponseDTOS)
+ val errorSuccess = eitherError(mockServiceError)
+
+ every { apiService.searchCats().execute() } returns responseSuccess
+ every {
+ servicesExecutor.execute(any>>())
+ } returns errorSuccess
+
+ val result = remoteDataSource.getItems()
+
+ Assertions.assertTrue(result is Either.Error)
+ Assertions.assertEquals(eitherError(mockServiceError), result)
+ verify(exactly = 1) {
+ apiService.searchCats()
+ servicesExecutor.execute(any>>())
+ }
+ confirmVerified(apiService, servicesExecutor)
+ }
+
+ @Test
+ fun `when getAllItems is called, then returns paginated Items`() = runTest {
+ val pagingData: PagingData- = PagingData.from(mockCataasResponseDTOS.toItems())
+
+ coEvery { apiService.searchCatsPaginated(skip = any()) } returns mockCataasResponseDTOS
+
+ val resultFlow = remoteDataSource.getAllItems()
+
+ resultFlow.test {
+ pagingData.map { expected -> awaitItem().map { Assertions.assertEquals(expected, it) } }
+ cancelAndIgnoreRemainingEvents()
+ }
+ }
+}
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
new file mode 100644
index 0000000..c56f297
--- /dev/null
+++ b/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/datasource/PexelsCatsRemoteDataSourceTest.kt
@@ -0,0 +1,97 @@
+package com.manuelnunez.apps.core.data.datasource
+
+import androidx.paging.PagingData
+import androidx.paging.map
+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.mapper.toItems
+import com.manuelnunez.apps.core.data.utils.mockPexelsSearchResponseDTO
+import com.manuelnunez.apps.core.domain.model.Item
+import com.manuelnunez.apps.core.services.dto.PexelsSearchResponseDTO
+import com.manuelnunez.apps.core.services.executors.RetrofitServiceRequest
+import com.manuelnunez.apps.core.services.executors.ServiceError
+import com.manuelnunez.apps.core.services.executors.ServicesExecutor
+import com.manuelnunez.apps.core.services.executors.toServiceResponse
+import com.manuelnunez.apps.core.services.service.PexelsService
+import io.mockk.coEvery
+import io.mockk.confirmVerified
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.verify
+import kotlinx.coroutines.test.runTest
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assertions.assertTrue
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import retrofit2.Response
+
+class PexelsCatsRemoteDataSourceTest {
+ private val servicesExecutor: ServicesExecutor = mockk()
+ private val apiService: PexelsService = mockk()
+ private val mockServiceError = mockk()
+
+ private lateinit var remoteDataSource: PexelsCatsRemoteDataSource
+
+ @BeforeEach
+ fun setup() {
+ remoteDataSource = PexelsCatsRemoteDataSourceImpl(servicesExecutor, apiService)
+ }
+
+ @Test
+ fun `when getItems is called successfully, then returns PexelsSearchResponseDTO`() {
+ val responseSuccess = Response.success(mockPexelsSearchResponseDTO)
+ val resultSuccess = eitherSuccess(responseSuccess.toServiceResponse())
+
+ every { apiService.searchCats().execute() } returns responseSuccess
+ every {
+ servicesExecutor.execute(any>())
+ } returns resultSuccess
+
+ val result = remoteDataSource.getItems()
+
+ assertTrue(result is Either.Success)
+ assertEquals(eitherSuccess(mockPexelsSearchResponseDTO), result)
+ verify(exactly = 1) {
+ apiService.searchCats()
+ servicesExecutor.execute(any>())
+ }
+ confirmVerified(apiService, servicesExecutor)
+ }
+
+ @Test
+ fun `when getItems call fails, then returns ServiceError`() {
+ val responseSuccess = Response.success(mockPexelsSearchResponseDTO)
+ val errorSuccess = eitherError(mockServiceError)
+
+ every { apiService.searchCats().execute() } returns responseSuccess
+ every {
+ servicesExecutor.execute(any>())
+ } returns errorSuccess
+
+ val result = remoteDataSource.getItems()
+
+ assertTrue(result is Either.Error)
+ assertEquals(eitherError(mockServiceError), result)
+ verify(exactly = 1) {
+ apiService.searchCats()
+ servicesExecutor.execute(any>())
+ }
+ confirmVerified(apiService, servicesExecutor)
+ }
+
+ @Test
+ fun `when getAllItems is called, then returns paginated Items`() = runTest {
+ val pagingData: PagingData
- = PagingData.from(mockPexelsSearchResponseDTO.toItems())
+
+ coEvery { apiService.searchCatsPaginated(page = any()) } returns mockPexelsSearchResponseDTO
+
+ val resultFlow = remoteDataSource.getAllItems()
+
+ resultFlow.test {
+ pagingData.map { expected -> awaitItem().map { assertEquals(expected, it) } }
+ cancelAndIgnoreRemainingEvents()
+ }
+ }
+}
diff --git a/core/services/build.gradle.kts b/core/services/build.gradle.kts
index eb7a8d8..8c50bdc 100644
--- a/core/services/build.gradle.kts
+++ b/core/services/build.gradle.kts
@@ -42,9 +42,13 @@ android {
kotlinOptions { jvmTarget = JavaVersion.VERSION_17.toString() }
buildFeatures { buildConfig = true }
+
+ packaging { resources { excludes.add("META-INF/{LICENSE-notice.md,LICENSE.md}") } }
}
dependencies {
+ implementation(projects.core.common)
+
implementation(libs.hilt.android)
ksp(libs.hilt.compiler)
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 6df6ea0..e96261c 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
@@ -1,14 +1,16 @@
package com.manuelnunez.apps.core.services.executors
import com.google.gson.JsonSyntaxException
+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.services.exception.NetworkException
-import com.manuelnunez.apps.core.services.util.Result
import retrofit2.Call
import java.util.concurrent.TimeoutException
class ServiceExecutorRetrofitImpl : ServicesExecutor {
- override fun execute(request: ServiceRequest): Result> {
+ override fun execute(request: ServiceRequest): Either, ServiceError> {
if (request !is RetrofitServiceRequest) {
throw IllegalArgumentException("Accepted only RetrofitServiceRequest")
}
@@ -16,26 +18,26 @@ class ServiceExecutorRetrofitImpl : ServicesExecutor {
return try {
val response = request.retrofitCall.execute()
if (response.isSuccessful) {
- Result.Success(
+ eitherSuccess(
ServiceResponse(
- optData = response.body(),
+ body = response.body(),
statusCode = response.code(),
headers = response.headers().toMultimap()))
} else {
- Result.Error(
+ eitherError(
ServiceError(
description = response.errorBody()?.string(),
statusCode = response.code(),
headers = response.headers().toMultimap()))
}
} catch (ex: TimeoutException) {
- Result.Error(ServiceError("Timeout error: ${ex.message}", -1, emptyMap()))
+ eitherError(ServiceError("Timeout error: ${ex.message}", -1, emptyMap()))
} catch (ex: NetworkException) {
- Result.Error(ServiceError("Network error: ${ex.message}", -1, emptyMap()))
+ eitherError(ServiceError("Network error: ${ex.message}", -1, emptyMap()))
} catch (ex: JsonSyntaxException) {
- Result.Error(ServiceError("Json data parsing error: ${ex.message}", -1, emptyMap()))
+ eitherError(ServiceError("Json data parsing error: ${ex.message}", -1, emptyMap()))
} catch (e: Exception) {
- Result.Error(ServiceError("Unknown error: ${e.message}", -1, emptyMap()))
+ eitherError(ServiceError("Unknown error: ${e.message}", -1, emptyMap()))
}
}
}
diff --git a/core/services/src/main/kotlin/com/manuelnunez/apps/core/services/executors/ServicesExecutor.kt b/core/services/src/main/kotlin/com/manuelnunez/apps/core/services/executors/ServicesExecutor.kt
index 641ed69..2b1108b 100644
--- a/core/services/src/main/kotlin/com/manuelnunez/apps/core/services/executors/ServicesExecutor.kt
+++ b/core/services/src/main/kotlin/com/manuelnunez/apps/core/services/executors/ServicesExecutor.kt
@@ -1,28 +1,32 @@
package com.manuelnunez.apps.core.services.executors
+import com.manuelnunez.apps.core.common.Either
import com.manuelnunez.apps.core.services.exception.NetworkException
import com.manuelnunez.apps.core.services.exception.ServiceException
-import com.manuelnunez.apps.core.services.util.Result
+import retrofit2.Response
interface ServicesExecutor {
@Throws(NetworkException::class, ServiceException::class)
- fun execute(request: ServiceRequest): Result>
+ fun execute(request: ServiceRequest): Either, ServiceError>
}
interface ServiceRequest
-open class ServiceResponse(
- val optData: T?,
+class ServiceResponse(
+ val body: T?,
val statusCode: Int,
val headers: Map>,
) {
val data: T
- get() = optData!!
+ get() = body!!
}
-open class ServiceError(
+class ServiceError(
val description: String?,
val statusCode: Int,
val headers: Map>,
)
+
+fun Response.toServiceResponse() =
+ ServiceResponse(this.body(), this.code(), this.headers().toMultimap())
diff --git a/core/services/src/main/kotlin/com/manuelnunez/apps/core/services/util/Result.kt b/core/services/src/main/kotlin/com/manuelnunez/apps/core/services/util/Result.kt
deleted file mode 100644
index bfde2be..0000000
--- a/core/services/src/main/kotlin/com/manuelnunez/apps/core/services/util/Result.kt
+++ /dev/null
@@ -1,9 +0,0 @@
-package com.manuelnunez.apps.core.services.util
-
-import com.manuelnunez.apps.core.services.executors.ServiceError
-
-sealed interface Result {
- data class Success(val data: T) : Result
-
- data class Error(val exception: ServiceError) : Result
-}
diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts
index c2d373d..79a3351 100644
--- a/core/ui/build.gradle.kts
+++ b/core/ui/build.gradle.kts
@@ -12,7 +12,10 @@ android {
targetCompatibility = JavaVersion.VERSION_17
}
- defaultConfig { minSdk = 21 }
+ defaultConfig {
+ minSdk = 21
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ }
kotlinOptions { jvmTarget = JavaVersion.VERSION_17.toString() }
@@ -33,4 +36,8 @@ dependencies {
val composeBom = platform(libs.androidx.compose.bom)
implementation(composeBom)
debugImplementation(libs.androidx.ui.tooling)
+
+ testImplementation(libs.junit)
+ api(libs.androidx.compose.ui.test.junit4)
+ debugApi(libs.androidx.compose.ui.test.manifest)
}
diff --git a/core/ui/src/androidTest/kotlin/com/manuelnunez/apps/core/ui/ThemeTest.kt b/core/ui/src/androidTest/kotlin/com/manuelnunez/apps/core/ui/ThemeTest.kt
new file mode 100644
index 0000000..f6e7303
--- /dev/null
+++ b/core/ui/src/androidTest/kotlin/com/manuelnunez/apps/core/ui/ThemeTest.kt
@@ -0,0 +1,167 @@
+package com.manuelnunez.apps.core.ui
+
+import android.os.Build.VERSION.SDK_INT
+import android.os.Build.VERSION_CODES
+import androidx.compose.material3.ColorScheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.material3.surfaceColorAtElevation
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.dp
+import com.manuelnunez.apps.core.ui.theme.BackgroundTheme
+import com.manuelnunez.apps.core.ui.theme.DarkColorScheme
+import com.manuelnunez.apps.core.ui.theme.GradientColors
+import com.manuelnunez.apps.core.ui.theme.LightColorScheme
+import com.manuelnunez.apps.core.ui.theme.LocalBackgroundTheme
+import com.manuelnunez.apps.core.ui.theme.LocalGradientColors
+import com.manuelnunez.apps.core.ui.theme.MainTheme
+import org.junit.Assert.assertEquals
+import org.junit.Rule
+import org.junit.Test
+
+class ThemeTest {
+
+ @get:Rule val composeTestRule = createComposeRule()
+
+ @Test
+ fun darkThemeFalse_dynamicColorFalse_androidThemeFalse() {
+ composeTestRule.setContent {
+ MainTheme(
+ darkTheme = false,
+ disableDynamicTheming = true,
+ ) {
+ val colorScheme = LightColorScheme
+ assertColorSchemesEqual(colorScheme, MaterialTheme.colorScheme)
+ val gradientColors = defaultGradientColors(colorScheme)
+ assertEquals(gradientColors, LocalGradientColors.current)
+ val backgroundTheme = defaultBackgroundTheme(colorScheme)
+ assertEquals(backgroundTheme, LocalBackgroundTheme.current)
+ }
+ }
+ }
+
+ @Test
+ fun darkThemeTrue_dynamicColorFalse_androidThemeFalse() {
+ composeTestRule.setContent {
+ MainTheme(
+ darkTheme = true,
+ disableDynamicTheming = true,
+ ) {
+ val colorScheme = DarkColorScheme
+ assertColorSchemesEqual(colorScheme, MaterialTheme.colorScheme)
+ val gradientColors = defaultGradientColors(colorScheme)
+ assertEquals(gradientColors, LocalGradientColors.current)
+ val backgroundTheme = defaultBackgroundTheme(colorScheme)
+ assertEquals(backgroundTheme, LocalBackgroundTheme.current)
+ }
+ }
+ }
+
+ @Test
+ fun darkThemeFalse_dynamicColorTrue_androidThemeFalse() {
+ composeTestRule.setContent {
+ MainTheme(
+ darkTheme = false,
+ disableDynamicTheming = false,
+ ) {
+ val colorScheme = dynamicLightColorSchemeWithFallback()
+ assertColorSchemesEqual(colorScheme, MaterialTheme.colorScheme)
+ val gradientColors = dynamicGradientColorsWithFallback(colorScheme)
+ assertEquals(gradientColors, LocalGradientColors.current)
+ val backgroundTheme = defaultBackgroundTheme(colorScheme)
+ assertEquals(backgroundTheme, LocalBackgroundTheme.current)
+ }
+ }
+ }
+
+ @Test
+ fun darkThemeTrue_dynamicColorTrue_androidThemeFalse() {
+ composeTestRule.setContent {
+ MainTheme(
+ darkTheme = true,
+ disableDynamicTheming = false,
+ ) {
+ val colorScheme = dynamicDarkColorSchemeWithFallback()
+ assertColorSchemesEqual(colorScheme, MaterialTheme.colorScheme)
+ val gradientColors = dynamicGradientColorsWithFallback(colorScheme)
+ assertEquals(gradientColors, LocalGradientColors.current)
+ val backgroundTheme = defaultBackgroundTheme(colorScheme)
+ assertEquals(backgroundTheme, LocalBackgroundTheme.current)
+ }
+ }
+ }
+
+ @Composable
+ private fun dynamicLightColorSchemeWithFallback(): ColorScheme =
+ when {
+ SDK_INT >= VERSION_CODES.S -> dynamicLightColorScheme(LocalContext.current)
+ else -> LightColorScheme
+ }
+
+ @Composable
+ private fun dynamicDarkColorSchemeWithFallback(): ColorScheme =
+ when {
+ SDK_INT >= VERSION_CODES.S -> dynamicDarkColorScheme(LocalContext.current)
+ else -> DarkColorScheme
+ }
+
+ private fun emptyGradientColors(colorScheme: ColorScheme): GradientColors =
+ GradientColors(container = colorScheme.surfaceColorAtElevation(2.dp))
+
+ private fun defaultGradientColors(colorScheme: ColorScheme): GradientColors =
+ GradientColors(
+ top = colorScheme.inverseOnSurface,
+ bottom = colorScheme.primaryContainer,
+ container = colorScheme.surface,
+ )
+
+ private fun dynamicGradientColorsWithFallback(colorScheme: ColorScheme): GradientColors =
+ when {
+ SDK_INT >= VERSION_CODES.S -> emptyGradientColors(colorScheme)
+ else -> defaultGradientColors(colorScheme)
+ }
+
+ private fun defaultBackgroundTheme(colorScheme: ColorScheme): BackgroundTheme =
+ BackgroundTheme(
+ color = colorScheme.surface,
+ tonalElevation = 2.dp,
+ )
+
+ /** Workaround for the fact that the NiA design system specify all color scheme values. */
+ private fun assertColorSchemesEqual(
+ expectedColorScheme: ColorScheme,
+ actualColorScheme: ColorScheme,
+ ) {
+ assertEquals(expectedColorScheme.primary, actualColorScheme.primary)
+ assertEquals(expectedColorScheme.onPrimary, actualColorScheme.onPrimary)
+ assertEquals(expectedColorScheme.primaryContainer, actualColorScheme.primaryContainer)
+ assertEquals(expectedColorScheme.onPrimaryContainer, actualColorScheme.onPrimaryContainer)
+ assertEquals(expectedColorScheme.secondary, actualColorScheme.secondary)
+ assertEquals(expectedColorScheme.onSecondary, actualColorScheme.onSecondary)
+ assertEquals(expectedColorScheme.secondaryContainer, actualColorScheme.secondaryContainer)
+ assertEquals(
+ expectedColorScheme.onSecondaryContainer,
+ actualColorScheme.onSecondaryContainer,
+ )
+ assertEquals(expectedColorScheme.tertiary, actualColorScheme.tertiary)
+ assertEquals(expectedColorScheme.onTertiary, actualColorScheme.onTertiary)
+ assertEquals(expectedColorScheme.tertiaryContainer, actualColorScheme.tertiaryContainer)
+ assertEquals(expectedColorScheme.onTertiaryContainer, actualColorScheme.onTertiaryContainer)
+ assertEquals(expectedColorScheme.error, actualColorScheme.error)
+ assertEquals(expectedColorScheme.onError, actualColorScheme.onError)
+ assertEquals(expectedColorScheme.errorContainer, actualColorScheme.errorContainer)
+ assertEquals(expectedColorScheme.onErrorContainer, actualColorScheme.onErrorContainer)
+ assertEquals(expectedColorScheme.background, actualColorScheme.background)
+ assertEquals(expectedColorScheme.onBackground, actualColorScheme.onBackground)
+ assertEquals(expectedColorScheme.surface, actualColorScheme.surface)
+ assertEquals(expectedColorScheme.onSurface, actualColorScheme.onSurface)
+ assertEquals(expectedColorScheme.surfaceVariant, actualColorScheme.surfaceVariant)
+ assertEquals(expectedColorScheme.onSurfaceVariant, actualColorScheme.onSurfaceVariant)
+ assertEquals(expectedColorScheme.inverseSurface, actualColorScheme.inverseSurface)
+ assertEquals(expectedColorScheme.inverseOnSurface, actualColorScheme.inverseOnSurface)
+ assertEquals(expectedColorScheme.outline, actualColorScheme.outline)
+ }
+}
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 fdaa029..c35280a 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
@@ -15,7 +15,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import com.manuelnunez.apps.core.ui.component.MainGradientBackground
-private val LightColorScheme =
+val LightColorScheme =
lightColorScheme(
primary = primaryLight,
onPrimary = onPrimaryLight,
@@ -54,7 +54,7 @@ private val LightColorScheme =
surfaceContainerHighest = surfaceContainerHighestLight,
)
-private val DarkColorScheme =
+val DarkColorScheme =
darkColorScheme(
primary = primaryDark,
onPrimary = onPrimaryDark,
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 dcfdccd..0f5f1e7 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
@@ -2,6 +2,7 @@ package com.manuelnunez.apps.feature.detail.ui
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.assertHasClickAction
+import androidx.compose.ui.test.assertHasNoClickAction
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
@@ -22,12 +23,7 @@ class DetailViewTest {
composeTestRule.setContent { DetailScreen(mockItem, onBackClick = {}) }
// description
- composeTestRule
- .onNodeWithText(
- mockItem.description,
- substring = true,
- )
- .assertExists()
+ composeTestRule.onNodeWithText(mockItem.description).assertExists()
// share button
composeTestRule
@@ -40,12 +36,9 @@ class DetailViewTest {
// Image
composeTestRule
- .onNodeWithContentDescription(
- mockItem.photoId,
- substring = true,
- )
+ .onNodeWithContentDescription(mockItem.photoId)
.assertExists()
- .assertHasClickAction()
+ .assertHasNoClickAction()
}
@Test
@@ -61,7 +54,7 @@ class DetailViewTest {
composeTestRule
.onNodeWithContentDescription(
- composeTestRule.activity.resources.getString(RCU.string.button_back),
+ composeTestRule.activity.resources.getString(RCU.string.alert_dialog_confirm_button),
substring = true,
)
.assertExists()
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 6c9e24c..e1cf49d 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
@@ -5,10 +5,9 @@ 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.home.ui.components.HomeErrorScreen
import com.manuelnunez.apps.features.home.ui.components.HomeScreen
-import com.manuelnunez.apps.features.home.ui.utils.mockFeaturedPhotos
-import com.manuelnunez.apps.features.home.ui.utils.mockPopularPhotos
import org.junit.Rule
import org.junit.Test
import com.manuelnunez.apps.core.ui.R as RCU
@@ -143,4 +142,28 @@ class HomeViewTest {
)
.assertExists()
}
+
+ private val mockPopularPhotos =
+ 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 popular items $id")
+ }
+ .shuffled()
+ .take(10)
+
+ private val mockFeaturedPhotos =
+ 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 featured items $id")
+ }
+ .shuffled()
+ .take(5)
}
diff --git a/features/home/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/home/ui/utils/MockUtils.kt b/features/home/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/home/ui/utils/MockUtils.kt
deleted file mode 100644
index 97d9d69..0000000
--- a/features/home/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/home/ui/utils/MockUtils.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-package com.manuelnunez.apps.features.home.ui.utils
-
-import com.manuelnunez.apps.core.domain.model.Item
-
-val mockPopularPhotos =
- 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 popular items $id")
- }
- .shuffled()
- .take(10)
-
-val mockFeaturedPhotos =
- 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 featured items $id")
- }
- .shuffled()
- .take(5)