Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .idea/codeStyles/Project.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,14 @@ android {
dependencies {
implementation(projects.feature.interests)
implementation(projects.feature.foryou)
implementation(projects.feature.bookmarks)
implementation(projects.feature.bookmarks.api)
implementation(projects.feature.bookmarks.impl)
implementation(projects.feature.topic)
implementation(projects.feature.search)
implementation(projects.feature.settings)

implementation(projects.core.common)
implementation(projects.core.navigation)
implementation(projects.core.ui)
implementation(projects.core.designsystem)
implementation(projects.core.data)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ import org.junit.Before
import org.junit.Rule
import org.junit.Test
import javax.inject.Inject
import com.google.samples.apps.nowinandroid.feature.bookmarks.R as BookmarksR
import com.google.samples.apps.nowinandroid.feature.bookmarks.impl.R as BookmarksR
import com.google.samples.apps.nowinandroid.feature.foryou.R as FeatureForyouR
import com.google.samples.apps.nowinandroid.feature.search.R as FeatureSearchR
import com.google.samples.apps.nowinandroid.feature.settings.R as SettingsR
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,20 @@ package com.google.samples.apps.nowinandroid.navigation
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 androidx.navigation.compose.composable
import androidx.navigation.compose.navigation
import androidx.navigation3.runtime.NavEntry
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.ui.NavDisplay
import com.google.samples.apps.nowinandroid.feature.bookmarks.api.navigation.BookmarksRoute
import com.google.samples.apps.nowinandroid.feature.bookmarks.impl.navigation.bookmarksScreen
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.foryou.navigation.forYouSection
import com.google.samples.apps.nowinandroid.feature.interests.navigation.navigateToInterests
import com.google.samples.apps.nowinandroid.feature.interests.navigation.InterestsRoute
import com.google.samples.apps.nowinandroid.feature.search.navigation.SearchRoute
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.TopicRoute
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
Expand All @@ -44,29 +52,57 @@ fun NiaNavHost(
modifier: Modifier = Modifier,
) {
val navController = appState.navController
NavHost(
navController = navController,
startDestination = ForYouBaseRoute,
modifier = modifier,
) {
forYouSection(
onTopicClick = navController::navigateToTopic,
val nav3Navigator = appState.nav3Navigator
NavDisplay(
backStack = nav3Navigator.backStack,
onBack = { nav3Navigator.goBack() },
entryProvider = entryProvider(
fallback = { key ->
println("$key not found, using fallback entry")
NavEntry(key = key) {
NavHost(
navController = navController,
startDestination = ForYouBaseRoute,
modifier = modifier,
) {
navigation<ForYouBaseRoute>(startDestination = ForYouRoute) {
composable<ForYouRoute> {}
composable<TopicRoute> {}
}
composable<ForYouRoute> {}
composable<BookmarksRoute> {}
composable<SearchRoute> {}
composable<InterestsRoute>{}
}
}
},
) {
forYouSection(
onTopicClick = {
nav3Navigator.goTo(route = TopicRoute(it))
},
)
bookmarksScreen(
onTopicClick = { it: String ->
nav3Navigator.goTo(route = InterestsRoute(it))
},
onShowSnackbar = onShowSnackbar
)
topicScreen(
showBackButton = true,
onBackClick = { nav3Navigator.goBack() },
onTopicClick = {
nav3Navigator.goTo(route = TopicRoute(it))
},
)
searchScreen(
onBackClick = navController::popBackStack,
onTopicClick = navController::navigateToTopic,
onInterestsClick = { appState.navigateToTopLevelDestination(INTERESTS) },
onTopicClick = {
nav3Navigator.goTo(route = InterestsRoute(it))
},
)
}
bookmarksScreen(
onTopicClick = navController::navigateToInterests,
onShowSnackbar = onShowSnackbar,
)
searchScreen(
onBackClick = navController::popBackStack,
onInterestsClick = { appState.navigateToTopLevelDestination(INTERESTS) },
onTopicClick = navController::navigateToInterests,
)
interestsListDetailScreen()
}
interestsListDetailScreen()
},
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ import androidx.annotation.StringRes
import androidx.compose.ui.graphics.vector.ImageVector
import com.google.samples.apps.nowinandroid.R
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.bookmarks.api.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 kotlin.reflect.KClass
import com.google.samples.apps.nowinandroid.feature.bookmarks.R as bookmarksR
import com.google.samples.apps.nowinandroid.feature.bookmarks.impl.R as bookmarksR
import com.google.samples.apps.nowinandroid.feature.foryou.R as forYouR
import com.google.samples.apps.nowinandroid.feature.search.R as searchR

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,9 @@ internal fun NiaApp(
) {
val unreadDestinations by appState.topLevelDestinationsWithUnreadResources
.collectAsStateWithLifecycle()

// TODO: This is only used to determine whether the bottom nav item is selected
// this can be improved by just getting
val currentDestination = appState.currentDestination

if (showSettingsDialog) {
Expand All @@ -144,6 +147,10 @@ internal fun NiaApp(
)
}

// TODO: figure out how have the existing navigation bar support both old and new routes
// essentially we need a migration path from what's below to the common UI recipe
// this probably comes down to how the back stack is modelled.

NiaNavigationSuiteScaffold(
navigationSuiteItems = {
appState.topLevelDestinations.forEach { destination ->
Expand All @@ -167,9 +174,9 @@ internal fun NiaApp(
},
label = { Text(stringResource(destination.iconTextId)) },
modifier =
Modifier
.testTag("NiaNavItem")
.then(if (hasUnread) Modifier.notificationDot() else Modifier),
Modifier
.testTag("NiaNavItem")
.then(if (hasUnread) Modifier.notificationDot() else Modifier),
)
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,35 +19,44 @@ package com.google.samples.apps.nowinandroid.ui
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.navigation.NavController
import androidx.navigation.NavDestination
import androidx.navigation.NavDestination.Companion.hasRoute
import androidx.navigation.NavGraph
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavHostController
import androidx.navigation.NavOptions
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navOptions
import androidx.navigation.toRoute
import androidx.tracing.trace
import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor
import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneMonitor
import com.google.samples.apps.nowinandroid.core.ui.TrackDisposableJank
import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.navigateToBookmarks
import com.google.samples.apps.nowinandroid.feature.foryou.navigation.navigateToForYou
import com.google.samples.apps.nowinandroid.feature.interests.navigation.navigateToInterests
import com.google.samples.apps.nowinandroid.feature.bookmarks.api.navigation.BookmarksRoute
import com.google.samples.apps.nowinandroid.feature.foryou.navigation.ForYouRoute
import com.google.samples.apps.nowinandroid.feature.interests.navigation.InterestsRoute
import com.google.samples.apps.nowinandroid.feature.search.navigation.SearchRoute
import com.google.samples.apps.nowinandroid.feature.search.navigation.navigateToSearch
import com.google.samples.apps.nowinandroid.feature.topic.navigation.TopicRoute
import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination
import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination.BOOKMARKS
import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination.FOR_YOU
import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination.INTERESTS
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.datetime.TimeZone

@Composable
Expand All @@ -59,6 +68,11 @@ fun rememberNiaAppState(
navController: NavHostController = rememberNavController(),
): NiaAppState {
NavigationTrackingSideEffect(navController)

val nav3Navigator = remember(navController) {
Nav3NavigatorSimple(navController)
}

return remember(
navController,
coroutineScope,
Expand All @@ -68,6 +82,7 @@ fun rememberNiaAppState(
) {
NiaAppState(
navController = navController,
nav3Navigator = nav3Navigator,
coroutineScope = coroutineScope,
networkMonitor = networkMonitor,
userNewsResourceRepository = userNewsResourceRepository,
Expand All @@ -79,6 +94,7 @@ fun rememberNiaAppState(
@Stable
class NiaAppState(
val navController: NavHostController,
val nav3Navigator: Nav3NavigatorSimple,
coroutineScope: CoroutineScope,
networkMonitor: NetworkMonitor,
userNewsResourceRepository: UserNewsResourceRepository,
Expand Down Expand Up @@ -169,9 +185,11 @@ class NiaAppState(
}

when (topLevelDestination) {
FOR_YOU -> navController.navigateToForYou(topLevelNavOptions)
BOOKMARKS -> navController.navigateToBookmarks(topLevelNavOptions)
INTERESTS -> navController.navigateToInterests(null, topLevelNavOptions)
FOR_YOU -> nav3Navigator.goTo(route = ForYouRoute, topLevelNavOptions)
BOOKMARKS -> nav3Navigator.goTo(route = BookmarksRoute, topLevelNavOptions)
INTERESTS -> {
nav3Navigator.goTo(route = InterestsRoute(null), topLevelNavOptions)
}
}
}
}
Expand All @@ -196,3 +214,107 @@ private fun NavigationTrackingSideEffect(navController: NavHostController) {
}
}
}

class Nav3NavigatorSimple(val navController: NavHostController){

val coroutineScope = CoroutineScope(Job() + Dispatchers.Main)

// We need a single element to avoid "backStack cannot be empty" error b/430023647
val backStack = mutableStateListOf<Any>(Unit)

// Expose the current top level route for consumers
lateinit var topLevelKey : String

// key = top level route string, value = route instance
private var topLevelStacks : LinkedHashMap<String, MutableList<Any>> = linkedMapOf()

init {
coroutineScope.launch {
navController.currentBackStack.collect { nav2BackStack ->

topLevelStacks.clear()

val rootNode : NavGraph = navController.graph
println("Root node start destination: ${rootNode.startDestinationRoute}")

nav2BackStack.forEach { entry ->

println("\n==ENTRY==")
println("Entry: $entry")
println("Destination: ${entry.destination}")
println("Destination navigatorName: ${entry.destination.navigatorName}")
println("Route: ${entry.destination.route}")
println("Parent destination: ${entry.destination.parent}")

val destination = entry.destination

// We only care about navigable destinations
if (destination.navigatorName == "composable"){

val parentDestination = destination.parent
var stackName = destination.route!!

// Convert the entry into a route instance
val routeInstance =
if (destination.hasRoute<BookmarksRoute>()) {
entry.toRoute<BookmarksRoute>()
} else if (destination.hasRoute<TopicRoute>()) {
entry.toRoute<TopicRoute>()
} else if (destination.hasRoute<SearchRoute>()) {
entry.toRoute<SearchRoute>()
} else if (destination.hasRoute<InterestsRoute>()) {
entry.toRoute<InterestsRoute>()
} else if (destination.hasRoute<ForYouRoute>()) {
entry.toRoute<ForYouRoute>()
} else {
// Non migrated top level route
println("Non migrated destination: $destination")
entry
}

// For nested stacks, use the parent destination's start destination route
// as the stack name.
if (parentDestination != rootNode){
stackName = parentDestination?.startDestinationRoute!!
}
add(stackName, routeInstance)
}
}
updateBackStack()
}
}
}

private fun add(stackName: String, route: Any){
if (topLevelStacks[stackName] == null){
topLevelStacks.put(stackName, mutableListOf(route))
} else {
topLevelStacks[stackName]?.add(route)
}
topLevelKey = stackName
updateBackStack()
}

private fun updateBackStack() {
backStack.apply {
clear()
addAll(topLevelStacks.flatMap { it.value })
}
println("Back stack: $backStack")
}

fun goBack(){
val removedKey = topLevelStacks[topLevelKey]?.removeLastOrNull()
// If the removed key was a top level key, remove the associated top level stack
topLevelStacks.remove(removedKey)
topLevelKey = topLevelStacks.keys.last()
updateBackStack()
navController.popBackStack()
}

fun goTo(route: Any, navOptions: NavOptions? = null){
add(topLevelKey, route)
navController.navigate(route = route, navOptions = navOptions)
}
}

Loading