diff --git a/app/src/androidInstrumentedTest/kotlin/com/google/samples/apps/nowinandroid/ui/NavigationTest.kt b/app/src/androidInstrumentedTest/kotlin/com/google/samples/apps/nowinandroid/ui/NavigationTest.kt
index d9989d2ce..84400bb09 100644
--- a/app/src/androidInstrumentedTest/kotlin/com/google/samples/apps/nowinandroid/ui/NavigationTest.kt
+++ b/app/src/androidInstrumentedTest/kotlin/com/google/samples/apps/nowinandroid/ui/NavigationTest.kt
@@ -16,13 +16,16 @@
package com.google.samples.apps.nowinandroid.ui
+import androidx.compose.ui.semantics.SemanticsActions.ScrollBy
import androidx.compose.ui.test.assertCountEquals
import androidx.compose.ui.test.assertIsOn
import androidx.compose.ui.test.assertIsSelected
import androidx.compose.ui.test.hasTestTag
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onAllNodesWithTag
import androidx.compose.ui.test.onAllNodesWithText
+import androidx.compose.ui.test.onFirst
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
@@ -78,6 +81,9 @@ class NavigationTest {
@Inject
lateinit var topicsRepository: TopicsRepository
+ @Inject
+ lateinit var newsRepository: NewsRepository
+
// The strings used for matching in these tests
private val navigateUp by composeTestRule.stringResource(FeatureForyouR.string.feature_foryou_navigate_up)
private val forYou by composeTestRule.stringResource(FeatureForyouR.string.feature_foryou_title)
@@ -270,4 +276,44 @@ class NavigationTest {
onNodeWithTag("topic:${topic.id}").assertExists()
}
}
+
+ @Test
+ fun navigatingToTopicFromForYou_showsTopicDetails() {
+ composeTestRule.apply {
+ // Get the first news resource
+ val newsResource = runBlocking {
+ newsRepository.getNewsResources().first().first()
+ }
+
+ // Get its first topic and follow it
+ val topic = newsResource.topics.first()
+ onNodeWithText(topic.name).performClick()
+
+ // Get the news feed and scroll to the news resource
+ // Note: Possible flakiness. If the content of the news resource is long then the topic
+ // tag might not be visible meaning it cannot be clicked
+ onNodeWithTag("forYou:feed")
+ .performScrollToNode(hasTestTag("newsResourceCard:${newsResource.id}"))
+ .fetchSemanticsNode()
+ .apply {
+ val newsResourceCardNode = onNodeWithTag("newsResourceCard:${newsResource.id}")
+ .fetchSemanticsNode()
+ config[ScrollBy].action?.invoke(
+ 0f,
+ // to ensure the bottom of the card is visible,
+ // manually scroll the difference between the height of
+ // the scrolling node and the height of the card
+ (newsResourceCardNode.size.height - size.height).coerceAtLeast(0).toFloat(),
+ )
+ }
+
+ // Click the first topic tag
+ onAllNodesWithTag("topicTag:${topic.id}", useUnmergedTree = true)
+ .onFirst()
+ .performClick()
+
+ // Verify that we're on the correct topic details screen
+ onNodeWithTag("topic:${topic.id}").assertExists()
+ }
+ }
}
diff --git a/app/src/androidMain/res/values/colors.xml b/app/src/androidMain/res/values/colors.xml
index 5a3dc450f..2b8c739cd 100644
--- a/app/src/androidMain/res/values/colors.xml
+++ b/app/src/androidMain/res/values/colors.xml
@@ -15,9 +15,6 @@
limitations under the License.
-->
-
- #4D000000
-
#000000
#FCFCFC
diff --git a/app/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/navigation/NiaNavHost.kt b/app/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/navigation/NiaNavHost.kt
index 035c19faf..20d60e3af 100644
--- a/app/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/navigation/NiaNavHost.kt
+++ b/app/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/navigation/NiaNavHost.kt
@@ -20,10 +20,12 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.navigation.compose.NavHost
import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.bookmarksScreen
-import com.google.samples.apps.nowinandroid.feature.foryou.navigation.ForYouRoute
-import com.google.samples.apps.nowinandroid.feature.foryou.navigation.forYouScreen
+import com.google.samples.apps.nowinandroid.feature.foryou.navigation.ForYouBaseRoute
+import com.google.samples.apps.nowinandroid.feature.foryou.navigation.forYouSection
import com.google.samples.apps.nowinandroid.feature.interests.navigation.navigateToInterests
import com.google.samples.apps.nowinandroid.feature.search.navigation.searchScreen
+import com.google.samples.apps.nowinandroid.feature.topic.navigation.navigateToTopic
+import com.google.samples.apps.nowinandroid.feature.topic.navigation.topicScreen
import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination.INTERESTS
import com.google.samples.apps.nowinandroid.ui.NiaAppState
import com.google.samples.apps.nowinandroid.ui.interests2pane.interestsListDetailScreen
@@ -44,10 +46,18 @@ fun NiaNavHost(
val navController = appState.navController
NavHost(
navController = navController,
- startDestination = ForYouRoute,
+ startDestination = ForYouBaseRoute,
modifier = modifier,
) {
- forYouScreen(onTopicClick = navController::navigateToInterests)
+ forYouSection(
+ onTopicClick = navController::navigateToTopic,
+ ) {
+ topicScreen(
+ showBackButton = true,
+ onBackClick = navController::popBackStack,
+ onTopicClick = navController::navigateToTopic,
+ )
+ }
bookmarksScreen(
onTopicClick = navController::navigateToInterests,
onShowSnackbar = onShowSnackbar,
diff --git a/app/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/navigation/TopLevelDestination.kt b/app/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/navigation/TopLevelDestination.kt
index d6bf34d4e..32592eb6c 100644
--- a/app/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/navigation/TopLevelDestination.kt
+++ b/app/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/navigation/TopLevelDestination.kt
@@ -19,6 +19,7 @@ package com.google.samples.apps.nowinandroid.navigation
import androidx.compose.ui.graphics.vector.ImageVector
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.BookmarksRoute
+import com.google.samples.apps.nowinandroid.feature.foryou.navigation.ForYouBaseRoute
import com.google.samples.apps.nowinandroid.feature.foryou.navigation.ForYouRoute
import com.google.samples.apps.nowinandroid.feature.interests.navigation.InterestsRoute
import nowinandroid.feature.bookmarks.generated.resources.feature_bookmarks_title
@@ -33,9 +34,18 @@ import nowinandroid.feature.foryou.generated.resources.Res as forYouR
import nowinandroid.feature.search.generated.resources.Res as searchR
/**
- * Type for the top level destinations in the application. Each of these destinations
- * can contain one or more screens (based on the window size). Navigation from one screen to the
- * next within a single destination will be handled directly in composables.
+ * Type for the top level destinations in the application. Contains metadata about the destination
+ * that is used in the top app bar and common navigation UI.
+ *
+ * @param selectedIcon The icon to be displayed in the navigation UI when this destination is
+ * selected.
+ * @param unselectedIcon The icon to be displayed in the navigation UI when this destination is
+ * not selected.
+ * @param iconTextId Text that to be displayed in the navigation UI.
+ * @param titleTextId Text that is displayed on the top app bar.
+ * @param route The route to use when navigating to this destination.
+ * @param baseRoute The highest ancestor of this destination. Defaults to [route], meaning that
+ * there is a single destination in that section of the app (no nested destinations).
*/
enum class TopLevelDestination(
val selectedIcon: ImageVector,
@@ -43,6 +53,7 @@ enum class TopLevelDestination(
val iconTextId: StringResource,
val titleTextId: StringResource,
val route: KClass<*>,
+ val baseRoute: KClass<*> = route,
) {
FOR_YOU(
selectedIcon = NiaIcons.Upcoming,
@@ -50,6 +61,7 @@ enum class TopLevelDestination(
iconTextId = forYouR.string.feature_foryou_title,
titleTextId = Res.string.app_name,
route = ForYouRoute::class,
+ baseRoute = ForYouBaseRoute::class,
),
BOOKMARKS(
selectedIcon = NiaIcons.Bookmarks,
diff --git a/app/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/ui/NiaApp.kt b/app/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/ui/NiaApp.kt
index 492961df1..143e6e1ff 100644
--- a/app/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/ui/NiaApp.kt
+++ b/app/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/ui/NiaApp.kt
@@ -147,7 +147,7 @@ internal fun NiaApp(
appState.topLevelDestinations.forEach { destination ->
val hasUnread = unreadDestinations.contains(destination)
val selected = currentDestination
- .isRouteInHierarchy(destination.route)
+ .isRouteInHierarchy(destination.baseRoute)
item(
selected = selected,
onClick = { appState.navigateToTopLevelDestination(destination) },
diff --git a/app/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppState.kt b/app/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppState.kt
index 3b1ed4a52..7647d4f33 100644
--- a/app/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppState.kt
+++ b/app/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppState.kt
@@ -88,7 +88,7 @@ class NiaAppState(
val currentTopLevelDestination: TopLevelDestination?
@Composable get() {
return TopLevelDestination.entries.firstOrNull { topLevelDestination ->
- currentDestination?.hasRoute(route = topLevelDestination.route) ?: false
+ currentDestination?.hasRoute(route = topLevelDestination.route) == true
}
}
diff --git a/app/src/testDemo/screenshots/compactWidth_expandedHeight_showsNavigationBar.png b/app/src/testDemo/screenshots/compactWidth_expandedHeight_showsNavigationBar.png
index e052b5920..30873b584 100644
Binary files a/app/src/testDemo/screenshots/compactWidth_expandedHeight_showsNavigationBar.png and b/app/src/testDemo/screenshots/compactWidth_expandedHeight_showsNavigationBar.png differ
diff --git a/app/src/testDemo/screenshots/expandedWidth_expandedHeight_showsNavigationRail.png b/app/src/testDemo/screenshots/expandedWidth_expandedHeight_showsNavigationRail.png
index 53bf6f3c5..e2dffaf01 100644
Binary files a/app/src/testDemo/screenshots/expandedWidth_expandedHeight_showsNavigationRail.png and b/app/src/testDemo/screenshots/expandedWidth_expandedHeight_showsNavigationRail.png differ
diff --git a/app/src/testDemo/screenshots/insets_snackbar_expanded_expanded.png b/app/src/testDemo/screenshots/insets_snackbar_expanded_expanded.png
index 3d2c79256..253b6be4c 100644
Binary files a/app/src/testDemo/screenshots/insets_snackbar_expanded_expanded.png and b/app/src/testDemo/screenshots/insets_snackbar_expanded_expanded.png differ
diff --git a/app/src/testDemo/screenshots/mediumWidth_expandedHeight_showsNavigationRail.png b/app/src/testDemo/screenshots/mediumWidth_expandedHeight_showsNavigationRail.png
index 3e38938d6..79f808f44 100644
Binary files a/app/src/testDemo/screenshots/mediumWidth_expandedHeight_showsNavigationRail.png and b/app/src/testDemo/screenshots/mediumWidth_expandedHeight_showsNavigationRail.png differ
diff --git a/app/src/testDemo/screenshots/snackbar_expanded_expanded.png b/app/src/testDemo/screenshots/snackbar_expanded_expanded.png
index 4997a83af..841a02eaf 100644
Binary files a/app/src/testDemo/screenshots/snackbar_expanded_expanded.png and b/app/src/testDemo/screenshots/snackbar_expanded_expanded.png differ
diff --git a/app/src/testDemo/screenshots/snackbar_medium_medium.png b/app/src/testDemo/screenshots/snackbar_medium_medium.png
index 36fffa9c6..2800575b8 100644
Binary files a/app/src/testDemo/screenshots/snackbar_medium_medium.png and b/app/src/testDemo/screenshots/snackbar_medium_medium.png differ
diff --git a/build-logic/convention/src/main/kotlin/JvmLibraryConventionPlugin.kt b/build-logic/convention/src/main/kotlin/JvmLibraryConventionPlugin.kt
index 35932c835..652409db6 100644
--- a/build-logic/convention/src/main/kotlin/JvmLibraryConventionPlugin.kt
+++ b/build-logic/convention/src/main/kotlin/JvmLibraryConventionPlugin.kt
@@ -17,6 +17,8 @@
import com.google.samples.apps.nowinandroid.configureKotlinJvm
import org.gradle.api.Plugin
import org.gradle.api.Project
+import org.gradle.kotlin.dsl.dependencies
+import org.gradle.kotlin.dsl.kotlin
class JvmLibraryConventionPlugin : Plugin {
override fun apply(target: Project) {
@@ -26,6 +28,9 @@ class JvmLibraryConventionPlugin : Plugin {
apply("nowinandroid.android.lint")
}
configureKotlinJvm()
+ dependencies {
+ add("testImplementation", kotlin("test"))
+ }
}
}
}
diff --git a/core/data-test/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/data/test/repository/FakeNewsRepository.kt b/core/data-test/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/data/test/repository/FakeNewsRepository.kt
index e4a2ea6b9..5767c9242 100644
--- a/core/data-test/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/data/test/repository/FakeNewsRepository.kt
+++ b/core/data-test/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/data/test/repository/FakeNewsRepository.kt
@@ -17,14 +17,11 @@
package com.google.samples.apps.nowinandroid.core.data.test.repository
import com.google.samples.apps.nowinandroid.core.data.Synchronizer
-import com.google.samples.apps.nowinandroid.core.data.model.asEntity
+import com.google.samples.apps.nowinandroid.core.data.model.asExternalModel
import com.google.samples.apps.nowinandroid.core.data.repository.NewsRepository
import com.google.samples.apps.nowinandroid.core.data.repository.NewsResourceQuery
-import com.google.samples.apps.nowinandroid.core.database.model.NewsResourceEntity
-import com.google.samples.apps.nowinandroid.core.database.model.asExternalModel
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.network.demo.DemoNiaNetworkDataSource
-import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
@@ -45,9 +42,11 @@ class FakeNewsRepository(
query: NewsResourceQuery,
): Flow> =
flow {
+ val newsResources = datasource.getNewsResources()
+ val topics = datasource.getTopics()
+
emit(
- datasource
- .getNewsResources()
+ newsResources
.filter { networkNewsResource ->
// Filter out any news resources which don't match the current query.
// If no query parameters (filterTopicIds or filterNewsIds) are specified
@@ -61,8 +60,7 @@ class FakeNewsRepository(
)
.all(true::equals)
}
- .map(NetworkNewsResource::asEntity)
- .map(NewsResourceEntity::asExternalModel),
+ .map { it.asExternalModel(topics) },
)
}.flowOn(ioDispatcher)
diff --git a/core/data/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/data/model/NewsResource.kt b/core/data/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/data/model/NewsResource.kt
index c3ad91dfe..01d0905f0 100644
--- a/core/data/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/data/model/NewsResource.kt
+++ b/core/data/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/data/model/NewsResource.kt
@@ -19,8 +19,10 @@ package com.google.samples.apps.nowinandroid.core.data.model
import com.google.samples.apps.nowinandroid.core.database.model.NewsResourceEntity
import com.google.samples.apps.nowinandroid.core.database.model.NewsResourceTopicCrossRef
import com.google.samples.apps.nowinandroid.core.database.model.TopicEntity
+import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource
-import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResourceExpanded
+import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic
+import com.google.samples.apps.nowinandroid.core.network.model.asExternalModel
fun NetworkNewsResource.asEntity() = NewsResourceEntity(
id = id,
@@ -32,16 +34,6 @@ fun NetworkNewsResource.asEntity() = NewsResourceEntity(
type = type,
)
-fun NetworkNewsResourceExpanded.asEntity() = NewsResourceEntity(
- id = id,
- title = title,
- content = content,
- url = url,
- headerImageUrl = headerImageUrl,
- publishDate = publishDate,
- type = type,
-)
-
/**
* A shell [TopicEntity] to fulfill the foreign key constraint when inserting
* a [NewsResourceEntity] into the DB
@@ -65,3 +57,17 @@ fun NetworkNewsResource.topicCrossReferences(): List
topicId = topicId,
)
}
+
+fun NetworkNewsResource.asExternalModel(topics: List) =
+ NewsResource(
+ id = id,
+ title = title,
+ content = content,
+ url = url,
+ headerImageUrl = headerImageUrl,
+ publishDate = publishDate,
+ type = type,
+ topics = topics
+ .filter { networkTopic -> this.topics.contains(networkTopic.id) }
+ .map(NetworkTopic::asExternalModel),
+ )
diff --git a/core/data/src/commonTest/kotlin/com/google/samples/apps/nowinandroid/core/data/model/NetworkEntityKtTest.kt b/core/data/src/commonTest/kotlin/com/google/samples/apps/nowinandroid/core/data/model/NetworkEntityKtTest.kt
deleted file mode 100644
index da64b9ee8..000000000
--- a/core/data/src/commonTest/kotlin/com/google/samples/apps/nowinandroid/core/data/model/NetworkEntityKtTest.kt
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.google.samples.apps.nowinandroid.core.data.model
-
-import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource
-import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResourceExpanded
-import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic
-import kotlinx.datetime.Instant
-import kotlin.test.Test
-import kotlin.test.assertEquals
-
-class NetworkEntityKtTest {
-
- @Test
- fun network_topic_can_be_mapped_to_topic_entity() {
- val networkModel = NetworkTopic(
- id = "0",
- name = "Test",
- shortDescription = "short description",
- longDescription = "long description",
- url = "URL",
- imageUrl = "image URL",
- )
- val entity = networkModel.asEntity()
-
- assertEquals("0", entity.id)
- assertEquals("Test", entity.name)
- assertEquals("short description", entity.shortDescription)
- assertEquals("long description", entity.longDescription)
- assertEquals("URL", entity.url)
- assertEquals("image URL", entity.imageUrl)
- }
-
- @Test
- fun network_news_resource_can_be_mapped_to_news_resource_entity() {
- val networkModel =
- NetworkNewsResource(
- id = "0",
- title = "title",
- content = "content",
- url = "url",
- headerImageUrl = "headerImageUrl",
- publishDate = Instant.fromEpochMilliseconds(1),
- type = "Article 📚",
- )
- val entity = networkModel.asEntity()
-
- assertEquals("0", entity.id)
- assertEquals("title", entity.title)
- assertEquals("content", entity.content)
- assertEquals("url", entity.url)
- assertEquals("headerImageUrl", entity.headerImageUrl)
- assertEquals(Instant.fromEpochMilliseconds(1), entity.publishDate)
- assertEquals("Article 📚", entity.type)
-
- val expandedNetworkModel =
- NetworkNewsResourceExpanded(
- id = "0",
- title = "title",
- content = "content",
- url = "url",
- headerImageUrl = "headerImageUrl",
- publishDate = Instant.fromEpochMilliseconds(1),
- type = "Article 📚",
- )
-
- val entityFromExpanded = expandedNetworkModel.asEntity()
-
- assertEquals("0", entityFromExpanded.id)
- assertEquals("title", entityFromExpanded.title)
- assertEquals("content", entityFromExpanded.content)
- assertEquals("url", entityFromExpanded.url)
- assertEquals("headerImageUrl", entityFromExpanded.headerImageUrl)
- assertEquals(Instant.fromEpochMilliseconds(1), entityFromExpanded.publishDate)
- assertEquals("Article 📚", entityFromExpanded.type)
- }
-}
diff --git a/core/data/src/commonTest/kotlin/com/google/samples/apps/nowinandroid/core/data/model/NetworkEntityTest.kt b/core/data/src/commonTest/kotlin/com/google/samples/apps/nowinandroid/core/data/model/NetworkEntityTest.kt
new file mode 100644
index 000000000..52dbe5117
--- /dev/null
+++ b/core/data/src/commonTest/kotlin/com/google/samples/apps/nowinandroid/core/data/model/NetworkEntityTest.kt
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.nowinandroid.core.data.model
+
+import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
+import com.google.samples.apps.nowinandroid.core.model.data.Topic
+import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource
+import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic
+import com.google.samples.apps.nowinandroid.core.network.model.asExternalModel
+import kotlinx.datetime.Instant
+import org.junit.Test
+import kotlin.test.assertEquals
+
+class NetworkEntityTest {
+
+ @Test
+ fun networkTopicMapsToDatabaseModel() {
+ val networkModel = NetworkTopic(
+ id = "0",
+ name = "Test",
+ shortDescription = "short description",
+ longDescription = "long description",
+ url = "URL",
+ imageUrl = "image URL",
+ )
+ val entity = networkModel.asEntity()
+
+ assertEquals("0", entity.id)
+ assertEquals("Test", entity.name)
+ assertEquals("short description", entity.shortDescription)
+ assertEquals("long description", entity.longDescription)
+ assertEquals("URL", entity.url)
+ assertEquals("image URL", entity.imageUrl)
+ }
+
+ @Test
+ fun networkNewsResourceMapsToDatabaseModel() {
+ val networkModel =
+ NetworkNewsResource(
+ id = "0",
+ title = "title",
+ content = "content",
+ url = "url",
+ headerImageUrl = "headerImageUrl",
+ publishDate = Instant.fromEpochMilliseconds(1),
+ type = "Article 📚",
+ )
+ val entity = networkModel.asEntity()
+
+ assertEquals("0", entity.id)
+ assertEquals("title", entity.title)
+ assertEquals("content", entity.content)
+ assertEquals("url", entity.url)
+ assertEquals("headerImageUrl", entity.headerImageUrl)
+ assertEquals(Instant.fromEpochMilliseconds(1), entity.publishDate)
+ assertEquals("Article 📚", entity.type)
+ }
+
+ @Test
+ fun networkTopicMapsToExternalModel() {
+ val networkTopic = NetworkTopic(
+ id = "0",
+ name = "Test",
+ shortDescription = "short description",
+ longDescription = "long description",
+ url = "URL",
+ imageUrl = "imageUrl",
+ )
+
+ val expected = Topic(
+ id = "0",
+ name = "Test",
+ shortDescription = "short description",
+ longDescription = "long description",
+ url = "URL",
+ imageUrl = "imageUrl",
+ )
+
+ assertEquals(expected, networkTopic.asExternalModel())
+ }
+
+ @Test
+ fun networkNewsResourceMapsToExternalModel() {
+ val networkNewsResource = NetworkNewsResource(
+ id = "0",
+ title = "title",
+ content = "content",
+ url = "url",
+ headerImageUrl = "headerImageUrl",
+ publishDate = Instant.fromEpochMilliseconds(1),
+ type = "Article 📚",
+ topics = listOf("1", "2"),
+ )
+
+ val networkTopics = listOf(
+ NetworkTopic(
+ id = "1",
+ name = "Test 1",
+ shortDescription = "short description 1",
+ longDescription = "long description 1",
+ url = "url 1",
+ imageUrl = "imageUrl 1",
+ ),
+ NetworkTopic(
+ id = "2",
+ name = "Test 2",
+ shortDescription = "short description 2",
+ longDescription = "long description 2",
+ url = "url 2",
+ imageUrl = "imageUrl 2",
+ ),
+ )
+
+ val expected = NewsResource(
+ id = "0",
+ title = "title",
+ content = "content",
+ url = "url",
+ headerImageUrl = "headerImageUrl",
+ publishDate = Instant.fromEpochMilliseconds(1),
+ type = "Article 📚",
+ topics = networkTopics.map(NetworkTopic::asExternalModel),
+ )
+ assertEquals(expected, networkNewsResource.asExternalModel(networkTopics))
+ }
+}
diff --git a/core/network/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/network/model/NetworkNewsResource.kt b/core/network/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/network/model/NetworkNewsResource.kt
index 89af19c99..7b66af796 100644
--- a/core/network/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/network/model/NetworkNewsResource.kt
+++ b/core/network/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/network/model/NetworkNewsResource.kt
@@ -34,18 +34,3 @@ data class NetworkNewsResource(
val type: String,
val topics: List = listOf(),
)
-
-/**
- * Network representation of [NewsResource] when fetched from /newsresources/{id}
- */
-@Serializable
-data class NetworkNewsResourceExpanded(
- val id: String,
- val title: String,
- val content: String,
- val url: String,
- val headerImageUrl: String,
- val publishDate: Instant,
- val type: String,
- val topics: List = listOf(),
-)
diff --git a/core/network/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/network/model/NetworkTopic.kt b/core/network/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/network/model/NetworkTopic.kt
index e1043938f..0d21c09e7 100644
--- a/core/network/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/network/model/NetworkTopic.kt
+++ b/core/network/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/network/model/NetworkTopic.kt
@@ -32,3 +32,13 @@ data class NetworkTopic(
val imageUrl: String = "",
val followed: Boolean = false,
)
+
+fun NetworkTopic.asExternalModel(): Topic =
+ Topic(
+ id = id,
+ name = name,
+ shortDescription = shortDescription,
+ longDescription = longDescription,
+ url = url,
+ imageUrl = imageUrl,
+ )
diff --git a/core/ui/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt b/core/ui/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt
index 5e2b25abd..9f7fd176e 100644
--- a/core/ui/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt
+++ b/core/ui/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt
@@ -49,6 +49,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalInspectionMode
+import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.onClick
import androidx.compose.ui.semantics.semantics
@@ -114,9 +115,11 @@ fun NewsResourceCardExpanded(
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
// Use custom label for accessibility services to communicate button's action to user.
// Pass null for action to only override the label and not the actual action.
- modifier = modifier.semantics {
- onClick(label = clickActionLabel, action = null)
- },
+ modifier = modifier
+ .semantics {
+ onClick(label = clickActionLabel, action = null)
+ }
+ .testTag("newsResourceCard:${userNewsResource.id}"),
) {
Column {
if (!userNewsResource.headerImageUrl.isNullOrEmpty()) {
@@ -334,9 +337,11 @@ fun NewsResourceTopics(
}
Text(
text = followableTopic.topic.name.uppercase(),
- modifier = Modifier.semantics {
- this.contentDescription = contentDescription
- },
+ modifier = Modifier
+ .semantics {
+ this.contentDescription = contentDescription
+ }
+ .testTag("topicTag:${followableTopic.topic.id}"),
)
},
)
diff --git a/feature/foryou/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/navigation/ForYouNavigation.kt b/feature/foryou/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/navigation/ForYouNavigation.kt
index 8898c471a..f78911397 100644
--- a/feature/foryou/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/navigation/ForYouNavigation.kt
+++ b/feature/foryou/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/navigation/ForYouNavigation.kt
@@ -20,32 +20,47 @@ import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.compose.composable
+import androidx.navigation.compose.navigation
import androidx.navigation.navDeepLink
import com.google.samples.apps.nowinandroid.core.notifications.DEEP_LINK_URI_PATTERN
import com.google.samples.apps.nowinandroid.feature.foryou.ForYouScreen
import kotlinx.serialization.Serializable
-@Serializable
-data object ForYouRoute
+@Serializable data object ForYouRoute // route to ForYou screen
+
+@Serializable data object ForYouBaseRoute // route to base navigation graph
fun NavController.navigateToForYou(navOptions: NavOptions) =
navigate(route = ForYouRoute, navOptions)
-fun NavGraphBuilder.forYouScreen(onTopicClick: (String) -> Unit) {
- composable(
- deepLinks = listOf(
- navDeepLink {
- /**
- * This destination has a deep link that enables a specific news resource to be
- * opened from a notification (@see SystemTrayNotifier for more). The news resource
- * ID is sent in the URI rather than being modelled in the route type because it's
- * transient data (stored in SavedStateHandle) that is cleared after the user has
- * opened the news resource.
- */
- uriPattern = DEEP_LINK_URI_PATTERN
- },
- ),
- ) {
- ForYouScreen(onTopicClick)
+/**
+ * The ForYou section of the app. It can also display information about topics.
+ * This should be supplied from a separate module.
+ *
+ * @param onTopicClick - Called when a topic is clicked, contains the ID of the topic
+ * @param topicDestination - Destination for topic content
+ */
+fun NavGraphBuilder.forYouSection(
+ onTopicClick: (String) -> Unit,
+ topicDestination: NavGraphBuilder.() -> Unit,
+) {
+ navigation(startDestination = ForYouRoute) {
+ composable(
+ deepLinks = listOf(
+ navDeepLink {
+ /**
+ * This destination has a deep link that enables a specific news resource to be
+ * opened from a notification (@see SystemTrayNotifier for more). The news resource
+ * ID is sent in the URI rather than being modelled in the route type because it's
+ * transient data (stored in SavedStateHandle) that is cleared after the user has
+ * opened the news resource.
+ */
+ uriPattern = DEEP_LINK_URI_PATTERN
+ },
+ ),
+ ) {
+ ForYouScreen(onTopicClick)
+ }
+ topicDestination()
}
}
diff --git a/feature/topic/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt b/feature/topic/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt
index be8808977..2caa41967 100644
--- a/feature/topic/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt
+++ b/feature/topic/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt
@@ -74,7 +74,7 @@ import org.jetbrains.compose.ui.tooling.preview.PreviewParameter
import org.koin.compose.viewmodel.koinViewModel
@Composable
-internal fun TopicScreen(
+fun TopicScreen(
showBackButton: Boolean,
onBackClick: () -> Unit,
onTopicClick: (String) -> Unit,
diff --git a/lint/build.gradle.kts b/lint/build.gradle.kts
index cfda9d64d..99a057362 100644
--- a/lint/build.gradle.kts
+++ b/lint/build.gradle.kts
@@ -15,7 +15,6 @@
*/
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
`java-library`
diff --git a/settings.gradle.kts b/settings.gradle.kts
index c7c765d80..b11358e53 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -65,3 +65,11 @@ include(":lint")
include(":shared")
include(":sync:work")
include(":sync:sync-test")
+
+check(JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_17)) {
+ """
+ Now in Android requires JDK 17+ but it is currently using JDK ${JavaVersion.current()}.
+ Java Home: [${System.getProperty("java.home")}]
+ https://developer.android.com/build/jdks#jdk-config-in-studio
+ """.trimIndent()
+}