优化导航实现

This commit is contained in:
Joker.X
2026-02-18 10:30:01 +08:00
parent 8391331a25
commit fd359939e9
17 changed files with 43 additions and 85 deletions

View File

@@ -2,6 +2,7 @@ package com.joker.kit.core.navigation
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.SharedTransitionScope
import androidx.compose.animation.core.FiniteAnimationSpec
import androidx.compose.animation.core.tween
import androidx.compose.animation.slideInHorizontally
@@ -9,6 +10,7 @@ import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.togetherWith
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.IntOffset
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
@@ -49,7 +51,9 @@ fun AppNavHost(
// 创建应用级回退栈,首个页面固定为主页面。
val backStack = rememberNavBackStack(MainRoutes.Main)
// 基于当前回退栈构建导航控制器,供 AppNavigator 分发命令时使用。
val navigationController = rememberBackStackNavigationController(backStack, navigator)
val navigationController = remember(backStack, navigator) {
createBackStackNavigationController(backStack, navigator)
}
// 在组合生命周期内绑定/解绑导航控制器,确保导航命令总是指向当前有效宿主。
DisposableEffect(navigationController) {
@@ -77,7 +81,7 @@ fun AppNavHost(
transitionSpec = { createForwardTransition() },
popTransitionSpec = { createBackwardTransition() },
predictivePopTransitionSpec = { createBackwardTransition() },
entryProvider = appEntryProvider(),
entryProvider = appEntryProvider(this@SharedTransitionLayout),
)
}
}
@@ -118,7 +122,7 @@ private fun createBackwardTransition() = slideInHorizontally(
* @return 应用级 EntryProvider
* @author Joker.X
*/
private fun appEntryProvider() = entryProvider {
private fun appEntryProvider(scope: SharedTransitionScope) = entryProvider {
mainGraph()
demoGraph()
authGraph()

View File

@@ -1,7 +1,5 @@
package com.joker.kit.core.navigation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavKey
@@ -13,14 +11,11 @@ import androidx.navigation3.runtime.NavKey
* @return BackStack 导航控制器
* @author Joker.X
*/
@Composable
internal fun rememberBackStackNavigationController(
fun createBackStackNavigationController(
backStack: NavBackStack<NavKey>,
navigator: AppNavigator,
): NavigationController {
return remember(backStack, navigator) {
BackStackNavigationController(backStack = backStack, navigator = navigator)
}
return BackStackNavigationController(backStack = backStack, navigator = navigator)
}
/**
@@ -49,7 +44,11 @@ private class BackStackNavigationController(
override fun navigateTo(route: NavKey, navOptions: NavigationOptions?) {
val popUpToRoute = navOptions?.popUpToRoute
if (popUpToRoute != null) {
backStack.popUpTo(route = popUpToRoute, inclusive = navOptions.inclusive)
backStack.popUpTo(
route = popUpToRoute,
inclusive = navOptions.inclusive,
allowPopToEmpty = navOptions.allowPopToEmpty,
)
}
backStack.add(route)
}
@@ -94,9 +93,14 @@ private class BackStackNavigationController(
*
* @param route 目标路由
* @param inclusive 是否包含目标路由
* @param allowPopToEmpty 当目标路由是栈底时,是否允许清空整个返回栈
* @author Joker.X
*/
private fun NavBackStack<NavKey>.popUpTo(route: NavKey, inclusive: Boolean) {
private fun NavBackStack<NavKey>.popUpTo(
route: NavKey,
inclusive: Boolean,
allowPopToEmpty: Boolean = false,
) {
val targetIndex = indexOfLast { it == route }
if (targetIndex == -1) return
@@ -104,7 +108,9 @@ private fun NavBackStack<NavKey>.popUpTo(route: NavKey, inclusive: Boolean) {
if (removeFromIndex >= size) return
if (removeFromIndex == 0) {
if (size > 1) {
if (allowPopToEmpty) {
clear()
} else if (size > 1) {
subList(1, size).clear()
}
return

View File

@@ -9,7 +9,7 @@ import androidx.navigation3.runtime.NavKey
*
* @author Joker.X
*/
internal interface NavigationController {
interface NavigationController {
/**
* 导航到目标路由
*

View File

@@ -7,9 +7,12 @@ import androidx.navigation3.runtime.NavKey
*
* @param popUpToRoute 回退栈弹出到的目标路由
* @param inclusive 是否包含目标路由本身
* @param allowPopToEmpty 是否允许清空整个返回栈(用于替换栈底页面,如 Splash -> Main
* @author Joker.X
*/
data class NavigationOptions(
val popUpToRoute: NavKey? = null,
val inclusive: Boolean = false,
val allowPopToEmpty: Boolean = false,
)

View File

@@ -1,23 +0,0 @@
package com.joker.kit.core.navigation
/**
* 公共导航参数定义
*
* 用于沉淀跨模块可复用的导航参数模型,避免在各业务模块重复声明。
*
* @author Joker.X
*/
/**
* 通用 ID 参数
*
* 适用于仅需要传递单个 ID 的页面跳转场景。
*
* @property id 通用业务 ID
* @author Joker.X
*/
data class IdParam(
/**
* 通用业务 ID
*/
val id: Long,
)

View File

@@ -27,27 +27,24 @@ internal fun LoginRoute(
viewModel: LoginViewModel = hiltViewModel()
) {
LoginScreen(
onLoginClick = viewModel::login,
onBackClick = ::navigateBack
onLoginClick = viewModel::login
)
}
/**
* 登录页面
*
* @param onBackClick 返回按钮回调
* @param onLoginClick 登录按钮回调
* @author Joker.X
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun LoginScreen(
onBackClick: () -> Unit = {},
onLoginClick: () -> Unit = {},
) {
AppScaffold(
titleText = "登录",
onBackClick = onBackClick,
onBackClick = { navigateBack() },
) {
LoginContentView(
onLoginClick = onLoginClick

View File

@@ -14,7 +14,6 @@ import javax.inject.Inject
/**
* 登录页 ViewModel
*
* @param navigator 导航管理器
* @param userState 全局用户状态
* @author Joker.X
*/

View File

@@ -75,8 +75,7 @@ internal fun DatabaseRoute(
onDescriptionChange = viewModel::onDescriptionChange,
onAddClick = viewModel::addItem,
onDeleteItem = viewModel::deleteItem,
onClearAll = viewModel::clearAll,
onBackClick = ::navigateBack
onClearAll = viewModel::clearAll
)
}
@@ -91,7 +90,6 @@ internal fun DatabaseRoute(
* @param onAddClick 点击新增记录
* @param onDeleteItem 删除指定记录
* @param onClearAll 清空所有记录
* @param onBackClick 返回按钮回调
* @author Joker.X
*/
@OptIn(ExperimentalMaterial3Api::class)
@@ -105,11 +103,10 @@ internal fun DatabaseScreen(
onAddClick: () -> Unit = {},
onDeleteItem: (Long) -> Unit = {},
onClearAll: () -> Unit = {},
onBackClick: () -> Unit = {},
) {
AppScaffold(
titleText = "数据库",
onBackClick = onBackClick
onBackClick = { navigateBack() }
) {
DatabaseContent(
title = title,

View File

@@ -68,8 +68,7 @@ internal fun LocalStorageRoute(
onAvatarChange = viewModel::onAvatarChange,
onSaveUser = viewModel::saveUser,
onClearUser = viewModel::clearUser,
onReloadUser = viewModel::loadUser,
onBackClick = ::navigateBack
onReloadUser = viewModel::loadUser
)
}
@@ -86,7 +85,6 @@ internal fun LocalStorageRoute(
* @param onSaveUser 保存用户信息
* @param onClearUser 清除用户信息
* @param onReloadUser 重新读取用户信息
* @param onBackClick 返回按钮回调
* @author Joker.X
*/
@OptIn(ExperimentalMaterial3Api::class)
@@ -102,11 +100,10 @@ internal fun LocalStorageScreen(
onSaveUser: () -> Unit = {},
onClearUser: () -> Unit = {},
onReloadUser: () -> Unit = {},
onBackClick: () -> Unit = {},
) {
AppScaffold(
titleText = "本地存储",
onBackClick = onBackClick,
onBackClick = { navigateBack() },
) {
LocalStorageContent(
userId = userId,

View File

@@ -27,7 +27,6 @@ internal fun NavigationResultRoute(
viewModel: NavigationResultViewModel = hiltViewModel()
) {
NavigationResultScreen(
onBackClick = ::navigateBack,
onSendResult = viewModel::sendResultAndBack
)
}
@@ -36,18 +35,16 @@ internal fun NavigationResultRoute(
* 结果回传示例界面
*
* @param onSendResult 发送结果并返回回调
* @param onBackClick 返回按钮回调
* @author Joker.X
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun NavigationResultScreen(
onBackClick: () -> Unit = {},
onSendResult: () -> Unit = {},
) {
AppScaffold(
titleText = "结果回传",
onBackClick = onBackClick
onBackClick = { navigateBack() }
) {
NavigationResultContent(onSendResult = onSendResult)
}

View File

@@ -28,8 +28,7 @@ internal fun NavigationWithArgsRoute(
)
) {
NavigationWithArgsScreen(
goodsId = viewModel.goodsId,
onBackClick = ::navigateBack
goodsId = viewModel.goodsId
)
}
@@ -37,18 +36,16 @@ internal fun NavigationWithArgsRoute(
* 带参跳转示例界面
*
* @param goodsId 传入的商品 ID
* @param onBackClick 返回按钮回调
* @author Joker.X
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun NavigationWithArgsScreen(
goodsId: Long = 0,
onBackClick: () -> Unit = {},
) {
AppScaffold(
titleText = "带参跳转",
onBackClick = onBackClick
onBackClick = { navigateBack() }
) {
NavigationWithArgsContent(goodsId = goodsId)
}

View File

@@ -34,7 +34,6 @@ internal fun NetworkDemoRoute(
NetworkDemoScreen(
uiState = uiState,
onBackClick = ::navigateBack,
onRetry = viewModel::retryRequest
)
}
@@ -43,7 +42,6 @@ internal fun NetworkDemoRoute(
* Network Demo 界面
*
* @param uiState UI 状态
* @param onBackClick 返回按钮回调
* @param onRetry 重试回调
* @author Joker.X
*/
@@ -51,12 +49,11 @@ internal fun NetworkDemoRoute(
@Composable
internal fun NetworkDemoScreen(
uiState: BaseNetWorkUiState<Goods> = BaseNetWorkUiState.Loading,
onBackClick: () -> Unit = {},
onRetry: () -> Unit = {},
) {
AppScaffold(
titleText = "Network Demo",
onBackClick = onBackClick,
onBackClick = { navigateBack() },
) {
BaseNetWorkView(
uiState = uiState,

View File

@@ -49,7 +49,6 @@ internal fun NetworkListDemoRoute(
onRefresh = viewModel::onRefresh,
onLoadMore = viewModel::onLoadMore,
shouldTriggerLoadMore = viewModel::shouldTriggerLoadMore,
onBackClick = ::navigateBack,
onRetry = viewModel::retryRequest,
)
}
@@ -64,7 +63,6 @@ internal fun NetworkListDemoRoute(
* @param onRefresh 刷新回调
* @param onLoadMore 加载更多回调
* @param shouldTriggerLoadMore 是否触发加载更多
* @param onBackClick 返回回调
* @param onRetry 重试回调
* @author Joker.X
*/
@@ -78,12 +76,11 @@ internal fun NetworkListDemoScreen(
onRefresh: () -> Unit = {},
onLoadMore: () -> Unit = {},
shouldTriggerLoadMore: (lastIndex: Int, totalCount: Int) -> Boolean = { _, _ -> false },
onBackClick: () -> Unit = {},
onRetry: () -> Unit = {},
) {
AppScaffold(
titleText = "Network List Demo",
onBackClick = onBackClick
onBackClick = { navigateBack() }
) {
BaseNetWorkListView(
uiState = uiState,

View File

@@ -38,7 +38,6 @@ internal fun NetworkRequestRoute(
NetworkRequestScreen(
goods = goods,
onBackClick = ::navigateBack,
onRequestClick = viewModel::onRequestClick
)
}
@@ -47,7 +46,6 @@ internal fun NetworkRequestRoute(
* 网络请求示例界面
*
* @param goods 商品信息
* @param onBackClick 返回按钮回调
* @param onRequestClick 请求按钮回调
* @author Joker.X
*/
@@ -55,12 +53,11 @@ internal fun NetworkRequestRoute(
@Composable
internal fun NetworkRequestScreen(
goods: Goods? = null,
onBackClick: () -> Unit = {},
onRequestClick: () -> Unit = {},
) {
AppScaffold(
titleText = "网络请求",
onBackClick = onBackClick
onBackClick = { navigateBack() }
) {
NetworkRequestContent(
goods = goods,

View File

@@ -51,8 +51,7 @@ internal fun StateManagementRoute(
count = count,
onIncrease = viewModel::increase,
onDecrease = viewModel::decrease,
onReset = viewModel::reset,
onBackClick = ::navigateBack
onReset = viewModel::reset
)
}
@@ -63,7 +62,6 @@ internal fun StateManagementRoute(
* @param onIncrease +1 回调
* @param onDecrease -1 回调
* @param onReset 重置回调
* @param onBackClick 返回按钮回调
* @author Joker.X
*/
@OptIn(ExperimentalMaterial3Api::class)
@@ -73,11 +71,10 @@ internal fun StateManagementScreen(
onIncrease: () -> Unit = {},
onDecrease: () -> Unit = {},
onReset: () -> Unit = {},
onBackClick: () -> Unit = {},
) {
AppScaffold(
titleText = "状态管理",
onBackClick = onBackClick,
onBackClick = { navigateBack() },
) {
StateManagementContent(
count = count,

View File

@@ -27,27 +27,24 @@ internal fun UserInfoRoute(
viewModel: UserInfoViewModel = hiltViewModel()
) {
UserInfoScreen(
onLogoutClick = viewModel::logout,
onBackClick = ::navigateBack
onLogoutClick = viewModel::logout
)
}
/**
* 用户信息页面
*
* @param onBackClick 返回按钮回调
* @param onLogoutClick 退出登录回调
* @author Joker.X
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun UserInfoScreen(
onBackClick: () -> Unit = {},
onLogoutClick: () -> Unit = {},
) {
AppScaffold(
titleText = "用户信息",
onBackClick = onBackClick,
onBackClick = { navigateBack() },
) {
UserInfoContentView(
modifier = Modifier.padding(SpacePaddingLarge),

View File

@@ -12,7 +12,6 @@ import javax.inject.Inject
/**
* 用户信息页 ViewModel
*
* @param navigator 导航管理器
* @param userState 全局用户状态
* @author Joker.X
*/