@ -1,72 +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.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 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
|
||||
|
||||
/**
|
||||
* Top-level navigation graph. Navigation is organized as explained at
|
||||
* https://d.android.com/jetpack/compose/nav-adaptive
|
||||
*
|
||||
* The navigation graph defined in this file defines the different top level routes. Navigation
|
||||
* within each route is handled using state and Back Handlers.
|
||||
*/
|
||||
@Composable
|
||||
fun NiaNavHost(
|
||||
appState: NiaAppState,
|
||||
onShowSnackbar: suspend (String, String?) -> Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val navController = appState.navController
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = ForYouBaseRoute,
|
||||
modifier = modifier,
|
||||
) {
|
||||
forYouSection(
|
||||
onTopicClick = navController::navigateToTopic,
|
||||
) {
|
||||
topicScreen(
|
||||
showBackButton = true,
|
||||
onBackClick = navController::popBackStack,
|
||||
onTopicClick = navController::navigateToTopic,
|
||||
)
|
||||
}
|
||||
bookmarksScreen(
|
||||
onTopicClick = navController::navigateToInterests,
|
||||
onShowSnackbar = onShowSnackbar,
|
||||
)
|
||||
searchScreen(
|
||||
onBackClick = navController::popBackStack,
|
||||
onInterestsClick = { appState.navigateToTopLevelDestination(INTERESTS) },
|
||||
onTopicClick = navController::navigateToInterests,
|
||||
)
|
||||
interestsListDetailScreen()
|
||||
}
|
||||
}
|
||||
@ -1,76 +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.navigation
|
||||
|
||||
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.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.foryou.R as forYouR
|
||||
import com.google.samples.apps.nowinandroid.feature.search.R as searchR
|
||||
|
||||
/**
|
||||
* 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,
|
||||
val unselectedIcon: ImageVector,
|
||||
@StringRes val iconTextId: Int,
|
||||
@StringRes val titleTextId: Int,
|
||||
val route: KClass<*>,
|
||||
val baseRoute: KClass<*> = route,
|
||||
) {
|
||||
FOR_YOU(
|
||||
selectedIcon = NiaIcons.Upcoming,
|
||||
unselectedIcon = NiaIcons.UpcomingBorder,
|
||||
iconTextId = forYouR.string.feature_foryou_title,
|
||||
titleTextId = R.string.app_name,
|
||||
route = ForYouRoute::class,
|
||||
baseRoute = ForYouBaseRoute::class,
|
||||
),
|
||||
BOOKMARKS(
|
||||
selectedIcon = NiaIcons.Bookmarks,
|
||||
unselectedIcon = NiaIcons.BookmarksBorder,
|
||||
iconTextId = bookmarksR.string.feature_bookmarks_title,
|
||||
titleTextId = bookmarksR.string.feature_bookmarks_title,
|
||||
route = BookmarksRoute::class,
|
||||
),
|
||||
INTERESTS(
|
||||
selectedIcon = NiaIcons.Grid3x3,
|
||||
unselectedIcon = NiaIcons.Grid3x3,
|
||||
iconTextId = searchR.string.feature_search_interests,
|
||||
titleTextId = searchR.string.feature_search_interests,
|
||||
route = InterestsRoute::class,
|
||||
),
|
||||
}
|
||||
@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2025 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.navigation
|
||||
|
||||
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.api.navigation.BookmarksNavKey
|
||||
import com.google.samples.apps.nowinandroid.feature.foryou.api.navigation.ForYouNavKey
|
||||
import com.google.samples.apps.nowinandroid.feature.interests.api.navigation.InterestsNavKey
|
||||
import com.google.samples.apps.nowinandroid.feature.bookmarks.api.R as bookmarksR
|
||||
import com.google.samples.apps.nowinandroid.feature.foryou.api.R as forYouR
|
||||
import com.google.samples.apps.nowinandroid.feature.search.api.R as searchR
|
||||
|
||||
/**
|
||||
* Type for the top level navigation items in the application. Contains UI information about the
|
||||
* current route 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.
|
||||
*/
|
||||
data class TopLevelNavItem(
|
||||
val selectedIcon: ImageVector,
|
||||
val unselectedIcon: ImageVector,
|
||||
@StringRes val iconTextId: Int,
|
||||
@StringRes val titleTextId: Int,
|
||||
)
|
||||
|
||||
val FOR_YOU = TopLevelNavItem(
|
||||
selectedIcon = NiaIcons.Upcoming,
|
||||
unselectedIcon = NiaIcons.UpcomingBorder,
|
||||
iconTextId = forYouR.string.feature_foryou_api_title,
|
||||
titleTextId = R.string.app_name,
|
||||
)
|
||||
|
||||
val BOOKMARKS = TopLevelNavItem(
|
||||
selectedIcon = NiaIcons.Bookmarks,
|
||||
unselectedIcon = NiaIcons.BookmarksBorder,
|
||||
iconTextId = bookmarksR.string.feature_bookmarks_api_title,
|
||||
titleTextId = bookmarksR.string.feature_bookmarks_api_title,
|
||||
)
|
||||
|
||||
val INTERESTS = TopLevelNavItem(
|
||||
selectedIcon = NiaIcons.Grid3x3,
|
||||
unselectedIcon = NiaIcons.Grid3x3,
|
||||
iconTextId = searchR.string.feature_search_api_interests,
|
||||
titleTextId = searchR.string.feature_search_api_interests,
|
||||
)
|
||||
|
||||
val TOP_LEVEL_NAV_ITEMS = mapOf(
|
||||
ForYouNavKey to FOR_YOU,
|
||||
BookmarksNavKey to BOOKMARKS,
|
||||
InterestsNavKey(null) to INTERESTS,
|
||||
)
|
||||
@ -1,43 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 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.ui.interests2pane
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.navigation.toRoute
|
||||
import com.google.samples.apps.nowinandroid.feature.interests.navigation.InterestsRoute
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import javax.inject.Inject
|
||||
|
||||
const val TOPIC_ID_KEY = "selectedTopicId"
|
||||
|
||||
@HiltViewModel
|
||||
class Interests2PaneViewModel @Inject constructor(
|
||||
private val savedStateHandle: SavedStateHandle,
|
||||
) : ViewModel() {
|
||||
|
||||
val route = savedStateHandle.toRoute<InterestsRoute>()
|
||||
val selectedTopicId: StateFlow<String?> = savedStateHandle.getStateFlow(
|
||||
key = TOPIC_ID_KEY,
|
||||
initialValue = route.initialTopicId,
|
||||
)
|
||||
|
||||
fun onTopicClick(topicId: String?) {
|
||||
savedStateHandle[TOPIC_ID_KEY] = topicId
|
||||
}
|
||||
}
|
||||
@ -1,244 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 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.ui.interests2pane
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.material3.LocalMinimumInteractiveComponentSize
|
||||
import androidx.compose.material3.VerticalDragHandle
|
||||
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
|
||||
import androidx.compose.material3.adaptive.WindowAdaptiveInfo
|
||||
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
|
||||
import androidx.compose.material3.adaptive.layout.AnimatedPane
|
||||
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole
|
||||
import androidx.compose.material3.adaptive.layout.PaneAdaptedValue
|
||||
import androidx.compose.material3.adaptive.layout.PaneExpansionAnchor
|
||||
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldDestinationItem
|
||||
import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirective
|
||||
import androidx.compose.material3.adaptive.layout.defaultDragHandleSemantics
|
||||
import androidx.compose.material3.adaptive.layout.rememberPaneExpansionState
|
||||
import androidx.compose.material3.adaptive.navigation.BackNavigationBehavior
|
||||
import androidx.compose.material3.adaptive.navigation.NavigableListDetailPaneScaffold
|
||||
import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator
|
||||
import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldPredictiveBackHandler
|
||||
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clipToBounds
|
||||
import androidx.compose.ui.layout.layout
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
import com.google.samples.apps.nowinandroid.feature.interests.InterestsRoute
|
||||
import com.google.samples.apps.nowinandroid.feature.interests.navigation.InterestsRoute
|
||||
import com.google.samples.apps.nowinandroid.feature.topic.TopicDetailPlaceholder
|
||||
import com.google.samples.apps.nowinandroid.feature.topic.TopicScreen
|
||||
import com.google.samples.apps.nowinandroid.feature.topic.TopicViewModel
|
||||
import com.google.samples.apps.nowinandroid.feature.topic.navigation.TopicRoute
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlin.math.max
|
||||
|
||||
@Serializable internal object TopicPlaceholderRoute
|
||||
|
||||
fun NavGraphBuilder.interestsListDetailScreen() {
|
||||
composable<InterestsRoute> {
|
||||
InterestsListDetailScreen()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun InterestsListDetailScreen(
|
||||
viewModel: Interests2PaneViewModel = hiltViewModel(),
|
||||
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo(),
|
||||
) {
|
||||
val selectedTopicId by viewModel.selectedTopicId.collectAsStateWithLifecycle()
|
||||
InterestsListDetailScreen(
|
||||
selectedTopicId = selectedTopicId,
|
||||
onTopicClick = viewModel::onTopicClick,
|
||||
windowAdaptiveInfo = windowAdaptiveInfo,
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
|
||||
@Composable
|
||||
internal fun InterestsListDetailScreen(
|
||||
selectedTopicId: String?,
|
||||
onTopicClick: (String) -> Unit,
|
||||
windowAdaptiveInfo: WindowAdaptiveInfo,
|
||||
) {
|
||||
val listDetailNavigator = rememberListDetailPaneScaffoldNavigator(
|
||||
scaffoldDirective = calculatePaneScaffoldDirective(windowAdaptiveInfo),
|
||||
initialDestinationHistory = listOfNotNull(
|
||||
ThreePaneScaffoldDestinationItem(ListDetailPaneScaffoldRole.List),
|
||||
ThreePaneScaffoldDestinationItem<Nothing>(ListDetailPaneScaffoldRole.Detail).takeIf {
|
||||
selectedTopicId != null
|
||||
},
|
||||
),
|
||||
)
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
val paneExpansionState = rememberPaneExpansionState(
|
||||
anchors = listOf(
|
||||
PaneExpansionAnchor.Proportion(0f),
|
||||
PaneExpansionAnchor.Proportion(0.5f),
|
||||
PaneExpansionAnchor.Proportion(1f),
|
||||
),
|
||||
)
|
||||
|
||||
ThreePaneScaffoldPredictiveBackHandler(
|
||||
listDetailNavigator,
|
||||
BackNavigationBehavior.PopUntilScaffoldValueChange,
|
||||
)
|
||||
BackHandler(
|
||||
paneExpansionState.currentAnchor == PaneExpansionAnchor.Proportion(0f) &&
|
||||
listDetailNavigator.isListPaneVisible() &&
|
||||
listDetailNavigator.isDetailPaneVisible(),
|
||||
) {
|
||||
coroutineScope.launch {
|
||||
paneExpansionState.animateTo(PaneExpansionAnchor.Proportion(1f))
|
||||
}
|
||||
}
|
||||
|
||||
var topicRoute by remember {
|
||||
val route = selectedTopicId?.let { TopicRoute(id = it) } ?: TopicPlaceholderRoute
|
||||
mutableStateOf(route)
|
||||
}
|
||||
|
||||
fun onTopicClickShowDetailPane(topicId: String) {
|
||||
onTopicClick(topicId)
|
||||
topicRoute = TopicRoute(id = topicId)
|
||||
coroutineScope.launch {
|
||||
listDetailNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail)
|
||||
}
|
||||
if (paneExpansionState.currentAnchor == PaneExpansionAnchor.Proportion(1f)) {
|
||||
coroutineScope.launch {
|
||||
paneExpansionState.animateTo(PaneExpansionAnchor.Proportion(0f))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val mutableInteractionSource = remember { MutableInteractionSource() }
|
||||
val minPaneWidth = 300.dp
|
||||
|
||||
NavigableListDetailPaneScaffold(
|
||||
navigator = listDetailNavigator,
|
||||
listPane = {
|
||||
AnimatedPane {
|
||||
Box(
|
||||
modifier = Modifier.clipToBounds()
|
||||
.layout { measurable, constraints ->
|
||||
val width = max(minPaneWidth.roundToPx(), constraints.maxWidth)
|
||||
val placeable = measurable.measure(
|
||||
constraints.copy(
|
||||
minWidth = minPaneWidth.roundToPx(),
|
||||
maxWidth = width,
|
||||
),
|
||||
)
|
||||
layout(constraints.maxWidth, placeable.height) {
|
||||
placeable.placeRelative(
|
||||
x = 0,
|
||||
y = 0,
|
||||
)
|
||||
}
|
||||
},
|
||||
) {
|
||||
InterestsRoute(
|
||||
onTopicClick = ::onTopicClickShowDetailPane,
|
||||
shouldHighlightSelectedTopic = listDetailNavigator.isDetailPaneVisible(),
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
detailPane = {
|
||||
AnimatedPane {
|
||||
Box(
|
||||
modifier = Modifier.clipToBounds()
|
||||
.layout { measurable, constraints ->
|
||||
val width = max(minPaneWidth.roundToPx(), constraints.maxWidth)
|
||||
val placeable = measurable.measure(
|
||||
constraints.copy(
|
||||
minWidth = minPaneWidth.roundToPx(),
|
||||
maxWidth = width,
|
||||
),
|
||||
)
|
||||
layout(constraints.maxWidth, placeable.height) {
|
||||
placeable.placeRelative(
|
||||
x = constraints.maxWidth -
|
||||
max(constraints.maxWidth, placeable.width),
|
||||
y = 0,
|
||||
)
|
||||
}
|
||||
},
|
||||
) {
|
||||
AnimatedContent(topicRoute) { route ->
|
||||
when (route) {
|
||||
is TopicRoute -> {
|
||||
TopicScreen(
|
||||
showBackButton = !listDetailNavigator.isListPaneVisible(),
|
||||
onBackClick = {
|
||||
coroutineScope.launch {
|
||||
listDetailNavigator.navigateBack()
|
||||
}
|
||||
},
|
||||
onTopicClick = ::onTopicClickShowDetailPane,
|
||||
viewModel = hiltViewModel<TopicViewModel, TopicViewModel.Factory>(
|
||||
key = route.id,
|
||||
) { factory ->
|
||||
factory.create(route.id)
|
||||
},
|
||||
)
|
||||
}
|
||||
is TopicPlaceholderRoute -> {
|
||||
TopicDetailPlaceholder()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
paneExpansionState = paneExpansionState,
|
||||
paneExpansionDragHandle = {
|
||||
VerticalDragHandle(
|
||||
modifier = Modifier.paneExpansionDraggable(
|
||||
state = paneExpansionState,
|
||||
minTouchTargetSize = LocalMinimumInteractiveComponentSize.current,
|
||||
interactionSource = mutableInteractionSource,
|
||||
semanticsProperties = paneExpansionState.defaultDragHandleSemantics(),
|
||||
),
|
||||
interactionSource = mutableInteractionSource,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
|
||||
private fun <T> ThreePaneScaffoldNavigator<T>.isListPaneVisible(): Boolean =
|
||||
scaffoldValue[ListDetailPaneScaffoldRole.List] == PaneAdaptedValue.Expanded
|
||||
|
||||
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
|
||||
private fun <T> ThreePaneScaffoldNavigator<T>.isDetailPaneVisible(): Boolean =
|
||||
scaffoldValue[ListDetailPaneScaffoldRole.Detail] == PaneAdaptedValue.Expanded
|
||||
@ -0,0 +1,17 @@
|
||||
#
|
||||
# Copyright 2025 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.
|
||||
#
|
||||
|
||||
sdk = 35
|
||||
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 276 KiB After Width: | Height: | Size: 276 KiB |
|
Before Width: | Height: | Size: 141 KiB After Width: | Height: | Size: 141 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 198 KiB After Width: | Height: | Size: 198 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 185 KiB After Width: | Height: | Size: 185 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 216 KiB After Width: | Height: | Size: 216 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 97 KiB |
@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import com.google.samples.apps.nowinandroid.libs
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.apply
|
||||
import org.gradle.kotlin.dsl.dependencies
|
||||
|
||||
class AndroidFeatureApiConventionPlugin : Plugin<Project> {
|
||||
override fun apply(target: Project) {
|
||||
with(target) {
|
||||
apply(plugin = "nowinandroid.android.library")
|
||||
apply(plugin = "org.jetbrains.kotlin.plugin.serialization")
|
||||
|
||||
dependencies {
|
||||
"api"(project(":core:navigation"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
#
|
||||
# Copyright 2025 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.
|
||||
#
|
||||
|
||||
sdk = 35
|
||||
@ -0,0 +1,3 @@
|
||||
# :core:navigation module
|
||||
## Dependency graph
|
||||

|
||||
@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2025 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.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.nowinandroid.android.library)
|
||||
alias(libs.plugins.nowinandroid.hilt)
|
||||
alias(libs.plugins.hilt)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
alias(libs.plugins.compose)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.google.samples.apps.nowinandroid.core.navigation"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(libs.androidx.navigation3.runtime)
|
||||
implementation(libs.androidx.savedstate.compose)
|
||||
implementation(libs.androidx.lifecycle.viewModel.navigation3)
|
||||
|
||||
testImplementation(libs.truth)
|
||||
|
||||
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
|
||||
androidTestImplementation(libs.androidx.test.ext)
|
||||
androidTestImplementation(libs.androidx.compose.ui.testManifest)
|
||||
androidTestImplementation(libs.androidx.lifecycle.viewModel.testing)
|
||||
androidTestImplementation(libs.truth)
|
||||
}
|
||||
@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright 2025 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.navigation
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import androidx.compose.runtime.toMutableStateList
|
||||
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
|
||||
import androidx.navigation3.runtime.NavBackStack
|
||||
import androidx.navigation3.runtime.NavEntry
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import androidx.navigation3.runtime.rememberDecoratedNavEntries
|
||||
import androidx.navigation3.runtime.rememberNavBackStack
|
||||
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
|
||||
|
||||
/**
|
||||
* Create a navigation state that persists config changes and process death.
|
||||
*/
|
||||
@Composable
|
||||
fun rememberNavigationState(
|
||||
startKey: NavKey,
|
||||
topLevelKeys: Set<NavKey>,
|
||||
): NavigationState {
|
||||
val topLevelStack = rememberNavBackStack(startKey)
|
||||
val subStacks = topLevelKeys.associateWith { key -> rememberNavBackStack(key) }
|
||||
|
||||
return remember(startKey, topLevelKeys) {
|
||||
NavigationState(
|
||||
startKey = startKey,
|
||||
topLevelStack = topLevelStack,
|
||||
subStacks = subStacks,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* State holder for navigation state.
|
||||
*
|
||||
* @param startKey - the starting navigation key. The user will exit the app through this key.
|
||||
* @param topLevelStack - the top level back stack. It holds only top level keys.
|
||||
* @param subStacks - the back stacks for each top level key
|
||||
*/
|
||||
class NavigationState(
|
||||
val startKey: NavKey,
|
||||
val topLevelStack: NavBackStack<NavKey>,
|
||||
val subStacks: Map<NavKey, NavBackStack<NavKey>>,
|
||||
) {
|
||||
val currentTopLevelKey: NavKey by derivedStateOf { topLevelStack.last() }
|
||||
|
||||
val topLevelKeys
|
||||
get() = subStacks.keys
|
||||
|
||||
@get:VisibleForTesting
|
||||
val currentSubStack: NavBackStack<NavKey>
|
||||
get() = subStacks[currentTopLevelKey]
|
||||
?: error("Sub stack for $currentTopLevelKey does not exist")
|
||||
|
||||
@get:VisibleForTesting
|
||||
val currentKey: NavKey by derivedStateOf { currentSubStack.last() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert NavigationState into NavEntries.
|
||||
*/
|
||||
@Composable
|
||||
fun NavigationState.toEntries(
|
||||
entryProvider: (NavKey) -> NavEntry<NavKey>,
|
||||
): SnapshotStateList<NavEntry<NavKey>> {
|
||||
val decoratedEntries = subStacks.mapValues { (_, stack) ->
|
||||
val decorators = listOf(
|
||||
rememberSaveableStateHolderNavEntryDecorator<NavKey>(),
|
||||
rememberViewModelStoreNavEntryDecorator<NavKey>(),
|
||||
)
|
||||
rememberDecoratedNavEntries(
|
||||
backStack = stack,
|
||||
entryDecorators = decorators,
|
||||
entryProvider = entryProvider,
|
||||
)
|
||||
}
|
||||
|
||||
return topLevelStack
|
||||
.flatMap { decoratedEntries[it] ?: emptyList() }
|
||||
.toMutableStateList()
|
||||
}
|
||||
@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright 2025 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.navigation
|
||||
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
|
||||
/**
|
||||
* Handles navigation events (forward and back) by updating the navigation state.
|
||||
*
|
||||
* @param state - The navigation state that will be updated in response to navigation events.
|
||||
*/
|
||||
class Navigator(val state: NavigationState) {
|
||||
|
||||
/**
|
||||
* Navigate to a navigation key
|
||||
*
|
||||
* @param key - the navigation key to navigate to.
|
||||
*/
|
||||
fun navigate(key: NavKey) {
|
||||
when (key) {
|
||||
state.currentTopLevelKey -> clearSubStack()
|
||||
in state.topLevelKeys -> goToTopLevel(key)
|
||||
else -> goToKey(key)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Go back to the previous navigation key.
|
||||
*/
|
||||
fun goBack() {
|
||||
when (state.currentKey) {
|
||||
state.startKey -> error("You cannot go back from the start route")
|
||||
state.currentTopLevelKey -> {
|
||||
// We're at the base of the current sub stack, go back to the previous top level
|
||||
// stack.
|
||||
state.topLevelStack.removeLastOrNull()
|
||||
}
|
||||
else -> state.currentSubStack.removeLastOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Go to a non top level key.
|
||||
*/
|
||||
private fun goToKey(key: NavKey) {
|
||||
state.currentSubStack.apply {
|
||||
// Remove it if it's already in the stack so it's added at the end.
|
||||
remove(key)
|
||||
add(key)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Go to a top level stack.
|
||||
*/
|
||||
private fun goToTopLevel(key: NavKey) {
|
||||
state.topLevelStack.apply {
|
||||
if (key == state.startKey) {
|
||||
// This is the start key. Clear the stack so it's added as the only key.
|
||||
clear()
|
||||
} else {
|
||||
// Remove it if it's already in the stack so it's added at the end.
|
||||
remove(key)
|
||||
}
|
||||
add(key)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clearing all but the root key in the current sub stack.
|
||||
*/
|
||||
private fun clearSubStack() {
|
||||
state.currentSubStack.run {
|
||||
if (size > 1) subList(1, size).clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,256 @@
|
||||
/*
|
||||
* Copyright 2025 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.navigation
|
||||
|
||||
import androidx.navigation3.runtime.NavBackStack
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
private object TestFirstTopLevelKey : NavKey
|
||||
private object TestSecondTopLevelKey : NavKey
|
||||
private object TestThirdTopLevelKey : NavKey
|
||||
private object TestKeyFirst : NavKey
|
||||
private object TestKeySecond : NavKey
|
||||
|
||||
class NavigatorTest {
|
||||
|
||||
private lateinit var navigationState: NavigationState
|
||||
private lateinit var navigator: Navigator
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
val startKey = TestFirstTopLevelKey
|
||||
val topLevelStack = NavBackStack<NavKey>(startKey)
|
||||
val topLevelKeys = listOf(
|
||||
startKey,
|
||||
TestSecondTopLevelKey,
|
||||
TestThirdTopLevelKey,
|
||||
)
|
||||
val subStacks = topLevelKeys.associateWith { key -> NavBackStack(key) }
|
||||
|
||||
navigationState = NavigationState(
|
||||
startKey = startKey,
|
||||
topLevelStack = topLevelStack,
|
||||
subStacks = subStacks,
|
||||
)
|
||||
navigator = Navigator(navigationState)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testStartKey() {
|
||||
assertThat(navigationState.startKey).isEqualTo(TestFirstTopLevelKey)
|
||||
assertThat(navigationState.currentTopLevelKey).isEqualTo(TestFirstTopLevelKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNavigate() {
|
||||
navigator.navigate(TestKeyFirst)
|
||||
|
||||
assertThat(navigationState.currentTopLevelKey).isEqualTo(TestFirstTopLevelKey)
|
||||
assertThat(navigationState.subStacks[TestFirstTopLevelKey]?.last()).isEqualTo(TestKeyFirst)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNavigateTopLevel() {
|
||||
navigator.navigate(TestSecondTopLevelKey)
|
||||
assertThat(navigationState.currentTopLevelKey).isEqualTo(TestSecondTopLevelKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNavigateSingleTop() {
|
||||
navigator.navigate(TestKeyFirst)
|
||||
|
||||
assertThat(navigationState.currentSubStack).containsExactly(
|
||||
TestFirstTopLevelKey,
|
||||
TestKeyFirst,
|
||||
).inOrder()
|
||||
|
||||
navigator.navigate(TestKeyFirst)
|
||||
|
||||
assertThat(navigationState.currentSubStack).containsExactly(
|
||||
TestFirstTopLevelKey,
|
||||
TestKeyFirst,
|
||||
).inOrder()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNavigateTopLevelSingleTop() {
|
||||
navigator.navigate(TestSecondTopLevelKey)
|
||||
navigator.navigate(TestKeyFirst)
|
||||
|
||||
assertThat(navigationState.currentSubStack).containsExactly(
|
||||
TestSecondTopLevelKey,
|
||||
TestKeyFirst,
|
||||
).inOrder()
|
||||
|
||||
navigator.navigate(TestSecondTopLevelKey)
|
||||
|
||||
assertThat(navigationState.currentSubStack).containsExactly(
|
||||
TestSecondTopLevelKey,
|
||||
).inOrder()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSubStack() {
|
||||
navigator.navigate(TestKeyFirst)
|
||||
|
||||
assertThat(navigationState.currentKey).isEqualTo(TestKeyFirst)
|
||||
assertThat(navigationState.currentTopLevelKey).isEqualTo(TestFirstTopLevelKey)
|
||||
|
||||
navigator.navigate(TestKeySecond)
|
||||
|
||||
assertThat(navigationState.currentKey).isEqualTo(TestKeySecond)
|
||||
assertThat(navigationState.currentTopLevelKey).isEqualTo(TestFirstTopLevelKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMultiStack() {
|
||||
// add to start stack
|
||||
navigator.navigate(TestKeyFirst)
|
||||
|
||||
assertThat(navigationState.currentKey).isEqualTo(TestKeyFirst)
|
||||
assertThat(navigationState.currentTopLevelKey).isEqualTo(TestFirstTopLevelKey)
|
||||
|
||||
// navigate to new top level
|
||||
navigator.navigate(TestSecondTopLevelKey)
|
||||
|
||||
assertThat(navigationState.currentKey).isEqualTo(TestSecondTopLevelKey)
|
||||
assertThat(navigationState.currentTopLevelKey).isEqualTo(TestSecondTopLevelKey)
|
||||
|
||||
// add to new stack
|
||||
navigator.navigate(TestKeySecond)
|
||||
|
||||
assertThat(navigationState.currentKey).isEqualTo(TestKeySecond)
|
||||
assertThat(navigationState.currentTopLevelKey).isEqualTo(TestSecondTopLevelKey)
|
||||
|
||||
// go back to start stack
|
||||
navigator.navigate(TestFirstTopLevelKey)
|
||||
|
||||
assertThat(navigationState.currentKey).isEqualTo(TestKeyFirst)
|
||||
assertThat(navigationState.currentTopLevelKey).isEqualTo(TestFirstTopLevelKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPopOneNonTopLevel() {
|
||||
navigator.navigate(TestKeyFirst)
|
||||
navigator.navigate(TestKeySecond)
|
||||
|
||||
assertThat(navigationState.currentSubStack).containsExactly(
|
||||
TestFirstTopLevelKey,
|
||||
TestKeyFirst,
|
||||
TestKeySecond,
|
||||
).inOrder()
|
||||
|
||||
navigator.goBack()
|
||||
|
||||
assertThat(navigationState.currentSubStack).containsExactly(
|
||||
TestFirstTopLevelKey,
|
||||
TestKeyFirst,
|
||||
).inOrder()
|
||||
|
||||
assertThat(navigationState.currentKey).isEqualTo(TestKeyFirst)
|
||||
assertThat(navigationState.currentTopLevelKey).isEqualTo(TestFirstTopLevelKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPopOneTopLevel() {
|
||||
navigator.navigate(TestKeyFirst)
|
||||
navigator.navigate(TestSecondTopLevelKey)
|
||||
|
||||
assertThat(navigationState.currentSubStack).containsExactly(
|
||||
TestSecondTopLevelKey,
|
||||
).inOrder()
|
||||
|
||||
assertThat(navigationState.currentKey).isEqualTo(TestSecondTopLevelKey)
|
||||
assertThat(navigationState.currentTopLevelKey).isEqualTo(TestSecondTopLevelKey)
|
||||
|
||||
// remove TopLevel
|
||||
navigator.goBack()
|
||||
|
||||
assertThat(navigationState.currentSubStack).containsExactly(
|
||||
TestFirstTopLevelKey,
|
||||
TestKeyFirst,
|
||||
).inOrder()
|
||||
|
||||
assertThat(navigationState.currentKey).isEqualTo(TestKeyFirst)
|
||||
assertThat(navigationState.currentTopLevelKey).isEqualTo(TestFirstTopLevelKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun popMultipleNonTopLevel() {
|
||||
navigator.navigate(TestKeyFirst)
|
||||
navigator.navigate(TestKeySecond)
|
||||
|
||||
assertThat(navigationState.currentSubStack).containsExactly(
|
||||
TestFirstTopLevelKey,
|
||||
TestKeyFirst,
|
||||
TestKeySecond,
|
||||
).inOrder()
|
||||
|
||||
navigator.goBack()
|
||||
navigator.goBack()
|
||||
|
||||
assertThat(navigationState.currentSubStack).containsExactly(
|
||||
TestFirstTopLevelKey,
|
||||
).inOrder()
|
||||
|
||||
assertThat(navigationState.currentKey).isEqualTo(TestFirstTopLevelKey)
|
||||
assertThat(navigationState.currentTopLevelKey).isEqualTo(TestFirstTopLevelKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun popMultipleTopLevel() {
|
||||
// second sub-stack
|
||||
navigator.navigate(TestSecondTopLevelKey)
|
||||
navigator.navigate(TestKeyFirst)
|
||||
|
||||
assertThat(navigationState.currentSubStack).containsExactly(
|
||||
TestSecondTopLevelKey,
|
||||
TestKeyFirst,
|
||||
).inOrder()
|
||||
|
||||
// third sub-stack
|
||||
navigator.navigate(TestThirdTopLevelKey)
|
||||
navigator.navigate(TestKeySecond)
|
||||
|
||||
assertThat(navigationState.currentSubStack).containsExactly(
|
||||
TestThirdTopLevelKey,
|
||||
TestKeySecond,
|
||||
).inOrder()
|
||||
|
||||
repeat(4) {
|
||||
navigator.goBack()
|
||||
}
|
||||
|
||||
assertThat(navigationState.currentSubStack).containsExactly(
|
||||
TestFirstTopLevelKey,
|
||||
).inOrder()
|
||||
|
||||
assertThat(navigationState.currentKey).isEqualTo(TestFirstTopLevelKey)
|
||||
assertThat(navigationState.currentTopLevelKey).isEqualTo(TestFirstTopLevelKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun throwOnEmptyBackStack() {
|
||||
assertFailsWith<IllegalStateException> {
|
||||
navigator.goBack()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.nowinandroid.android.feature.api)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.google.samples.apps.nowinandroid.feature.bookmarks.api"
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
/*
|
||||
* 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.feature.bookmarks.api.navigation
|
||||
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
object BookmarksNavKey : NavKey
|
||||
@ -0,0 +1,57 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2023 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
|
||||
|
||||
http://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.
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="64dp"
|
||||
android:height="64dp"
|
||||
android:viewportWidth="64"
|
||||
android:viewportHeight="64">
|
||||
<path
|
||||
android:pathData="M16,2H55C57.209,2 59,3.791 59,6V52"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeLineCap="round">
|
||||
<aapt:attr name="android:strokeColor">
|
||||
<gradient
|
||||
android:startX="8"
|
||||
android:startY="8"
|
||||
android:endX="56"
|
||||
android:endY="56"
|
||||
android:type="linear">
|
||||
<item android:offset="0" android:color="#FFFFA8FF"/>
|
||||
<item android:offset="1" android:color="#FFFF8B5E"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:pathData="M45,10H9C6.791,10 5,11.791 5,14V55.854C5,59.177 8.817,61.051 11.446,59.019L24.554,48.89C25.995,47.777 28.005,47.777 29.446,48.89L42.554,59.019C45.183,61.051 49,59.177 49,55.854V14C49,11.791 47.209,10 45,10Z"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeLineCap="round">
|
||||
<aapt:attr name="android:strokeColor">
|
||||
<gradient
|
||||
android:startX="8"
|
||||
android:startY="8"
|
||||
android:endX="56"
|
||||
android:endY="56"
|
||||
android:type="linear">
|
||||
<item android:offset="0" android:color="#FFFFA8FF"/>
|
||||
<item android:offset="1" android:color="#FFFF8B5E"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
</vector>
|
||||
@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2025 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.feature.bookmarks.impl.navigation
|
||||
|
||||
import androidx.compose.material3.SnackbarDuration.Short
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.SnackbarResult.ActionPerformed
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.navigation3.runtime.EntryProviderScope
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import com.google.samples.apps.nowinandroid.core.navigation.Navigator
|
||||
import com.google.samples.apps.nowinandroid.feature.bookmarks.api.navigation.BookmarksNavKey
|
||||
import com.google.samples.apps.nowinandroid.feature.bookmarks.impl.BookmarksScreen
|
||||
import com.google.samples.apps.nowinandroid.feature.topic.api.navigation.navigateToTopic
|
||||
|
||||
fun EntryProviderScope<NavKey>.bookmarksEntry(navigator: Navigator) {
|
||||
entry<BookmarksNavKey> {
|
||||
val snackbarHostState = LocalSnackbarHostState.current
|
||||
BookmarksScreen(
|
||||
onTopicClick = navigator::navigateToTopic,
|
||||
onShowSnackbar = { message, action ->
|
||||
snackbarHostState.showSnackbar(
|
||||
message = message,
|
||||
actionLabel = action,
|
||||
duration = Short,
|
||||
) == ActionPerformed
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Why is this here?
|
||||
val LocalSnackbarHostState = compositionLocalOf<SnackbarHostState> {
|
||||
error("SnackbarHostState state should be initialized at runtime")
|
||||
}
|
||||
@ -1,38 +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.feature.bookmarks.navigation
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavOptions
|
||||
import androidx.navigation.compose.composable
|
||||
import com.google.samples.apps.nowinandroid.feature.bookmarks.BookmarksRoute
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable object BookmarksRoute
|
||||
|
||||
fun NavController.navigateToBookmarks(navOptions: NavOptions) =
|
||||
navigate(route = BookmarksRoute, navOptions)
|
||||
|
||||
fun NavGraphBuilder.bookmarksScreen(
|
||||
onTopicClick: (String) -> Unit,
|
||||
onShowSnackbar: suspend (String, String?) -> Boolean,
|
||||
) {
|
||||
composable<BookmarksRoute> {
|
||||
BookmarksRoute(onTopicClick, onShowSnackbar)
|
||||
}
|
||||
}
|
||||
@ -1,57 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2023 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
|
||||
|
||||
http://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.
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="64dp"
|
||||
android:height="64dp"
|
||||
android:viewportWidth="64"
|
||||
android:viewportHeight="64">
|
||||
<path
|
||||
android:pathData="M16,2H55C57.209,2 59,3.791 59,6V52"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeLineCap="round">
|
||||
<aapt:attr name="android:strokeColor">
|
||||
<gradient
|
||||
android:startX="8"
|
||||
android:startY="8"
|
||||
android:endX="56"
|
||||
android:endY="56"
|
||||
android:type="linear">
|
||||
<item android:offset="0" android:color="#FFFFA8FF"/>
|
||||
<item android:offset="1" android:color="#FFFF8B5E"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:pathData="M45,10H9C6.791,10 5,11.791 5,14V55.854C5,59.177 8.817,61.051 11.446,59.019L24.554,48.89C25.995,47.777 28.005,47.777 29.446,48.89L42.554,59.019C45.183,61.051 49,59.177 49,55.854V14C49,11.791 47.209,10 45,10Z"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeLineCap="round">
|
||||
<aapt:attr name="android:strokeColor">
|
||||
<gradient
|
||||
android:startX="8"
|
||||
android:startY="8"
|
||||
android:endX="56"
|
||||
android:endY="56"
|
||||
android:type="linear">
|
||||
<item android:offset="0" android:color="#FFFFA8FF"/>
|
||||
<item android:offset="1" android:color="#FFFF8B5E"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
</vector>
|
||||
@ -0,0 +1,57 @@
|
||||
# `:feature:foryou:api`
|
||||
|
||||
## Module dependency graph
|
||||
|
||||
<!--region graph-->
|
||||
```mermaid
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
elk:
|
||||
nodePlacementStrategy: SIMPLE
|
||||
---
|
||||
graph TB
|
||||
subgraph :feature
|
||||
direction TB
|
||||
subgraph :feature:foryou
|
||||
direction TB
|
||||
:feature:foryou:api[api]:::android-library
|
||||
end
|
||||
end
|
||||
subgraph :core
|
||||
direction TB
|
||||
:core:navigation[navigation]:::android-library
|
||||
end
|
||||
|
||||
:feature:foryou:api --> :core:navigation
|
||||
|
||||
classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
|
||||
classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000;
|
||||
classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000;
|
||||
classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000;
|
||||
classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000;
|
||||
classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000;
|
||||
```
|
||||
|
||||
<details><summary>📋 Graph legend</summary>
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
application[application]:::android-application
|
||||
feature[feature]:::android-feature
|
||||
library[library]:::android-library
|
||||
jvm[jvm]:::jvm-library
|
||||
|
||||
application -.-> feature
|
||||
library --> jvm
|
||||
|
||||
classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
|
||||
classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000;
|
||||
classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000;
|
||||
classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000;
|
||||
classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000;
|
||||
classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000;
|
||||
```
|
||||
|
||||
</details>
|
||||
<!--endregion-->
|
||||
@ -0,0 +1,27 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.nowinandroid.android.feature.api)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.google.samples.apps.nowinandroid.feature.foryou.api"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(projects.core.navigation)
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
/*
|
||||
* 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.feature.foryou.api.navigation
|
||||
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
object ForYouNavKey : NavKey
|
||||
@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2025 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.feature.foryou.impl.navigation
|
||||
|
||||
import androidx.navigation3.runtime.EntryProviderScope
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import com.google.samples.apps.nowinandroid.core.navigation.Navigator
|
||||
import com.google.samples.apps.nowinandroid.feature.foryou.api.navigation.ForYouNavKey
|
||||
import com.google.samples.apps.nowinandroid.feature.foryou.impl.ForYouScreen
|
||||
import com.google.samples.apps.nowinandroid.feature.topic.api.navigation.navigateToTopic
|
||||
|
||||
fun EntryProviderScope<NavKey>.forYouEntry(navigator: Navigator) {
|
||||
entry<ForYouNavKey> {
|
||||
ForYouScreen(
|
||||
onTopicClick = navigator::navigateToTopic,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 208 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 245 KiB |
|
After Width: | Height: | Size: 195 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 231 KiB |
|
After Width: | Height: | Size: 113 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 25 KiB |