初步重构导航到 nav3

This commit is contained in:
Joker.X
2026-02-14 15:59:43 +08:00
parent d7b54a2ff9
commit 8391331a25
95 changed files with 1546 additions and 1359 deletions

View File

@@ -150,7 +150,9 @@ dependencies {
implementation(libs.coil.compose)
// 导航组件
implementation(libs.navigation.compose)
implementation(libs.androidx.navigation3.runtime)
implementation(libs.androidx.navigation3.ui)
implementation(libs.androidx.lifecycle.viewmodel.navigation3)
// 序列化
implementation(libs.kotlinx.serialization.json)
@@ -178,7 +180,7 @@ dependencies {
// 依赖注入 (Hilt + Navigation)
implementation(libs.hilt.android)
ksp(libs.hilt.android.compiler)
implementation(libs.hilt.navigation.compose)
implementation(libs.hilt.lifecycle.viewmodel.compose)
androidTestImplementation(libs.hilt.android.testing)
kspAndroidTest(libs.hilt.android.compiler)
@@ -203,4 +205,4 @@ dependencies {
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
debugImplementation(libs.androidx.compose.ui.tooling)
debugImplementation(libs.androidx.compose.ui.test.manifest)
}
}

View File

@@ -5,7 +5,6 @@ import android.content.res.Configuration
import com.joker.kit.core.state.UserState
import com.joker.kit.core.util.storage.MMKVUtils
import com.joker.kit.core.util.toast.ToastUtils
import com.joker.kit.BuildConfig
import dagger.hilt.android.HiltAndroidApp
import timber.log.Timber
import javax.inject.Inject

View File

@@ -6,8 +6,8 @@ import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import com.joker.kit.core.designsystem.theme.AppTheme
import com.joker.kit.navigation.AppNavHost
import com.joker.kit.navigation.AppNavigator
import com.joker.kit.core.navigation.AppNavHost
import com.joker.kit.core.navigation.AppNavigator
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject

View File

@@ -11,4 +11,4 @@ import javax.inject.Inject
*/
@HiltViewModel
class MainActivityViewModel @Inject constructor(
) : ViewModel() {}
) : ViewModel()

View File

@@ -1,19 +1,17 @@
package com.joker.kit.core.base.viewmodel
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.Observer
import androidx.lifecycle.viewModelScope
import androidx.navigation.NavBackStackEntry
import com.joker.kit.core.base.state.BaseNetWorkListUiState
import com.joker.kit.core.base.state.LoadMoreState
import com.joker.kit.core.model.network.NetworkPageData
import com.joker.kit.core.model.network.NetworkResponse
import com.joker.kit.core.navigation.NavigationResultKey
import com.joker.kit.core.navigation.RefreshResult
import com.joker.kit.core.navigation.RefreshResultKey
import com.joker.kit.core.navigation.resultEvents
import com.joker.kit.core.result.ResultHandler
import com.joker.kit.core.result.asResult
import com.joker.kit.core.state.UserState
import com.joker.kit.navigation.AppNavigator
import com.joker.kit.navigation.NavigationResultKey
import com.joker.kit.navigation.RefreshResultKey
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -28,14 +26,16 @@ import kotlinx.coroutines.launch
* 封装了常见的列表操作逻辑,简化子类实现
*
* @param T 列表项数据类型
* @param navigator 导航控制器
* @param userState 用户状态
* @author Joker.X
*/
abstract class BaseNetWorkListViewModel<T : Any>(
navigator: AppNavigator,
userState: UserState
) : BaseViewModel(navigator, userState) {
abstract class BaseNetWorkListViewModel<T : Any> : BaseViewModel() {
/**
* 刷新结果监听任务
*
* 用于保证只注册一次刷新结果监听,避免重复 collect 导致重复刷新和内存浪费。
* 当该任务不为 null 时,表示当前 ViewModel 已经建立监听。
*/
private var refreshObserveJob: Job? = null
/**
* 当前页码
@@ -274,34 +274,26 @@ abstract class BaseNetWorkListViewModel<T : Any>(
/**
* 视图层调用此方法,监听页面刷新信号(基于 NavigationResultKey
*
* @param backStackEntry 当前页面的 NavBackStackEntry
* @param key 刷新结果的类型安全 Key默认使用全局的 [RefreshResultKey]
*
* 用法:在 Composable 中调用
* ```kotlin
* val backStackEntry = navController.currentBackStackEntry
* LaunchedEffect(backStackEntry) {
* viewModel.observeRefreshState(backStackEntry)
* }
* viewModel.observeRefreshState()
* ```
*
* 只需调用一次,自动去重和解绑,无内存泄漏。
* 语义等价于旧方案中的 "refresh" 布尔标记
* 当 [RefreshResult.refresh] 为 true 时触发刷新
*/
fun observeRefreshState(
backStackEntry: NavBackStackEntry?,
key: NavigationResultKey<Boolean> = RefreshResultKey
key: NavigationResultKey<RefreshResult> = RefreshResultKey
) {
if (backStackEntry == null) return
val owner: LifecycleOwner = backStackEntry
backStackEntry.savedStateHandle
.getLiveData<Boolean>(key.key)
.observe(owner, Observer<Boolean> { value ->
if (value) {
if (refreshObserveJob != null) return
refreshObserveJob = viewModelScope.launch {
resultEvents(key).collect { refreshResult ->
if (refreshResult.refresh == true) {
onRefresh()
// 只刷新一次
backStackEntry.savedStateHandle[key.key] = false
}
})
}
}
}
}
}

View File

@@ -1,18 +1,15 @@
package com.joker.kit.core.base.viewmodel
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.Observer
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import androidx.navigation.NavBackStackEntry
import com.joker.kit.core.base.state.BaseNetWorkUiState
import com.joker.kit.core.model.network.NetworkResponse
import com.joker.kit.core.navigation.NavigationResultKey
import com.joker.kit.core.navigation.RefreshResult
import com.joker.kit.core.navigation.RefreshResultKey
import com.joker.kit.core.navigation.resultEvents
import com.joker.kit.core.result.ResultHandler
import com.joker.kit.core.result.asResult
import com.joker.kit.core.state.UserState
import com.joker.kit.navigation.AppNavigator
import com.joker.kit.navigation.NavigationResultKey
import com.joker.kit.navigation.RefreshResultKey
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -27,16 +24,16 @@ import kotlinx.coroutines.launch
* 支持自动从SavedStateHandle获取路由参数ID
*
* @param T 数据类型
* @param navigator 导航控制器
* @param userState 用户状态
* @param savedStateHandle 保存状态句柄,用于获取路由参数
* @author Joker.X
*/
abstract class BaseNetWorkViewModel<T>(
navigator: AppNavigator,
userState: UserState,
protected val savedStateHandle: SavedStateHandle? = null,
) : BaseViewModel(navigator, userState) {
abstract class BaseNetWorkViewModel<T> : BaseViewModel() {
/**
* 刷新结果监听任务
*
* 用于保证只注册一次刷新结果监听,避免重复 collect 导致重复请求。
* 当该任务不为 null 时,表示当前 ViewModel 已经建立监听。
*/
private var refreshObserveJob: Job? = null
/**
* 通用网络请求UI状态
@@ -176,34 +173,26 @@ abstract class BaseNetWorkViewModel<T>(
/**
* 视图层调用此方法,监听页面刷新信号(基于 NavigationResultKey
*
* @param backStackEntry 当前页面的 NavBackStackEntry
* @param key 刷新结果的类型安全 Key默认使用全局的 [RefreshResultKey]
*
* 用法:在 Composable 中调用
* ```kotlin
* val backStackEntry = navController.currentBackStackEntry
* LaunchedEffect(backStackEntry) {
* viewModel.observeRefreshState(backStackEntry)
* }
* viewModel.observeRefreshState()
* ```
*
* 只需调用一次,自动去重和解绑,无内存泄漏。
* 语义等价于旧方案中的 "refresh" 布尔标记
* 当 [RefreshResult.refresh] 为 true 时触发刷新
*/
fun observeRefreshState(
backStackEntry: NavBackStackEntry?,
key: NavigationResultKey<Boolean> = RefreshResultKey
key: NavigationResultKey<RefreshResult> = RefreshResultKey
) {
if (backStackEntry == null) return
val owner: LifecycleOwner = backStackEntry
backStackEntry.savedStateHandle
.getLiveData<Boolean>(key.key)
.observe(owner, Observer<Boolean> { value ->
if (value) {
if (refreshObserveJob != null) return
refreshObserveJob = viewModelScope.launch {
resultEvents(key).collect { refreshResult ->
if (refreshResult.refresh == true) {
executeRequest()
// 只刷新一次
backStackEntry.savedStateHandle[key.key] = false
}
})
}
}
}
}
}

View File

@@ -1,193 +1,10 @@
package com.joker.kit.core.base.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.navigation.NavOptions
import com.joker.kit.core.state.UserState
import com.joker.kit.navigation.AppNavigator
import com.joker.kit.navigation.NavigationResultKey
import com.joker.kit.navigation.RouteInterceptor
import kotlinx.coroutines.launch
/**
* 基础ViewModel(类型安全版本)
* 基础 ViewModel
*
* 提供所有ViewModel通用的功能
* 1. 类型安全的导航
* 2. 路由拦截(登录检查)
* 3. 类型安全的结果返回
*
* 使用示例:
* ```kotlin
* class MyViewModel @Inject constructor(
* navigator: AppNavigator,
* userState: UserState
* ) : BaseViewModel(navigator, userState) {
* fun onItemClick(id: Long) {
* navigate(GoodsRoutes.Detail(goodsId = id))
* }
*
* fun onSuccess() {
* navigateBack(RefreshResult)
* }
* }
* ```
*
* @param navigator 导航控制器
* @param userState 应用状态
* @param routeInterceptor 路由拦截器
* @author Joker.X
*/
abstract class BaseViewModel(
protected val navigator: AppNavigator,
protected val userState: UserState,
protected val routeInterceptor: RouteInterceptor = RouteInterceptor()
) : ViewModel() {
// ==================== 基础导航方法 ====================
/**
* 导航到指定路由(类型安全)
* 自动处理登录拦截逻辑
*
* @param route 目标路由对象(必须是 @Serializable
* @param navOptions 导航选项(可选)
*
* 使用示例:
* ```kotlin
* // 简单导航
* navigate(MainRoutes.Home)
*
* // 带参数导航
* navigate(GoodsRoutes.Detail(goodsId = 123))
*
* // 带 NavOptions
* navigate(UserRoutes.Profile, navOptions)
* ```
*
* @author Joker.X
*/
fun navigate(route: Any, navOptions: NavOptions? = null) {
viewModelScope.launch {
val targetRoute = checkRouteInterception(route)
navigator.navigateTo(targetRoute, navOptions)
}
}
/**
* 导航到指定路由并关闭当前页面
* 自动处理登录拦截逻辑
*
* @param route 目标路由对象
* @param currentRoute 当前页面路由对象,将被关闭
*
* 使用示例:
* ```kotlin
* navigateAndCloseCurrent(
* route = MainRoutes.Home,
* currentRoute = AuthRoutes.Login
* )
* ```
*
* @author Joker.X
*/
fun navigateAndCloseCurrent(route: Any, currentRoute: Any) {
viewModelScope.launch {
val targetRoute = checkRouteInterception(route)
val navOptions = NavOptions.Builder()
.setPopUpTo(
route = currentRoute,
inclusive = true, // 设为true表示当前页面也会被弹出
saveState = false // 不保存状态
)
.build()
navigator.navigateTo(targetRoute, navOptions)
}
}
// ==================== 返回导航方法 ====================
/**
* 返回上一页
*
* 使用示例:
* ```kotlin
* navigateBack()
* ```
*
* @author Joker.X
*/
fun navigateBack() {
viewModelScope.launch {
navigator.navigateBack()
}
}
/**
* 返回上一页并携带类型安全的结果(使用 NavigationResultKey
*
* 这是 V3.2 版本的最终方案,实现了端到端的类型安全。
*
* @param key 类型安全的结果 Key
* @param result 要传递的结果对象
*
* 使用示例:
* ```kotlin
* // 1. 定义返回结果数据类型
* @Serializable
* data class Address(val id: Long, val fullAddress: String)
*
* // 2. 定义 ResultKey
* object SelectAddressResultKey : NavigationResultKey<Address>
*
* // 3. 返回时携带结果
* popBackStackWithResult(SelectAddressResultKey, address)
* ```
*
* @author Joker.X
*/
fun <T> popBackStackWithResult(key: NavigationResultKey<T>, result: T) {
viewModelScope.launch {
navigator.popBackStackWithResult(key, result)
}
}
/**
* 返回到指定路由
*
* @param route 目标路由对象
* @param inclusive 是否包含目标路由本身
*
* 使用示例:
* ```kotlin
* // 返回到主页并保留主页
* navigateBackTo(MainRoutes.Main, inclusive = false)
* ```
*
* @author Joker.X
*/
fun navigateBackTo(route: Any, inclusive: Boolean = false) {
viewModelScope.launch {
navigator.navigateBackTo(route, inclusive)
}
}
// ==================== 内部方法 ====================
/**
* 检查路由是否需要登录拦截(类型安全)
*
* @param route 目标路由对象
* @return 如果需要拦截返回登录页面路由,否则返回原路由
* @author Joker.X
*/
private fun checkRouteInterception(route: Any): Any {
return if (routeInterceptor.requiresLogin(route) && !userState.isLoggedIn.value) {
// 需要登录但未登录,跳转到登录页面
routeInterceptor.getLoginRoute()
} else {
// 不需要登录或已登录,正常跳转
route
}
}
}
abstract class BaseViewModel : ViewModel()

View File

@@ -1,8 +1,8 @@
package com.joker.kit.core.data.repository
import com.joker.kit.core.network.datasource.userinfo.UserInfoNetworkDataSource
import com.joker.kit.core.model.entity.User
import com.joker.kit.core.model.network.NetworkResponse
import com.joker.kit.core.network.datasource.userinfo.UserInfoNetworkDataSource
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow

View File

@@ -57,7 +57,7 @@ class UserInfoStoreDataSourceImpl @Inject constructor() : UserInfoStoreDataSourc
* @author Joker.X
*/
override suspend fun updateUserInfo(updates: Map<String, Any?>) {
val currentUser = getUserInfo() ?: return
getUserInfo() ?: return
val userJson = MMKVUtils.getString(KEY_USER_INFO, "")
if (userJson.isEmpty()) return

View File

@@ -0,0 +1,126 @@
package com.joker.kit.core.navigation
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.core.FiniteAnimationSpec
import androidx.compose.animation.core.tween
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.togetherWith
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.IntOffset
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
import androidx.navigation3.ui.NavDisplay
import com.joker.kit.core.navigation.main.MainRoutes
import com.joker.kit.feature.auth.navigation.authGraph
import com.joker.kit.feature.demo.navigation.demoGraph
import com.joker.kit.feature.main.navigation.mainGraph
import com.joker.kit.feature.user.navigation.userGraph
/**
* 页面切换动画时长(毫秒)
*/
private const val NAV_ANIMATION_DURATION = 300
/**
* 页面切换动画规范
*/
private val NAV_ANIMATION_SPEC: FiniteAnimationSpec<IntOffset> =
tween(durationMillis = NAV_ANIMATION_DURATION)
/**
* 应用导航宿主
*
* @param navigator 导航管理器
* @param modifier 修饰符
* @author Joker.X
*/
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun AppNavHost(
navigator: AppNavigator,
modifier: Modifier = Modifier,
) {
// 创建应用级回退栈,首个页面固定为主页面。
val backStack = rememberNavBackStack(MainRoutes.Main)
// 基于当前回退栈构建导航控制器,供 AppNavigator 分发命令时使用。
val navigationController = rememberBackStackNavigationController(backStack, navigator)
// 在组合生命周期内绑定/解绑导航控制器,确保导航命令总是指向当前有效宿主。
DisposableEffect(navigationController) {
// 绑定到 AppNavigator接收全局导航命令。
navigator.attachController(navigationController)
// 绑定到全局导航服务,支持业务层直接调用 navigate(...)。
NavigationService.bind(navigator)
onDispose {
// 宿主销毁时先解绑导航服务,避免持有失效导航器引用。
NavigationService.unbind(navigator)
// 最后从 AppNavigator 注销控制器,防止后续命令误发到旧宿主。
navigator.detachController(navigationController)
}
}
SharedTransitionLayout {
NavDisplay(
backStack = backStack,
modifier = modifier,
onBack = { navigationController.navigateBack() },
entryDecorators = listOf(
rememberSaveableStateHolderNavEntryDecorator(),
rememberViewModelStoreNavEntryDecorator(),
),
transitionSpec = { createForwardTransition() },
popTransitionSpec = { createBackwardTransition() },
predictivePopTransitionSpec = { createBackwardTransition() },
entryProvider = appEntryProvider(),
)
}
}
/**
* 创建前进导航动画(右入左出)
*
* @return 前进导航动画
* @author Joker.X
*/
private fun createForwardTransition() = slideInHorizontally(
initialOffsetX = { it },
animationSpec = NAV_ANIMATION_SPEC,
) togetherWith slideOutHorizontally(
targetOffsetX = { -it },
animationSpec = NAV_ANIMATION_SPEC,
)
/**
* 创建返回导航动画(左入右出)
*
* @return 返回导航动画
* @author Joker.X
*/
private fun createBackwardTransition() = slideInHorizontally(
initialOffsetX = { -it },
animationSpec = NAV_ANIMATION_SPEC,
) togetherWith slideOutHorizontally(
targetOffsetX = { it },
animationSpec = NAV_ANIMATION_SPEC,
)
/**
* 构建应用级路由注册器
*
* 按模块聚合 graph避免全部 entry 混在同一个函数中。
*
* @return 应用级 EntryProvider
* @author Joker.X
*/
private fun appEntryProvider() = entryProvider {
mainGraph()
demoGraph()
authGraph()
userGraph()
}

View File

@@ -0,0 +1,192 @@
package com.joker.kit.core.navigation
import androidx.navigation3.runtime.NavKey
import com.joker.kit.core.state.UserState
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
import javax.inject.Inject
import javax.inject.Singleton
/**
* 导航管理器
*
* 提供给 ViewModel 的统一导航入口:
* 1. ViewModel 直接调用导航 API
* 2. AppNavHost 注册 BackStack 控制器
* 3. 导航结果通过 resultEvents 分发
*
* @author Joker.X
*/
@Singleton
class AppNavigator @Inject constructor(
private val userState: UserState,
) {
/**
* 导航控制器访问锁
*/
private val lock = Any()
/**
* 当前活跃的导航控制器
*/
private var controller: NavigationController? = null
/**
* 控制器未注册时缓存的导航命令队列
*/
private val pendingCommands = ArrayDeque<NavigationCommand>()
/**
* 页面结果事件流
*/
private val _resultEvents = MutableSharedFlow<ResultEvent>(extraBufferCapacity = 32)
/**
* 路由拦截器
*
* 在统一导航入口做登录拦截,避免不同调用入口出现行为不一致。
*/
private val routeInterceptor: RouteInterceptor = RouteInterceptor()
/**
* 注册导航控制器
*
* @author Joker.X
*/
internal fun attachController(navigationController: NavigationController) {
synchronized(lock) {
controller = navigationController
while (pendingCommands.isNotEmpty()) {
pendingCommands.removeFirst().execute(navigationController)
}
}
}
/**
* 注销导航控制器
*
* @author Joker.X
*/
internal fun detachController(navigationController: NavigationController) {
synchronized(lock) {
if (controller === navigationController) {
controller = null
}
}
}
/**
* 导航到指定路由
*
* @param route 类型安全路由对象
* @param navOptions 导航选项(可选)
* @author Joker.X
*/
fun navigateTo(route: NavKey, navOptions: NavigationOptions? = null) {
val targetRoute = resolveTargetRoute(route)
executeOrEnqueue(NavigationCommand.NavigateTo(targetRoute, navOptions))
}
/**
* 返回上一页
*
* @author Joker.X
*/
fun navigateBack() {
executeOrEnqueue(NavigationCommand.NavigateUp)
}
/**
* 返回上一页并携带类型安全结果
*
* @param key 类型安全结果 Key
* @param result 返回结果
* @author Joker.X
*/
fun <T> popBackStackWithResult(key: NavigationResultKey<T>, result: T) {
executeOrEnqueue(NavigationCommand.PopBackStackWithResult(key, result))
}
/**
* 返回到指定路由
*
* @param route 目标路由对象
* @param inclusive 是否包含目标路由
* @author Joker.X
*/
fun navigateBackTo(route: NavKey, inclusive: Boolean = false) {
executeOrEnqueue(NavigationCommand.NavigateBackTo(route, inclusive))
}
/**
* 监听某个 ResultKey 对应的结果流
*
* @param key 类型安全结果 Key
* @return 强类型结果流
* @author Joker.X
*/
fun <T> resultEvents(key: NavigationResultKey<T>): Flow<T> {
return _resultEvents
.filter { it.key == key.key }
.map { key.deserialize(it.rawValue) }
}
/**
* 分发回传结果事件
*
* @param key 类型安全结果 Key
* @param result 返回结果
* @author Joker.X
*/
internal fun <T> dispatchResult(key: NavigationResultKey<T>, result: T) {
val rawValue = key.serialize(result)
_resultEvents.tryEmit(ResultEvent(key = key.key, rawValue = rawValue))
}
/**
* 执行导航命令(若控制器未注册则先缓存)
*
* @author Joker.X
*/
private fun executeOrEnqueue(command: NavigationCommand) {
synchronized(lock) {
val currentController = controller
if (currentController != null) {
command.execute(currentController)
} else {
pendingCommands.addLast(command)
}
}
}
/**
* 解析最终跳转路由
*
* 当目标路由需要登录且当前未登录时,返回登录页面路由。
*
* @param route 原始目标路由
* @return 实际执行跳转的路由
* @author Joker.X
*/
private fun resolveTargetRoute(route: NavKey): NavKey {
return if (routeInterceptor.requiresLogin(route) && !userState.isLoggedIn.value) {
routeInterceptor.getLoginRoute()
} else {
route
}
}
}
/**
* 导航回传结果事件
*
* @param key 结果 Key
* @param rawValue 序列化后的结果值
* @author Joker.X
*/
private data class ResultEvent(
val key: String,
val rawValue: Any,
)

View File

@@ -0,0 +1,114 @@
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
/**
* 创建 BackStack 导航控制器
*
* @param backStack 回退栈
* @param navigator 导航管理器
* @return BackStack 导航控制器
* @author Joker.X
*/
@Composable
internal fun rememberBackStackNavigationController(
backStack: NavBackStack<NavKey>,
navigator: AppNavigator,
): NavigationController {
return remember(backStack, navigator) {
BackStackNavigationController(backStack = backStack, navigator = navigator)
}
}
/**
* 基于 BackStack 的导航控制器实现
*
* @author Joker.X
*/
private class BackStackNavigationController(
/**
* 回退栈
*/
private val backStack: NavBackStack<NavKey>,
/**
* 导航管理器
*/
private val navigator: AppNavigator,
) : NavigationController {
/**
* 导航到目标页面
*
* @param route 目标路由
* @param navOptions 导航选项
* @author Joker.X
*/
override fun navigateTo(route: NavKey, navOptions: NavigationOptions?) {
val popUpToRoute = navOptions?.popUpToRoute
if (popUpToRoute != null) {
backStack.popUpTo(route = popUpToRoute, inclusive = navOptions.inclusive)
}
backStack.add(route)
}
/**
* 返回上一页
*
* @author Joker.X
*/
override fun navigateBack() {
if (backStack.size > 1) {
backStack.removeLastOrNull()
}
}
/**
* 返回到指定页面
*
* @param route 目标路由
* @param inclusive 是否包含目标路由
* @author Joker.X
*/
override fun navigateBackTo(route: NavKey, inclusive: Boolean) {
backStack.popUpTo(route = route, inclusive = inclusive)
}
/**
* 回退并携带结果
*
* @param key 结果 Key
* @param result 回传结果
* @author Joker.X
*/
override fun <T> popBackStackWithResult(key: NavigationResultKey<T>, result: T) {
navigator.dispatchResult(key = key, result = result)
navigateBack()
}
}
/**
* BackStack 按路由弹栈
*
* @param route 目标路由
* @param inclusive 是否包含目标路由
* @author Joker.X
*/
private fun NavBackStack<NavKey>.popUpTo(route: NavKey, inclusive: Boolean) {
val targetIndex = indexOfLast { it == route }
if (targetIndex == -1) return
val removeFromIndex = if (inclusive) targetIndex else targetIndex + 1
if (removeFromIndex >= size) return
if (removeFromIndex == 0) {
if (size > 1) {
subList(1, size).clear()
}
return
}
subList(removeFromIndex, size).clear()
}

View File

@@ -0,0 +1,101 @@
package com.joker.kit.core.navigation
import androidx.navigation3.runtime.NavKey
/**
* 导航命令
*
* @author Joker.X
*/
internal sealed interface NavigationCommand {
/**
* 执行导航命令
*
* @param controller 导航控制器
* @author Joker.X
*/
fun execute(controller: NavigationController)
/**
* 导航到指定路由命令
*
* @property route 目标路由
* @property navOptions 导航选项
* @author Joker.X
*/
data class NavigateTo(
val route: NavKey,
val navOptions: NavigationOptions?,
) : NavigationCommand {
/**
* 执行命令
*
* @param controller 导航控制器
* @author Joker.X
*/
override fun execute(controller: NavigationController) {
controller.navigateTo(route, navOptions)
}
}
/**
* 返回上一页命令
*
* @author Joker.X
*/
data object NavigateUp : NavigationCommand {
/**
* 执行命令
*
* @param controller 导航控制器
* @author Joker.X
*/
override fun execute(controller: NavigationController) {
controller.navigateBack()
}
}
/**
* 回退到指定路由命令
*
* @property route 目标路由
* @property inclusive 是否包含目标路由
* @author Joker.X
*/
data class NavigateBackTo(
val route: NavKey,
val inclusive: Boolean,
) : NavigationCommand {
/**
* 执行命令
*
* @param controller 导航控制器
* @author Joker.X
*/
override fun execute(controller: NavigationController) {
controller.navigateBackTo(route, inclusive)
}
}
/**
* 回退并回传结果命令
*
* @property key 结果 Key
* @property result 结果值
* @author Joker.X
*/
data class PopBackStackWithResult<T>(
val key: NavigationResultKey<T>,
val result: T,
) : NavigationCommand {
/**
* 执行命令
*
* @param controller 导航控制器
* @author Joker.X
*/
override fun execute(controller: NavigationController) {
controller.popBackStackWithResult(key, result)
}
}
}

View File

@@ -0,0 +1,46 @@
package com.joker.kit.core.navigation
import androidx.navigation3.runtime.NavKey
/**
* 导航控制器接口
*
* 由 AppNavHost 提供 BackStack 实现。
*
* @author Joker.X
*/
internal interface NavigationController {
/**
* 导航到目标路由
*
* @param route 目标路由
* @param navOptions 导航选项
* @author Joker.X
*/
fun navigateTo(route: NavKey, navOptions: NavigationOptions?)
/**
* 返回上一页
*
* @author Joker.X
*/
fun navigateBack()
/**
* 回退到指定路由
*
* @param route 目标路由
* @param inclusive 是否包含目标路由
* @author Joker.X
*/
fun navigateBackTo(route: NavKey, inclusive: Boolean)
/**
* 回退并携带结果
*
* @param key 结果 Key
* @param result 结果值
* @author Joker.X
*/
fun <T> popBackStackWithResult(key: NavigationResultKey<T>, result: T)
}

View File

@@ -0,0 +1,15 @@
package com.joker.kit.core.navigation
import androidx.navigation3.runtime.NavKey
/**
* 导航选项
*
* @param popUpToRoute 回退栈弹出到的目标路由
* @param inclusive 是否包含目标路由本身
* @author Joker.X
*/
data class NavigationOptions(
val popUpToRoute: NavKey? = null,
val inclusive: Boolean = false,
)

View File

@@ -0,0 +1,23 @@
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

@@ -0,0 +1,17 @@
package com.joker.kit.core.navigation
/**
* 公共导航返回结果定义
*
* 用于沉淀跨模块可复用的页面返回结果模型。
*
* @author Joker.X
*/
data class RefreshResult(
/**
* 是否需要刷新
*
* true 表示需要刷新false 或 null 表示不刷新。
*/
val refresh: Boolean? = null,
)

View File

@@ -1,4 +1,4 @@
package com.joker.kit.navigation
package com.joker.kit.core.navigation
/**
* 导航返回结果的类型安全 Key
@@ -17,7 +17,7 @@ package com.joker.kit.navigation
*
* 使用示例接收结果
* ```kotlin
* navController.collectResult(SelectAddressResultKey) { address ->
* navigator.resultEvents(SelectAddressResultKey).collect { address ->
* viewModel.onAddressSelected(address)
* }
* ```
@@ -27,7 +27,7 @@ package com.joker.kit.navigation
*/
interface NavigationResultKey<T> {
/**
* 底层用于 SavedStateHandle 存储的字符串 key
* 底层用于结果分发的字符串 key
*
* 默认实现使用 Key 对象自身的完全限定类名保证全局唯一且无需手写字符串
*/
@@ -35,7 +35,7 @@ interface NavigationResultKey<T> {
get() = this::class.java.name
/**
* 将结果对象序列化为 SavedStateHandle 可接受的底层类型
* 将结果对象序列化为可分发的底层类型
*
* 默认实现为透传即直接存储原始对象适用于 BooleanIntString 等基础类型
* 复杂类型可以在具体的 Key 中重写此方法例如序列化为 JSON 字符串
@@ -43,7 +43,7 @@ interface NavigationResultKey<T> {
fun serialize(value: T): Any = value as Any
/**
* SavedStateHandle 中还原结果对象
* 结果分发流中还原结果对象
*
* 默认实现为简单强转复杂类型的 Key 需要重写以配合 [serialize]
*/

View File

@@ -0,0 +1,241 @@
package com.joker.kit.core.navigation
import androidx.navigation3.runtime.NavKey
import kotlinx.coroutines.flow.Flow
/**
* 全局导航服务
*
* 统一维护当前可用导航器,并对外提供通用导航能力。
* 业务层只需调用本文件提供的简写函数,无需关注底层绑定细节。
*
* @author Joker.X
*/
object NavigationService {
/**
* 当前导航器实例
*/
@Volatile
private var navigator: AppNavigator? = null
/**
* 绑定导航器
*
* @param appNavigator 待绑定的导航器
* @author Joker.X
*/
fun bind(appNavigator: AppNavigator) {
navigator = appNavigator
}
/**
* 解绑导航器
*
* @param appNavigator 待解绑的导航器
* @author Joker.X
*/
fun unbind(appNavigator: AppNavigator) {
if (navigator === appNavigator) {
navigator = null
}
}
/**
* 获取当前导航器
*
* @return 当前导航器实例
* @throws IllegalStateException 当导航器未绑定时抛出异常
* @author Joker.X
*/
fun requireNavigator(): AppNavigator {
return navigator ?: error("AppNavigator is not bound")
}
/**
* 跳转到目标路由
*
* @param route 目标路由
* @param navOptions 导航选项
* @author Joker.X
*/
fun navigate(route: NavKey, navOptions: NavigationOptions? = null) {
requireNavigator().navigateTo(route = route, navOptions = navOptions)
}
/**
* 跳转到目标路由并关闭当前页面
*
* 常用于“当前页操作成功后进入下一个页面,并且不允许再返回当前页”的场景。
*
* @param route 目标路由
* @param currentRoute 当前路由(将被关闭)
* @author Joker.X
*/
fun navigateAndCloseCurrent(route: NavKey, currentRoute: NavKey) {
val navOptions = NavigationOptions(
popUpToRoute = currentRoute,
inclusive = true,
)
requireNavigator().navigateTo(route = route, navOptions = navOptions)
}
/**
* 跳转到目标路由并按条件清理回退栈
*
* @param route 目标路由
* @param popUpToRoute 回退栈清理到的目标路由
* @param inclusive 是否包含 [popUpToRoute]
* @author Joker.X
*/
fun navigateWithPopUpTo(route: NavKey, popUpToRoute: NavKey, inclusive: Boolean = false) {
val navOptions = NavigationOptions(
popUpToRoute = popUpToRoute,
inclusive = inclusive,
)
requireNavigator().navigateTo(route = route, navOptions = navOptions)
}
/**
* 返回上一页
*
* @author Joker.X
*/
fun navigateBack() {
requireNavigator().navigateBack()
}
/**
* 返回到指定路由
*
* @param route 目标路由
* @param inclusive 是否包含目标路由
* @author Joker.X
*/
fun navigateBackTo(route: NavKey, inclusive: Boolean = false) {
requireNavigator().navigateBackTo(route = route, inclusive = inclusive)
}
/**
* 返回上一页并携带结果
*
* @param key 结果 Key
* @param result 返回结果
* @author Joker.X
*/
fun <T> popBackStackWithResult(key: NavigationResultKey<T>, result: T) {
requireNavigator().popBackStackWithResult(key = key, result = result)
}
/**
* 返回上一页并携带结果(语义化别名)
*
* @param key 结果 Key
* @param result 返回结果
* @author Joker.X
*/
fun <T> navigateBackWithResult(key: NavigationResultKey<T>, result: T) {
popBackStackWithResult(key = key, result = result)
}
/**
* 监听指定结果 Key 的结果流
*
* @param key 结果 Key
* @return 对应结果流
* @author Joker.X
*/
fun <T> resultEvents(key: NavigationResultKey<T>): Flow<T> {
return requireNavigator().resultEvents(key)
}
}
/**
* 跳转到目标路由
*
* @param route 目标路由
* @param navOptions 导航选项
* @author Joker.X
*/
fun navigate(route: NavKey, navOptions: NavigationOptions? = null) {
NavigationService.navigate(route = route, navOptions = navOptions)
}
/**
* 跳转到目标路由并关闭当前页面
*
* @param route 目标路由
* @param currentRoute 当前路由(将被关闭)
* @author Joker.X
*/
fun navigateAndCloseCurrent(route: NavKey, currentRoute: NavKey) {
NavigationService.navigateAndCloseCurrent(route = route, currentRoute = currentRoute)
}
/**
* 跳转到目标路由并按条件清理回退栈
*
* @param route 目标路由
* @param popUpToRoute 回退栈清理到的目标路由
* @param inclusive 是否包含 [popUpToRoute]
* @author Joker.X
*/
fun navigateWithPopUpTo(route: NavKey, popUpToRoute: NavKey, inclusive: Boolean = false) {
NavigationService.navigateWithPopUpTo(
route = route,
popUpToRoute = popUpToRoute,
inclusive = inclusive,
)
}
/**
* 返回上一页
*
* @author Joker.X
*/
fun navigateBack() {
NavigationService.navigateBack()
}
/**
* 返回到指定路由
*
* @param route 目标路由
* @param inclusive 是否包含目标路由
* @author Joker.X
*/
fun navigateBackTo(route: NavKey, inclusive: Boolean = false) {
NavigationService.navigateBackTo(route = route, inclusive = inclusive)
}
/**
* 返回上一页并携带结果
*
* @param key 结果 Key
* @param result 返回结果
* @author Joker.X
*/
fun <T> popBackStackWithResult(key: NavigationResultKey<T>, result: T) {
NavigationService.popBackStackWithResult(key = key, result = result)
}
/**
* 返回上一页并携带结果(语义化别名)
*
* @param key 结果 Key
* @param result 返回结果
* @author Joker.X
*/
fun <T> navigateBackWithResult(key: NavigationResultKey<T>, result: T) {
NavigationService.navigateBackWithResult(key = key, result = result)
}
/**
* 监听指定结果 Key 的结果流
*
* @param key 结果 Key
* @return 对应结果流
* @author Joker.X
*/
fun <T> resultEvents(key: NavigationResultKey<T>): Flow<T> {
return NavigationService.resultEvents(key)
}

View File

@@ -0,0 +1,23 @@
package com.joker.kit.core.navigation
/**
* 通用的页面刷新结果 Key。
*
* 使用公共的 [RefreshResult] 作为返回类型:
* - refresh = true 表示上一个页面需要刷新数据
* - refresh = false 或 null 表示不刷新
*
* 示例:
* ```kotlin
* // 子页面:操作成功后返回并通知上一个页面刷新
* popBackStackWithResult(RefreshResultKey, RefreshResult(refresh = true))
*
* // 上一个页面ViewModel
* fun observeRefresh() {
* observeRefreshState(RefreshResultKey)
* }
* ```
*
* @author Joker.X
*/
object RefreshResultKey : NavigationResultKey<RefreshResult>

View File

@@ -0,0 +1,48 @@
package com.joker.kit.core.navigation
import androidx.navigation3.runtime.NavKey
import com.joker.kit.core.navigation.auth.AuthRoutes
import com.joker.kit.core.navigation.user.UserRoutes
import kotlin.reflect.KClass
/**
* 路由拦截器
*
* 负责管理需要登录的页面配置和路由拦截逻辑
* 使用类型安全的方式处理路由拦截
*
* @author Joker.X
*/
class RouteInterceptor {
/**
* 需要登录的路由类型集合
*
* 在这里统一声明所有需要登录才能访问的页面类型。
*
* @author Joker.X
*/
private val loginRequiredRouteTypes: Set<KClass<out NavKey>> = setOf(
UserRoutes.Info::class
)
/**
* 检查指定路由对象是否需要登录
*
* @param route 要检查的路由对象(类型安全)
* @return true表示需要登录false表示不需要登录
* @author Joker.X
*/
fun requiresLogin(route: NavKey): Boolean {
val routeClass = route::class
return loginRequiredRouteTypes.contains(routeClass)
}
/**
* 获取登录页面路由对象
*
* @return 登录页面的路由对象
* @author Joker.X
*/
fun getLoginRoute(): NavKey = AuthRoutes.Login
}

View File

@@ -0,0 +1,20 @@
package com.joker.kit.core.navigation.auth
import androidx.navigation3.runtime.NavKey
import kotlinx.serialization.Serializable
/**
* 认证模块路由
*
* @author Joker.X
*/
object AuthRoutes {
/**
* 登录页路由
*
* @author Joker.X
*/
@Serializable
data object Login : NavKey
}

View File

@@ -0,0 +1,87 @@
package com.joker.kit.core.navigation.demo
import com.joker.kit.core.navigation.navigate
/**
* Demo 模块导航封装
*
* 统一管理 Demo 模块页面跳转,避免在多个 ViewModel 中
* 编写重复的中转方法。
*
* @author Joker.X
*/
object DemoNavigator {
/**
* 跳转到网络状态示例页
*
* @author Joker.X
*/
fun toNetworkDemo() {
navigate(DemoRoutes.NetworkDemo)
}
/**
* 跳转到网络列表示例页
*
* @author Joker.X
*/
fun toNetworkListDemo() {
navigate(DemoRoutes.NetworkListDemo)
}
/**
* 跳转到数据库示例页
*
* @author Joker.X
*/
fun toDatabase() {
navigate(DemoRoutes.Database)
}
/**
* 跳转到本地存储示例页
*
* @author Joker.X
*/
fun toLocalStorage() {
navigate(DemoRoutes.LocalStorage)
}
/**
* 跳转到状态管理示例页
*
* @author Joker.X
*/
fun toStateManagement() {
navigate(DemoRoutes.StateManagement)
}
/**
* 跳转到网络请求示例页
*
* @author Joker.X
*/
fun toNetworkRequest() {
navigate(DemoRoutes.NetworkRequest)
}
/**
* 跳转到带参示例页
*
* @param goodsId 商品 ID
* @author Joker.X
*/
fun toNavigationWithArgs(goodsId: Long = 0) {
navigate(DemoRoutes.NavigationWithArgs(goodsId = goodsId))
}
/**
* 跳转到结果回传示例页
*
* @author Joker.X
*/
fun toNavigationResult() {
navigate(DemoRoutes.NavigationResult)
}
}

View File

@@ -1,11 +1,11 @@
package com.joker.kit.navigation.results
package com.joker.kit.core.navigation.demo
import com.joker.kit.navigation.NavigationResultKey
import com.joker.kit.core.navigation.NavigationResultKey
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
/**
* Demo 结果回传示例返回 DemoResult 数据类
* Demo 结果回传 Key
*
* @author Joker.X
*/
@@ -13,8 +13,8 @@ object DemoResultKey : NavigationResultKey<DemoResult> {
/**
* 序列化结果
*
* @param value 待序列化的结果对象
* @return 序列化后的字符串
* @param value 待序列化对象
* @return 序列化字符串
* @author Joker.X
*/
override fun serialize(value: DemoResult): Any = Json.encodeToString(value)
@@ -22,8 +22,8 @@ object DemoResultKey : NavigationResultKey<DemoResult> {
/**
* 反序列化结果
*
* @param raw 原始保存的数据
* @return 解析后的结果对象
* @param raw 原始
* @return 反序列化后的结果
* @author Joker.X
*/
override fun deserialize(raw: Any): DemoResult = Json.decodeFromString(raw as String)
@@ -32,12 +32,12 @@ object DemoResultKey : NavigationResultKey<DemoResult> {
/**
* Demo 结果数据
*
* @param id 结果标识
* @param id 结果 ID
* @param message 结果信息
* @author Joker.X
*/
@Serializable
data class DemoResult(
val id: Long,
val message: String
val message: String,
)

View File

@@ -0,0 +1,78 @@
package com.joker.kit.core.navigation.demo
import androidx.navigation3.runtime.NavKey
import kotlinx.serialization.Serializable
/**
* Demo 模块路由
*
* @author Joker.X
*/
object DemoRoutes {
/**
* Network Demo 示例页路由
*
* @author Joker.X
*/
@Serializable
data object NetworkDemo : NavKey
/**
* Network List Demo 示例页路由
*
* @author Joker.X
*/
@Serializable
data object NetworkListDemo : NavKey
/**
* 数据库示例页路由
*
* @author Joker.X
*/
@Serializable
data object Database : NavKey
/**
* 本地存储示例页路由
*
* @author Joker.X
*/
@Serializable
data object LocalStorage : NavKey
/**
* 状态管理示例页路由
*
* @author Joker.X
*/
@Serializable
data object StateManagement : NavKey
/**
* 通用网络请求示例页路由
*
* @author Joker.X
*/
@Serializable
data object NetworkRequest : NavKey
/**
* 带参跳转示例页路由
*
* @param goodsId 商品 ID
* @author Joker.X
*/
@Serializable
data class NavigationWithArgs(
val goodsId: Long,
) : NavKey
/**
* 结果回传示例页路由
*
* @author Joker.X
*/
@Serializable
data object NavigationResult : NavKey
}

View File

@@ -1,5 +1,6 @@
package com.joker.kit.navigation.routes
package com.joker.kit.core.navigation.main
import androidx.navigation3.runtime.NavKey
import kotlinx.serialization.Serializable
/**
@@ -8,13 +9,12 @@ import kotlinx.serialization.Serializable
* @author Joker.X
*/
object MainRoutes {
/**
* 主框架路由
*
* 应用的主框架包含底部导航栏
* 主框架路由
*
* @author Joker.X
*/
@Serializable
data object Main
data object Main : NavKey
}

View File

@@ -0,0 +1,20 @@
package com.joker.kit.core.navigation.user
import com.joker.kit.core.navigation.navigate
/**
* 用户模块导航封装
*
* @author Joker.X
*/
object UserNavigator {
/**
* 跳转到用户信息页
*
* @author Joker.X
*/
fun toUserInfo() {
navigate(UserRoutes.Info)
}
}

View File

@@ -0,0 +1,20 @@
package com.joker.kit.core.navigation.user
import androidx.navigation3.runtime.NavKey
import kotlinx.serialization.Serializable
/**
* 用户模块路由
*
* @author Joker.X
*/
object UserRoutes {
/**
* 用户信息页路由
*
* @author Joker.X
*/
@Serializable
data object Info : NavKey
}

View File

@@ -3,8 +3,8 @@ package com.joker.kit.core.network.di
import android.content.Context
import com.chuckerteam.chucker.api.ChuckerInterceptor
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import com.joker.kit.core.network.interceptor.AuthInterceptor
import com.joker.kit.BuildConfig
import com.joker.kit.core.network.interceptor.AuthInterceptor
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn

View File

@@ -6,7 +6,6 @@ import okhttp3.Interceptor
import okhttp3.Response
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.text.isNotBlank
/**
* 认证拦截器 - 添加授权头信息

View File

@@ -1,13 +1,13 @@
package com.joker.kit.core.state
import com.joker.kit.core.state.di.ApplicationScope
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
import javax.inject.Singleton
/**
* Demo 计数器状态

View File

@@ -5,8 +5,8 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton

View File

@@ -10,10 +10,10 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.joker.kit.core.base.state.BaseNetWorkListUiState
import com.joker.kit.core.ui.component.empty.EmptyData
import com.joker.kit.core.ui.component.empty.EmptyNetwork
import com.joker.kit.core.ui.component.loading.PageLoading
import com.joker.kit.core.base.state.BaseNetWorkListUiState
/**
* 基础网络列表视图组件

View File

@@ -10,9 +10,9 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.joker.kit.core.base.state.BaseNetWorkUiState
import com.joker.kit.core.ui.component.empty.EmptyNetwork
import com.joker.kit.core.ui.component.loading.PageLoading
import com.joker.kit.core.base.state.BaseNetWorkUiState
/**
* 基础网络视图组件,用于处理网络请求的三种状态:加载中、错误和成功

View File

@@ -244,7 +244,7 @@ fun AppText(
}
/**
* 通用文本组件 - AnnotatedString版本
* 通用文本组件 - AnnotatedString 重载
*
* @param text 富文本内容
* @param modifier 修饰符
@@ -369,7 +369,7 @@ fun AppText(
}
/**
* 基础文本组件 - 字符串版本
* 基础文本组件 - 字符串重载
*
* 该组件是对 Material3 Text 的轻量封装主要用于设置ContentColor
*
@@ -411,7 +411,7 @@ private fun BasicText(
}
/**
* 基础文本组件 - AnnotatedString版本
* 基础文本组件 - AnnotatedString 重载
*
* 该组件是对 Material3 Text 的轻量封装主要用于设置ContentColor
*
@@ -450,4 +450,4 @@ private fun BasicText(
onTextLayout = onTextLayout
)
}
}
}

View File

@@ -1,21 +1,17 @@
package com.joker.kit.feature.auth.navigation
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionScope
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation3.runtime.EntryProviderScope
import androidx.navigation3.runtime.NavKey
import com.joker.kit.core.navigation.auth.AuthRoutes
import com.joker.kit.feature.auth.view.LoginRoute
/**
* 认证模块导航图
*
* @param navController 导航控制器
* @param sharedTransitionScope 共享转场作用域
* @author Joker.X
*/
@OptIn(ExperimentalSharedTransitionApi::class)
fun NavGraphBuilder.authGraph(
navController: NavHostController,
sharedTransitionScope: SharedTransitionScope
) {
loginScreen(sharedTransitionScope)
fun EntryProviderScope<NavKey>.authGraph() {
entry<AuthRoutes.Login> {
LoginRoute()
}
}

View File

@@ -1,21 +0,0 @@
package com.joker.kit.feature.auth.navigation
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionScope
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import com.joker.kit.feature.auth.view.LoginRoute
import com.joker.kit.navigation.routes.AuthRoutes
/**
* 注册登录页路由
*
* @param sharedTransitionScope 共享转场作用域
* @author Joker.X
*/
@OptIn(ExperimentalSharedTransitionApi::class)
fun NavGraphBuilder.loginScreen(sharedTransitionScope: SharedTransitionScope) {
composable<AuthRoutes.Login> {
LoginRoute()
}
}

View File

@@ -11,6 +11,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import com.joker.kit.core.designsystem.theme.AppTheme
import com.joker.kit.core.designsystem.theme.SpacePaddingLarge
import com.joker.kit.core.navigation.navigateBack
import com.joker.kit.core.ui.component.scaffold.AppScaffold
import com.joker.kit.core.ui.component.text.AppText
import com.joker.kit.feature.auth.viewmodel.LoginViewModel
@@ -27,7 +28,7 @@ internal fun LoginRoute(
) {
LoginScreen(
onLoginClick = viewModel::login,
onBackClick = viewModel::navigateBack
onBackClick = ::navigateBack
)
}

View File

@@ -4,9 +4,9 @@ import androidx.lifecycle.viewModelScope
import com.joker.kit.core.base.viewmodel.BaseViewModel
import com.joker.kit.core.model.entity.Auth
import com.joker.kit.core.model.entity.User
import com.joker.kit.core.navigation.navigateBack
import com.joker.kit.core.state.UserState
import com.joker.kit.core.util.toast.ToastUtils
import com.joker.kit.navigation.AppNavigator
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
@@ -20,12 +20,8 @@ import javax.inject.Inject
*/
@HiltViewModel
class LoginViewModel @Inject constructor(
navigator: AppNavigator,
userState: UserState
) : BaseViewModel(
navigator = navigator,
userState = userState
) {
private val userState: UserState
) : BaseViewModel() {
/**
* 模拟登录:构造假的 Auth/User写入 UserState演示路由拦截放行。

View File

@@ -1,21 +0,0 @@
package com.joker.kit.feature.demo.navigation
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionScope
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import com.joker.kit.feature.demo.view.DatabaseRoute
import com.joker.kit.navigation.routes.DemoRoutes
/**
* 数据库示例页面导航
*
* @param sharedTransitionScope 共享转场作用域
* @author Joker.X
*/
@OptIn(ExperimentalSharedTransitionApi::class)
fun NavGraphBuilder.databaseScreen(sharedTransitionScope: SharedTransitionScope) {
composable<DemoRoutes.Database> {
DatabaseRoute()
}
}

View File

@@ -1,28 +1,45 @@
package com.joker.kit.feature.demo.navigation
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionScope
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation3.runtime.EntryProviderScope
import androidx.navigation3.runtime.NavKey
import com.joker.kit.core.navigation.demo.DemoRoutes
import com.joker.kit.feature.demo.view.DatabaseRoute
import com.joker.kit.feature.demo.view.LocalStorageRoute
import com.joker.kit.feature.demo.view.NavigationResultRoute
import com.joker.kit.feature.demo.view.NavigationWithArgsRoute
import com.joker.kit.feature.demo.view.NetworkDemoRoute
import com.joker.kit.feature.demo.view.NetworkListDemoRoute
import com.joker.kit.feature.demo.view.NetworkRequestRoute
import com.joker.kit.feature.demo.view.StateManagementRoute
/**
* Demo 模块导航图
*
* @param navController 导航控制器
* @param sharedTransitionScope 共享转场作用域
* @author Joker.X
*/
@OptIn(ExperimentalSharedTransitionApi::class)
fun NavGraphBuilder.demoGraph(
navController: NavHostController,
sharedTransitionScope: SharedTransitionScope
) {
networkDemoScreen(sharedTransitionScope)
networkListDemoScreen(sharedTransitionScope)
databaseScreen(sharedTransitionScope)
localStorageScreen(sharedTransitionScope)
stateManagementScreen(sharedTransitionScope)
networkRequestScreen(sharedTransitionScope)
navigationWithArgsScreen(sharedTransitionScope)
navigationResultScreen(sharedTransitionScope)
fun EntryProviderScope<NavKey>.demoGraph() {
entry<DemoRoutes.NetworkDemo> {
NetworkDemoRoute()
}
entry<DemoRoutes.NetworkListDemo> {
NetworkListDemoRoute()
}
entry<DemoRoutes.Database> {
DatabaseRoute()
}
entry<DemoRoutes.LocalStorage> {
LocalStorageRoute()
}
entry<DemoRoutes.StateManagement> {
StateManagementRoute()
}
entry<DemoRoutes.NetworkRequest> {
NetworkRequestRoute()
}
entry<DemoRoutes.NavigationWithArgs> { key ->
NavigationWithArgsRoute(navKey = key)
}
entry<DemoRoutes.NavigationResult> {
NavigationResultRoute()
}
}

View File

@@ -1,21 +0,0 @@
package com.joker.kit.feature.demo.navigation
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionScope
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import com.joker.kit.feature.demo.view.LocalStorageRoute
import com.joker.kit.navigation.routes.DemoRoutes
/**
* 本地存储示例页面导航
*
* @param sharedTransitionScope 共享转场作用域
* @author Joker.X
*/
@OptIn(ExperimentalSharedTransitionApi::class)
fun NavGraphBuilder.localStorageScreen(sharedTransitionScope: SharedTransitionScope) {
composable<DemoRoutes.LocalStorage> {
LocalStorageRoute()
}
}

View File

@@ -1,21 +0,0 @@
package com.joker.kit.feature.demo.navigation
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionScope
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import com.joker.kit.feature.demo.view.NavigationResultRoute
import com.joker.kit.navigation.routes.DemoRoutes
/**
* 结果回传示例页面导航
*
* @param sharedTransitionScope 共享转场作用域
* @author Joker.X
*/
@OptIn(ExperimentalSharedTransitionApi::class)
fun NavGraphBuilder.navigationResultScreen(sharedTransitionScope: SharedTransitionScope) {
composable<DemoRoutes.NavigationResult> {
NavigationResultRoute()
}
}

View File

@@ -1,21 +0,0 @@
package com.joker.kit.feature.demo.navigation
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionScope
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import com.joker.kit.feature.demo.view.NavigationWithArgsRoute
import com.joker.kit.navigation.routes.DemoRoutes
/**
* 带参跳转示例页面导航
*
* @param sharedTransitionScope 共享转场作用域
* @author Joker.X
*/
@OptIn(ExperimentalSharedTransitionApi::class)
fun NavGraphBuilder.navigationWithArgsScreen(sharedTransitionScope: SharedTransitionScope) {
composable<DemoRoutes.NavigationWithArgs> {
NavigationWithArgsRoute()
}
}

View File

@@ -1,21 +0,0 @@
package com.joker.kit.feature.demo.navigation
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionScope
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import com.joker.kit.feature.demo.view.NetworkDemoRoute
import com.joker.kit.navigation.routes.DemoRoutes
/**
* Network Demo 页面导航
*
* @param sharedTransitionScope 共享转场作用域
* @author Joker.X
*/
@OptIn(ExperimentalSharedTransitionApi::class)
fun NavGraphBuilder.networkDemoScreen(sharedTransitionScope: SharedTransitionScope) {
composable<DemoRoutes.NetworkDemo> {
NetworkDemoRoute()
}
}

View File

@@ -1,21 +0,0 @@
package com.joker.kit.feature.demo.navigation
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionScope
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import com.joker.kit.feature.demo.view.NetworkListDemoRoute
import com.joker.kit.navigation.routes.DemoRoutes
/**
* Network List Demo 页面导航
*
* @param sharedTransitionScope 共享转场作用域
* @author Joker.X
*/
@OptIn(ExperimentalSharedTransitionApi::class)
fun NavGraphBuilder.networkListDemoScreen(sharedTransitionScope: SharedTransitionScope) {
composable<DemoRoutes.NetworkListDemo> {
NetworkListDemoRoute()
}
}

View File

@@ -1,21 +0,0 @@
package com.joker.kit.feature.demo.navigation
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionScope
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import com.joker.kit.feature.demo.view.NetworkRequestRoute
import com.joker.kit.navigation.routes.DemoRoutes
/**
* 网络请求示例页面导航
*
* @param sharedTransitionScope 共享转场作用域
* @author Joker.X
*/
@OptIn(ExperimentalSharedTransitionApi::class)
fun NavGraphBuilder.networkRequestScreen(sharedTransitionScope: SharedTransitionScope) {
composable<DemoRoutes.NetworkRequest> {
NetworkRequestRoute()
}
}

View File

@@ -1,21 +0,0 @@
package com.joker.kit.feature.demo.navigation
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionScope
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import com.joker.kit.feature.demo.view.StateManagementRoute
import com.joker.kit.navigation.routes.DemoRoutes
/**
* 状态管理示例页面导航
*
* @param sharedTransitionScope 共享转场作用域
* @author Joker.X
*/
@OptIn(ExperimentalSharedTransitionApi::class)
fun NavGraphBuilder.stateManagementScreen(sharedTransitionScope: SharedTransitionScope) {
composable<DemoRoutes.StateManagement> {
StateManagementRoute()
}
}

View File

@@ -39,6 +39,7 @@ import com.joker.kit.core.designsystem.theme.SpacePaddingMedium
import com.joker.kit.core.designsystem.theme.SpaceVerticalLarge
import com.joker.kit.core.designsystem.theme.SpaceVerticalMedium
import com.joker.kit.core.designsystem.theme.SpaceVerticalSmall
import com.joker.kit.core.navigation.navigateBack
import com.joker.kit.core.ui.component.divider.Divider
import com.joker.kit.core.ui.component.scaffold.AppScaffold
import com.joker.kit.core.ui.component.text.AppText
@@ -75,7 +76,7 @@ internal fun DatabaseRoute(
onAddClick = viewModel::addItem,
onDeleteItem = viewModel::deleteItem,
onClearAll = viewModel::clearAll,
onBackClick = viewModel::navigateBack
onBackClick = ::navigateBack
)
}

View File

@@ -11,8 +11,8 @@ import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.Divider
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
@@ -32,6 +32,7 @@ import com.joker.kit.core.designsystem.theme.SpacePaddingMedium
import com.joker.kit.core.designsystem.theme.SpaceVerticalLarge
import com.joker.kit.core.designsystem.theme.SpaceVerticalMedium
import com.joker.kit.core.model.entity.User
import com.joker.kit.core.navigation.navigateBack
import com.joker.kit.core.ui.component.scaffold.AppScaffold
import com.joker.kit.core.ui.component.text.AppText
import com.joker.kit.core.ui.component.text.TextSize
@@ -68,7 +69,7 @@ internal fun LocalStorageRoute(
onSaveUser = viewModel::saveUser,
onClearUser = viewModel::clearUser,
onReloadUser = viewModel::loadUser,
onBackClick = viewModel::navigateBack
onBackClick = ::navigateBack
)
}
@@ -253,7 +254,7 @@ private fun UserCard(
}
}
Divider()
HorizontalDivider()
val userText = user?.let {
val name = it.nickName ?: "未设置昵称"

View File

@@ -11,6 +11,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import com.joker.kit.core.designsystem.theme.AppTheme
import com.joker.kit.core.designsystem.theme.SpacePaddingLarge
import com.joker.kit.core.navigation.navigateBack
import com.joker.kit.core.ui.component.scaffold.AppScaffold
import com.joker.kit.core.ui.component.text.AppText
import com.joker.kit.feature.demo.viewmodel.NavigationResultViewModel
@@ -26,7 +27,7 @@ internal fun NavigationResultRoute(
viewModel: NavigationResultViewModel = hiltViewModel()
) {
NavigationResultScreen(
onBackClick = viewModel::navigateBack,
onBackClick = ::navigateBack,
onSendResult = viewModel::sendResultAndBack
)
}

View File

@@ -5,6 +5,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import com.joker.kit.core.designsystem.theme.AppTheme
import com.joker.kit.core.navigation.demo.DemoRoutes
import com.joker.kit.core.navigation.navigateBack
import com.joker.kit.core.ui.component.scaffold.AppScaffold
import com.joker.kit.core.ui.component.text.AppText
import com.joker.kit.feature.demo.viewmodel.NavigationWithArgsViewModel
@@ -12,16 +14,22 @@ import com.joker.kit.feature.demo.viewmodel.NavigationWithArgsViewModel
/**
* 带参跳转示例路由
*
* @param navKey 导航参数
* @param viewModel Hilt 注入的 NavigationWithArgsViewModel
* @author Joker.X
*/
@Composable
internal fun NavigationWithArgsRoute(
viewModel: NavigationWithArgsViewModel = hiltViewModel()
navKey: DemoRoutes.NavigationWithArgs,
viewModel: NavigationWithArgsViewModel = hiltViewModel<NavigationWithArgsViewModel, NavigationWithArgsViewModel.Factory>(
creationCallback = { factory ->
factory.create(navKey)
}
)
) {
NavigationWithArgsScreen(
goodsId = viewModel.goodsId,
onBackClick = viewModel::navigateBack
onBackClick = ::navigateBack
)
}

View File

@@ -13,6 +13,7 @@ import com.joker.kit.core.base.state.BaseNetWorkUiState
import com.joker.kit.core.designsystem.theme.AppTheme
import com.joker.kit.core.designsystem.theme.SpacePaddingMedium
import com.joker.kit.core.model.entity.Goods
import com.joker.kit.core.navigation.navigateBack
import com.joker.kit.core.ui.component.network.BaseNetWorkView
import com.joker.kit.core.ui.component.scaffold.AppScaffold
import com.joker.kit.core.ui.component.text.AppText
@@ -33,7 +34,7 @@ internal fun NetworkDemoRoute(
NetworkDemoScreen(
uiState = uiState,
onBackClick = viewModel::navigateBack,
onBackClick = ::navigateBack,
onRetry = viewModel::retryRequest
)
}

View File

@@ -15,6 +15,7 @@ import com.joker.kit.core.base.state.LoadMoreState
import com.joker.kit.core.designsystem.theme.AppTheme
import com.joker.kit.core.designsystem.theme.ShapeMedium
import com.joker.kit.core.model.entity.Goods
import com.joker.kit.core.navigation.navigateBack
import com.joker.kit.core.ui.component.network.BaseNetWorkListView
import com.joker.kit.core.ui.component.refresh.RefreshLayout
import com.joker.kit.core.ui.component.scaffold.AppScaffold
@@ -48,7 +49,7 @@ internal fun NetworkListDemoRoute(
onRefresh = viewModel::onRefresh,
onLoadMore = viewModel::onLoadMore,
shouldTriggerLoadMore = viewModel::shouldTriggerLoadMore,
onBackClick = viewModel::navigateBack,
onBackClick = ::navigateBack,
onRetry = viewModel::retryRequest,
)
}

View File

@@ -18,6 +18,7 @@ import com.joker.kit.core.designsystem.theme.AppTheme
import com.joker.kit.core.designsystem.theme.SpacePaddingLarge
import com.joker.kit.core.designsystem.theme.SpacePaddingMedium
import com.joker.kit.core.model.entity.Goods
import com.joker.kit.core.navigation.navigateBack
import com.joker.kit.core.ui.component.scaffold.AppScaffold
import com.joker.kit.core.ui.component.text.AppText
import com.joker.kit.feature.demo.viewmodel.NetworkRequestViewModel
@@ -37,7 +38,7 @@ internal fun NetworkRequestRoute(
NetworkRequestScreen(
goods = goods,
onBackClick = viewModel::navigateBack,
onBackClick = ::navigateBack,
onRequestClick = viewModel::onRequestClick
)
}

View File

@@ -27,6 +27,7 @@ import com.joker.kit.core.designsystem.theme.SpacePaddingLarge
import com.joker.kit.core.designsystem.theme.SpacePaddingMedium
import com.joker.kit.core.designsystem.theme.SpaceVerticalLarge
import com.joker.kit.core.designsystem.theme.SpaceVerticalMedium
import com.joker.kit.core.navigation.navigateBack
import com.joker.kit.core.ui.component.scaffold.AppScaffold
import com.joker.kit.core.ui.component.text.AppText
import com.joker.kit.core.ui.component.text.TextSize
@@ -51,7 +52,7 @@ internal fun StateManagementRoute(
onIncrease = viewModel::increase,
onDecrease = viewModel::decrease,
onReset = viewModel::reset,
onBackClick = viewModel::navigateBack
onBackClick = ::navigateBack
)
}

View File

@@ -4,8 +4,6 @@ import androidx.lifecycle.viewModelScope
import com.joker.kit.core.base.viewmodel.BaseViewModel
import com.joker.kit.core.data.repository.DemoRepository
import com.joker.kit.core.database.entity.DemoEntity
import com.joker.kit.core.state.UserState
import com.joker.kit.navigation.AppNavigator
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
@@ -18,17 +16,13 @@ import javax.inject.Inject
/**
* 数据库示例页 ViewModel
*
* @param navigator 导航管理器
* @param userState 用户状态管理
* @param demoRepository Demo 仓库,封装 DemoDataSource 的增删改查
* @author Joker.X
*/
@HiltViewModel
class DatabaseViewModel @Inject constructor(
navigator: AppNavigator,
userState: UserState,
private val demoRepository: DemoRepository
) : BaseViewModel(navigator, userState) {
) : BaseViewModel() {
/** 标题输入 */
private val _title = MutableStateFlow("")

View File

@@ -4,31 +4,25 @@ import androidx.lifecycle.viewModelScope
import com.joker.kit.core.base.viewmodel.BaseViewModel
import com.joker.kit.core.data.repository.UserInfoStoreRepository
import com.joker.kit.core.model.entity.User
import com.joker.kit.core.state.UserState
import com.joker.kit.navigation.AppNavigator
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
/**
* 本地存储示例页 ViewModel
*
* 通过本地仓库 (UserInfoStoreRepository) 演示“用户信息” 的保存 / 读取 / 清除。
*
* @param navigator 导航管理器
* @param userState 用户状态管理
* @param userInfoStoreRepository 用户信息本地存储仓库
* @author Joker.X
*/
@HiltViewModel
class LocalStorageViewModel @Inject constructor(
navigator: AppNavigator,
userState: UserState,
private val userInfoStoreRepository: UserInfoStoreRepository
) : BaseViewModel(navigator, userState) {
) : BaseViewModel() {
/** 用户 id 输入 */
private val _userId = MutableStateFlow("1")

View File

@@ -1,28 +1,20 @@
package com.joker.kit.feature.demo.viewmodel
import com.joker.kit.core.base.viewmodel.BaseViewModel
import com.joker.kit.core.state.UserState
import com.joker.kit.navigation.AppNavigator
import com.joker.kit.navigation.results.DemoResult
import com.joker.kit.navigation.results.DemoResultKey
import com.joker.kit.core.navigation.demo.DemoResult
import com.joker.kit.core.navigation.demo.DemoResultKey
import com.joker.kit.core.navigation.popBackStackWithResult
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
/**
* 结果回传示例页 ViewModel
*
* @param navigator 导航管理器
* @param userState 用户状态
* @author Joker.X
*/
@HiltViewModel
class NavigationResultViewModel @Inject constructor(
navigator: AppNavigator,
userState: UserState
) : BaseViewModel(
navigator = navigator,
userState = userState
) {
) : BaseViewModel() {
/**
* 回传结果并返回上一页
*

View File

@@ -1,44 +1,43 @@
package com.joker.kit.feature.demo.viewmodel
import androidx.lifecycle.SavedStateHandle
import androidx.navigation.toRoute
import com.joker.kit.core.base.viewmodel.BaseViewModel
import com.joker.kit.core.state.UserState
import com.joker.kit.navigation.AppNavigator
import com.joker.kit.navigation.routes.DemoRoutes
import com.joker.kit.core.navigation.demo.DemoRoutes
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
/**
* 带参跳转示例页 ViewModel
*
* @param navigator 导航管理器
* @param userState 用户状态
* @param savedStateHandle 路由参数存储
* @param navKey 导航参数
* @author Joker.X
*/
@HiltViewModel
class NavigationWithArgsViewModel @Inject constructor(
navigator: AppNavigator,
userState: UserState,
savedStateHandle: SavedStateHandle
) : BaseViewModel(
navigator = navigator,
userState = userState
) {
@HiltViewModel(assistedFactory = NavigationWithArgsViewModel.Factory::class)
class NavigationWithArgsViewModel @AssistedInject constructor(
@Assisted private val navKey: DemoRoutes.NavigationWithArgs
) : BaseViewModel() {
/**
* 路由参数
* 当前商品 ID用于请求商品详情
*
* @return 路由解析结果
* @author Joker.X
*/
private val route = savedStateHandle.toRoute<DemoRoutes.NavigationWithArgs>()
val goodsId: Long = navKey.goodsId
/**
* 商品ID
* Assisted Factory
*
* @return 传递的商品 ID
* @author Joker.X
*/
val goodsId: Long = route.goodsId
@AssistedFactory
interface Factory {
/**
* 创建 ViewModel 实例
*
* @param navKey 导航参数
* @return ViewModel 实例
* @author Joker.X
*/
fun create(navKey: DemoRoutes.NavigationWithArgs): NavigationWithArgsViewModel
}
}

View File

@@ -4,8 +4,6 @@ import com.joker.kit.core.base.viewmodel.BaseNetWorkViewModel
import com.joker.kit.core.data.repository.GoodsRepository
import com.joker.kit.core.model.entity.Goods
import com.joker.kit.core.model.network.NetworkResponse
import com.joker.kit.core.state.UserState
import com.joker.kit.navigation.AppNavigator
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
@@ -13,17 +11,13 @@ import javax.inject.Inject
/**
* 网络状态 Demo 页面 ViewModel
*
* @param navigator 导航管理器
* @param userState 用户状态管理
* @param goodsRepository 商品数据仓库
* @author Joker.X
*/
@HiltViewModel
class NetworkDemoViewModel @Inject constructor(
navigator: AppNavigator,
userState: UserState,
private val goodsRepository: GoodsRepository
) : BaseNetWorkViewModel<Goods>(navigator, userState) {
) : BaseNetWorkViewModel<Goods>() {
init {
super.executeRequest()

View File

@@ -6,8 +6,6 @@ import com.joker.kit.core.model.entity.Goods
import com.joker.kit.core.model.network.NetworkPageData
import com.joker.kit.core.model.network.NetworkResponse
import com.joker.kit.core.model.request.GoodsSearchRequest
import com.joker.kit.core.state.UserState
import com.joker.kit.navigation.AppNavigator
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
@@ -15,19 +13,13 @@ import javax.inject.Inject
/**
* Network List Demo 示例页 ViewModel
*
* @param navigator 导航器
* @param userState 用户状态管理
* @param goodsRepository 商品数据仓库
* @author Joker.X
*/
@HiltViewModel
class NetworkListDemoViewModel @Inject constructor(
navigator: AppNavigator,
userState: UserState,
private val goodsRepository: GoodsRepository
) : BaseNetWorkListViewModel<Goods>(
navigator = navigator,
userState = userState
) {
init {

View File

@@ -6,8 +6,6 @@ import com.joker.kit.core.data.repository.GoodsRepository
import com.joker.kit.core.model.entity.Goods
import com.joker.kit.core.result.ResultHandler
import com.joker.kit.core.result.asResult
import com.joker.kit.core.state.UserState
import com.joker.kit.navigation.AppNavigator
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -17,20 +15,13 @@ import javax.inject.Inject
/**
* 网络请求示例页 ViewModel
*
* @param navigator 导航管理器
* @param userState 用户状态
* @param goodsRepository 商品仓库
* @author Joker.X
*/
@HiltViewModel
class NetworkRequestViewModel @Inject constructor(
navigator: AppNavigator,
userState: UserState,
private val goodsRepository: GoodsRepository
) : BaseViewModel(
navigator = navigator,
userState = userState
) {
) : BaseViewModel() {
/**
* 商品信息

View File

@@ -2,8 +2,6 @@ package com.joker.kit.feature.demo.viewmodel
import com.joker.kit.core.base.viewmodel.BaseViewModel
import com.joker.kit.core.state.DemoCounterState
import com.joker.kit.core.state.UserState
import com.joker.kit.navigation.AppNavigator
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
@@ -11,17 +9,13 @@ import javax.inject.Inject
/**
* 状态管理示例页 ViewModel
*
* @param navigator 应用导航器
* @param userState 全局用户状态
* @param counterState 计数器状态
* @author Joker.X
*/
@HiltViewModel
class StateManagementViewModel @Inject constructor(
navigator: AppNavigator,
userState: UserState,
private val counterState: DemoCounterState
) : BaseViewModel(navigator, userState) {
) : BaseViewModel() {
/**
* 对外暴露的计数器 StateFlow

View File

@@ -1,11 +1,11 @@
package com.joker.kit.feature.main.component
import androidx.compose.foundation.clickable
import androidx.compose.material3.ListItem
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.foundation.clickable
import com.joker.kit.core.designsystem.theme.AppTheme
import com.joker.kit.core.designsystem.theme.ShapeMedium
import com.joker.kit.core.ui.component.text.AppText
@@ -53,7 +53,7 @@ private fun DemoCardPreview() {
info = DemoCardInfo(
title = "示例组件",
description = "预览展示 Demo 卡片默认样式。",
route = null
navigateAction = null
)
)
}

View File

@@ -1,8 +1,8 @@
package com.joker.kit.feature.main.data
import com.joker.kit.core.navigation.demo.DemoNavigator
import com.joker.kit.core.navigation.user.UserNavigator
import com.joker.kit.feature.main.model.DemoCardInfo
import com.joker.kit.navigation.routes.DemoRoutes
import com.joker.kit.navigation.routes.UserRoutes
/**
* Demo 卡片静态数据源
@@ -10,55 +10,64 @@ import com.joker.kit.navigation.routes.UserRoutes
* @author Joker.X
*/
object DemoCardData {
/**
* Core 页签下的演示卡片
*
* @author Joker.X
*/
val coreCards: List<DemoCardInfo> = listOf(
DemoCardInfo(
title = "Network Demo",
description = "网络状态切换,包含加载、错误、重试等流程。",
route = DemoRoutes.NetworkDemo
navigateAction = { DemoNavigator.toNetworkDemo() }
),
DemoCardInfo(
title = "Network List Demo",
description = "下拉刷新与分页加载的统一列表模板,内置空状态与重试。",
route = DemoRoutes.NetworkListDemo
navigateAction = { DemoNavigator.toNetworkListDemo() }
),
DemoCardInfo(
title = "数据库",
description = "Room 的增删改查示例,含简单的列表展示与数据观察。",
route = DemoRoutes.Database
navigateAction = { DemoNavigator.toDatabase() }
),
DemoCardInfo(
title = "本地存储",
description = "DataStore / MMKV 的写入与清除示例,演示单值增删改查。",
route = DemoRoutes.LocalStorage
navigateAction = { DemoNavigator.toLocalStorage() }
),
DemoCardInfo(
title = "状态管理",
description = "全局 DemoCounterState 计数器共享示例,展示跨页面 StateFlow 同步。",
route = DemoRoutes.StateManagement
navigateAction = { DemoNavigator.toStateManagement() }
),
DemoCardInfo(
title = "网络请求",
description = "结合 ResultHandler 的通用接口请求、加载状态与错误提示。",
route = DemoRoutes.NetworkRequest
navigateAction = { DemoNavigator.toNetworkRequest() }
)
)
/**
* Navigation 页签下的演示卡片
*
* @author Joker.X
*/
val navigationCards: List<DemoCardInfo> = listOf(
DemoCardInfo(
title = "带参跳转",
description = "类型安全路由参数,包含必填/可选参数与目标页接收方式。",
route = DemoRoutes.NavigationWithArgs(123)
navigateAction = { DemoNavigator.toNavigationWithArgs(goodsId = 123) }
),
DemoCardInfo(
title = "结果回传",
description = "NavigationResultKey 返回数据,包含刷新信号与数据实体回传。",
route = DemoRoutes.NavigationResult
navigateAction = { DemoNavigator.toNavigationResult() }
),
DemoCardInfo(
title = "导航拦截",
description = "登录拦截流程:未登录跳登录页,登录成功后才能进入用户详情。",
route = UserRoutes.Info
navigateAction = { UserNavigator.toUserInfo() }
)
)
}

View File

@@ -1,15 +1,22 @@
package com.joker.kit.feature.main.model
/**
* Demo 卡片导航动作
*
* @author Joker.X
*/
typealias DemoCardNavigateAction = () -> Unit
/**
* Demo 卡片信息
*
* @param title 标题
* @param description 描述内容
* @param route 跳转路由
* @param navigateAction 跳转动作
* @author Joker.X
*/
data class DemoCardInfo(
val title: String,
val description: String,
val route: Any? = null
val navigateAction: DemoCardNavigateAction? = null
)

View File

@@ -1,22 +1,17 @@
package com.joker.kit.feature.main.navigation
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionScope
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation3.runtime.EntryProviderScope
import androidx.navigation3.runtime.NavKey
import com.joker.kit.core.navigation.main.MainRoutes
import com.joker.kit.feature.main.view.MainRoute
/**
* 主模块导航图
*
* @param navController 导航控制器
* @param sharedTransitionScope 共享转场作用域
* @author Joker.X
*/
@OptIn(ExperimentalSharedTransitionApi::class)
fun NavGraphBuilder.mainGraph(
navController: NavHostController,
sharedTransitionScope: SharedTransitionScope
) {
// 只调用页面级导航函数,不包含其他逻辑
mainScreen(navController, sharedTransitionScope)
fun EntryProviderScope<NavKey>.mainGraph() {
entry<MainRoutes.Main> {
MainRoute()
}
}

View File

@@ -1,26 +0,0 @@
package com.joker.kit.feature.main.navigation
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionScope
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
import com.joker.kit.feature.main.view.MainRoute
import com.joker.kit.navigation.routes.MainRoutes
/**
* 注册主页面路由
*
* @param navController NavHostController
* @param sharedTransitionScope 共享转场作用域
* @author Joker.X
*/
@OptIn(ExperimentalSharedTransitionApi::class)
fun NavGraphBuilder.mainScreen(
navController: NavHostController,
sharedTransitionScope: SharedTransitionScope
) {
composable<MainRoutes.Main> {
MainRoute(navController = navController)
}
}

View File

@@ -40,7 +40,7 @@ internal fun CoreDemoRoute(
CoreDemoScreen(
cards = cards,
counter = count,
onCardClick = viewModel::onCardClick
onCardClick = { info -> info.navigateAction?.invoke() }
)
}

View File

@@ -19,12 +19,10 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavController
import com.joker.kit.core.designsystem.theme.AppTheme
import com.joker.kit.core.ui.component.text.AppText
import com.joker.kit.core.ui.component.text.TextSize
@@ -40,14 +38,12 @@ import com.joker.kit.feature.main.viewmodel.MainViewModel
*/
@Composable
internal fun MainRoute(
viewModel: MainViewModel = hiltViewModel(),
navController: NavController
viewModel: MainViewModel = hiltViewModel()
) {
val uiState by viewModel.uiState.collectAsState()
MainScreen(
uiState = uiState,
onTabSelected = viewModel::selectTab,
navController = navController
onTabSelected = viewModel::selectTab
)
}
@@ -62,13 +58,11 @@ internal fun MainRoute(
@Composable
internal fun MainScreen(
uiState: MainUiState = MainUiState(),
onTabSelected: (MainTab) -> Unit,
navController: NavController = NavController(LocalContext.current)
onTabSelected: (MainTab) -> Unit
) {
MainScreenContent(
uiState = uiState,
onTabSelected = onTabSelected,
navController = navController
onTabSelected = onTabSelected
)
}
@@ -77,15 +71,13 @@ internal fun MainScreen(
*
* @param uiState UI 状态
* @param onTabSelected Tab 切换回调
* @param navController 导航控制器
* @author Joker.X
*/
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@Composable
private fun MainScreenContent(
uiState: MainUiState,
onTabSelected: (MainTab) -> Unit,
navController: NavController
onTabSelected: (MainTab) -> Unit
) {
val pagerState = rememberPagerState(pageCount = { MainTab.allTabs.size })
val currentPage = pagerState.currentPage
@@ -121,9 +113,7 @@ private fun MainScreenContent(
) { page ->
when (MainTab.fromIndex(page)) {
MainTab.Core -> CoreDemoRoute()
MainTab.Navigation -> NavigationDemoRoute(
navController = navController
)
MainTab.Navigation -> NavigationDemoRoute()
}
}
}

View File

@@ -16,30 +16,25 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavController
import com.joker.kit.core.designsystem.theme.AppTheme
import com.joker.kit.core.designsystem.theme.SpacePaddingLarge
import com.joker.kit.core.designsystem.theme.SpaceVerticalMedium
import com.joker.kit.core.navigation.demo.DemoResult
import com.joker.kit.core.ui.component.text.AppText
import com.joker.kit.feature.main.component.DemoCard
import com.joker.kit.feature.main.data.DemoCardData
import com.joker.kit.feature.main.model.DemoCardInfo
import com.joker.kit.feature.main.viewmodel.NavigationDemoViewModel
import com.joker.kit.navigation.extension.observeResult
import com.joker.kit.navigation.results.DemoResult
import com.joker.kit.navigation.results.DemoResultKey
/**
* Navigation Demo 路由
*
* @param viewModel Navigation Demo ViewModel
* @param navController 用于监听结果的 NavController
* @author Joker.X
*/
@Composable
internal fun NavigationDemoRoute(
viewModel: NavigationDemoViewModel = hiltViewModel(),
navController: NavController
viewModel: NavigationDemoViewModel = hiltViewModel()
) {
val cards by viewModel.cards.collectAsState()
val isLoggedIn by viewModel.isLoggedIn.collectAsState()
@@ -49,12 +44,8 @@ internal fun NavigationDemoRoute(
cards = cards,
isLoggedIn = isLoggedIn,
demoResult = demoResult,
onCardClick = viewModel::onCardClick
onCardClick = { info -> info.navigateAction?.invoke() }
)
navController.observeResult(DemoResultKey) { result ->
viewModel.onResultReceived(result)
}
}
/**

View File

@@ -1,11 +1,9 @@
package com.joker.kit.feature.main.viewmodel
import com.joker.kit.core.base.viewmodel.BaseViewModel
import androidx.lifecycle.ViewModel
import com.joker.kit.core.state.DemoCounterState
import com.joker.kit.core.state.UserState
import com.joker.kit.feature.main.data.DemoCardData
import com.joker.kit.feature.main.model.DemoCardInfo
import com.joker.kit.navigation.AppNavigator
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -15,22 +13,25 @@ import javax.inject.Inject
/**
* Core Demo ViewModel
*
* @param navigator 导航管理器
* @param userState 用户状态
* @param counterState 计数器状态
* @author Joker.X
*/
@HiltViewModel
class CoreDemoViewModel @Inject constructor(
navigator: AppNavigator,
userState: UserState,
counterState: DemoCounterState
) : BaseViewModel(
navigator = navigator,
userState = userState
) {
) : ViewModel() {
/**
* Demo 卡片源数据
*
* @author Joker.X
*/
private val _cards = MutableStateFlow(DemoCardData.coreCards)
/**
* Demo 卡片状态流
*
* @author Joker.X
*/
val cards: StateFlow<List<DemoCardInfo>> = _cards.asStateFlow()
/**
@@ -40,14 +41,4 @@ class CoreDemoViewModel @Inject constructor(
* @author Joker.X
*/
val count: StateFlow<Int> = counterState.count
/**
* 处理卡片点击
*
* @param info 被点击的卡片信息
* @author Joker.X
*/
fun onCardClick(info: DemoCardInfo) {
info.route?.let { navigate(it) }
}
}

View File

@@ -1,8 +1,6 @@
package com.joker.kit.feature.main.viewmodel
import com.joker.kit.core.base.viewmodel.BaseViewModel
import com.joker.kit.core.state.UserState
import com.joker.kit.navigation.AppNavigator
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -12,18 +10,11 @@ import javax.inject.Inject
/**
* 主界面 ViewModel
*
* @param navigator 导航控制器
* @param userState 全局用户状态
* @author Joker.X
*/
@HiltViewModel
class MainViewModel @Inject constructor(
navigator: AppNavigator,
userState: UserState
) : BaseViewModel(
navigator = navigator,
userState = userState
) {
) : BaseViewModel() {
private val _uiState = MutableStateFlow(MainUiState())
val uiState: StateFlow<MainUiState> = _uiState.asStateFlow()

View File

@@ -1,56 +1,63 @@
package com.joker.kit.feature.main.viewmodel
import androidx.lifecycle.viewModelScope
import com.joker.kit.core.base.viewmodel.BaseViewModel
import com.joker.kit.core.navigation.demo.DemoResult
import com.joker.kit.core.navigation.demo.DemoResultKey
import com.joker.kit.core.navigation.resultEvents
import com.joker.kit.core.state.UserState
import com.joker.kit.feature.main.data.DemoCardData
import com.joker.kit.feature.main.model.DemoCardInfo
import com.joker.kit.navigation.AppNavigator
import com.joker.kit.navigation.results.DemoResult
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
/**
* Navigation Demo ViewModel
*
* @param navigator 导航管理器
* @param userState 用户状态
* @author Joker.X
*/
@HiltViewModel
class NavigationDemoViewModel @Inject constructor(
navigator: AppNavigator,
userState: UserState,
) : BaseViewModel(
navigator = navigator,
userState = userState
) {
private val userState: UserState,
) : BaseViewModel() {
/**
* 初始化时监听结果回传
*
* @author Joker.X
*/
init {
observeDemoResult()
}
/**
* Navigation 卡片源数据
*/
private val _cards = MutableStateFlow(DemoCardData.navigationCards)
/**
* Navigation 卡片状态流
*/
val cards: StateFlow<List<DemoCardInfo>> = _cards.asStateFlow()
/**
* 全局登录状态
*
* @return 登录状态流
* @author Joker.X
*/
val isLoggedIn: StateFlow<Boolean> = userState.isLoggedIn
/**
* Demo 结果状态源
*/
private val _demoResult = MutableStateFlow<DemoResult?>(null)
val demoResult: StateFlow<DemoResult?> = _demoResult.asStateFlow()
/**
* 处理卡片点击
*
* @param info 卡片信息
* @author Joker.X
* Demo 结果状态流
*/
fun onCardClick(info: DemoCardInfo) {
info.route?.let { navigate(it) }
}
val demoResult: StateFlow<DemoResult?> = _demoResult.asStateFlow()
/**
* 处理回传结果
@@ -61,4 +68,17 @@ class NavigationDemoViewModel @Inject constructor(
fun onResultReceived(result: DemoResult) {
_demoResult.value = result
}
/**
* 监听结果回传事件
*
* @author Joker.X
*/
private fun observeDemoResult() {
viewModelScope.launch {
resultEvents(DemoResultKey).collect { result ->
onResultReceived(result)
}
}
}
}

View File

@@ -1,21 +1,17 @@
package com.joker.kit.feature.user.navigation
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionScope
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation3.runtime.EntryProviderScope
import androidx.navigation3.runtime.NavKey
import com.joker.kit.core.navigation.user.UserRoutes
import com.joker.kit.feature.user.view.UserInfoRoute
/**
* 用户模块导航图
*
* @param navController 导航控制器
* @param sharedTransitionScope 共享转场作用域
* @author Joker.X
*/
@OptIn(ExperimentalSharedTransitionApi::class)
fun NavGraphBuilder.userGraph(
navController: NavHostController,
sharedTransitionScope: SharedTransitionScope
) {
userInfoScreen(sharedTransitionScope)
fun EntryProviderScope<NavKey>.userGraph() {
entry<UserRoutes.Info> {
UserInfoRoute()
}
}

View File

@@ -1,21 +0,0 @@
package com.joker.kit.feature.user.navigation
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionScope
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import com.joker.kit.feature.user.view.UserInfoRoute
import com.joker.kit.navigation.routes.UserRoutes
/**
* 注册用户信息页路由
*
* @param sharedTransitionScope 共享转场作用域
* @author Joker.X
*/
@OptIn(ExperimentalSharedTransitionApi::class)
fun NavGraphBuilder.userInfoScreen(sharedTransitionScope: SharedTransitionScope) {
composable<UserRoutes.Info> {
UserInfoRoute()
}
}

View File

@@ -5,14 +5,13 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import com.joker.kit.core.designsystem.theme.AppTheme
import com.joker.kit.core.designsystem.theme.SpacePaddingLarge
import com.joker.kit.core.navigation.navigateBack
import com.joker.kit.core.ui.component.scaffold.AppScaffold
import com.joker.kit.core.ui.component.text.AppText
import com.joker.kit.feature.user.viewmodel.UserInfoViewModel
@@ -29,7 +28,7 @@ internal fun UserInfoRoute(
) {
UserInfoScreen(
onLogoutClick = viewModel::logout,
onBackClick = viewModel::navigateBack
onBackClick = ::navigateBack
)
}

View File

@@ -2,9 +2,9 @@ package com.joker.kit.feature.user.viewmodel
import androidx.lifecycle.viewModelScope
import com.joker.kit.core.base.viewmodel.BaseViewModel
import com.joker.kit.core.navigation.navigateBack
import com.joker.kit.core.state.UserState
import com.joker.kit.core.util.toast.ToastUtils
import com.joker.kit.navigation.AppNavigator
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
@@ -18,12 +18,8 @@ import javax.inject.Inject
*/
@HiltViewModel
class UserInfoViewModel @Inject constructor(
navigator: AppNavigator,
userState: UserState
) : BaseViewModel(
navigator = navigator,
userState = userState
) {
private val userState: UserState
) : BaseViewModel() {
/**
* 一键退出登录(本地清空)

View File

@@ -1,82 +0,0 @@
package com.joker.kit.navigation
import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.core.tween
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.rememberNavController
import com.joker.kit.feature.auth.navigation.authGraph
import com.joker.kit.feature.demo.navigation.demoGraph
import com.joker.kit.feature.main.navigation.mainGraph
import com.joker.kit.feature.user.navigation.userGraph
import com.joker.kit.navigation.routes.MainRoutes
import kotlinx.coroutines.flow.collectLatest
/**
* 应用导航宿主
* 配置整个应用的导航图和动画
*
* @param navigator 导航管理器
* @param modifier 修饰符
* @author Joker.X
*/
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun AppNavHost(
navigator: AppNavigator,
modifier: Modifier = Modifier
) {
val navController = rememberNavController()
// 监听导航事件
LaunchedEffect(navController) {
navigator.navigationEvents.collectLatest { event ->
navController.handleNavigationEvent(event)
}
}
SharedTransitionLayout {
NavHost(
navController = navController,
startDestination = MainRoutes.Main,
modifier = modifier,
// 页面进入动画
enterTransition = {
slideIntoContainer(
towards = AnimatedContentTransitionScope.SlideDirection.Left,
animationSpec = tween(300)
)
},
// 页面退出动画
exitTransition = {
slideOutOfContainer(
towards = AnimatedContentTransitionScope.SlideDirection.Left,
animationSpec = tween(300)
)
},
// 返回时页面进入动画
popEnterTransition = {
slideIntoContainer(
towards = AnimatedContentTransitionScope.SlideDirection.Right,
animationSpec = tween(300)
)
},
// 返回时页面退出动画
popExitTransition = {
slideOutOfContainer(
towards = AnimatedContentTransitionScope.SlideDirection.Right,
animationSpec = tween(300)
)
}
) {
mainGraph(navController, this@SharedTransitionLayout)
demoGraph(navController, this@SharedTransitionLayout)
authGraph(navController, this@SharedTransitionLayout)
userGraph(navController, this@SharedTransitionLayout)
}
}
}

View File

@@ -1,176 +0,0 @@
package com.joker.kit.navigation
import androidx.navigation.NavController
import androidx.navigation.NavOptions
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import javax.inject.Inject
import javax.inject.Singleton
/**
* 导航管理器
*
* 负责处理应用内所有的导航请求,采用事件驱动模式:
* 1. ViewModel 通过 AppNavigator 发送导航事件
* 2. AppNavHost 监听事件并通过 NavController 执行导航
* 3. 实现了 ViewModel 与 NavController 的解耦
*
* 优势:
* - ViewModel 无需持有 NavController 引用
* - 支持类型安全的路由导航
* - 便于单元测试
* - 统一管理导航逻辑
*
* 使用示例:
* ```kotlin
* // 在 ViewModel 中
* class MyViewModel @Inject constructor(
* private val navigator: AppNavigator
* ) : ViewModel() {
* fun navigateToDetail() {
* viewModelScope.launch {
* navigator.navigateTo(GoodsRoutes.Detail(id = 123))
* }
* }
* }
* ```
*
* @author Joker.X
*/
@Singleton
class AppNavigator @Inject constructor() {
private val _navigationEvents = MutableSharedFlow<NavigationEvent>()
val navigationEvents: SharedFlow<NavigationEvent> = _navigationEvents.asSharedFlow()
/**
* 导航到指定路由
*
* @param route 类型安全的路由对象(必须是 @Serializable
* @param navOptions 导航选项(可选)
*
* 使用示例:
* ```kotlin
* // 简单导航
* navigateTo(MainRoutes.Home)
*
* // 带参数导航
* navigateTo(GoodsRoutes.Detail(goodsId = 123))
*
* // 带 NavOptions
* navigateTo(UserRoutes.Profile, navOptions)
* ```
*
* @author Joker.X
*/
suspend fun navigateTo(route: Any, navOptions: NavOptions? = null) {
_navigationEvents.emit(NavigationEvent.NavigateTo(route, navOptions))
}
/**
* 返回上一页(简单返回,无结果)
*
* 使用示例:
* ```kotlin
* navigateBack()
* ```
*
* @author Joker.X
*/
suspend fun navigateBack() {
_navigationEvents.emit(NavigationEvent.NavigateUp)
}
/**
* 返回上一页并携带类型安全的结果(使用 NavigationResultKey
*
* 这是 V3.2 版本的最终方案,实现了端到端的类型安全
*
* @param key 类型安全的结果 Key
* @param result 要传递的结果对象
*
* 使用示例:
* ```kotlin
* // 1. 定义返回结果数据类型
* @Serializable
* data class Address(val id: Long, val fullAddress: String)
*
* // 2. 定义 ResultKey
* object SelectAddressResultKey : NavigationResultKey<Address>
*
* // 3. 在发送方使用
* popBackStackWithResult(SelectAddressResultKey, address)
* ```
*
* 在接收方使用:
* ```kotlin
* navController.collectResult(SelectAddressResultKey) { address ->
* // address 是强类型的 Address 对象,绝对类型安全
* viewModel.updateAddress(address)
* }
* ```
*
* @author Joker.X
*/
suspend fun <T> popBackStackWithResult(key: NavigationResultKey<T>, result: T) {
_navigationEvents.emit(NavigationEvent.PopBackStackWithResult(key, result))
}
/**
* 返回到指定路由
*
* @param route 目标路由对象(必须是 @Serializable
* @param inclusive 是否包含目标路由本身true: 目标也会被弹出false: 保留目标)
*
* 使用示例:
* ```kotlin
* // 返回到主页并保留主页
* navigateBackTo(MainRoutes.Main, inclusive = false)
*
* // 返回到登录页并移除登录页(重新加载登录页)
* navigateBackTo(AuthRoutes.Login, inclusive = true)
* ```
*
* @author Joker.X
*/
suspend fun navigateBackTo(route: Any, inclusive: Boolean = false) {
_navigationEvents.emit(NavigationEvent.NavigateBackTo(route, inclusive))
}
}
/**
* 处理导航事件的 NavController 扩展函数
*
* 将导航事件转换为实际的导航操作
*
* @param event 导航事件
* @author Joker.X
*/
fun NavController.handleNavigationEvent(event: NavigationEvent) {
when (event) {
is NavigationEvent.NavigateTo -> {
// 使用类型安全的 navigate 方法
this.navigate(event.route, event.navOptions)
}
is NavigationEvent.NavigateUp -> {
// 简单返回,不携带结果
this.popBackStack()
}
is NavigationEvent.PopBackStackWithResult<*> -> {
// 使用 NavigationResultKey 的类型安全结果回传
// 通过 key.serialize 将结果转换为 SavedStateHandle 支持的类型
@Suppress("UNCHECKED_CAST")
val key = event.key as NavigationResultKey<Any?>
val rawValue = key.serialize(event.result)
previousBackStackEntry?.savedStateHandle?.set(key.key, rawValue)
this.popBackStack()
}
is NavigationEvent.NavigateBackTo -> {
// 弹出回退栈到指定路由
this.popBackStack(event.route, event.inclusive)
}
}
}

View File

@@ -1,54 +0,0 @@
package com.joker.kit.navigation
import androidx.navigation.NavOptions
/**
* 导航事件
* 定义所有可能的导航操作类型
*
* @author Joker.X
*/
sealed class NavigationEvent {
/**
* 导航到指定路由
*
* @param route 类型安全的路由对象(必须是 @Serializable
* @param navOptions 导航选项
* @author Joker.X
*/
data class NavigateTo(
val route: Any,
val navOptions: NavOptions? = null
) : NavigationEvent()
/**
* 返回上一页(简单返回,无结果)
*
* @author Joker.X
*/
data object NavigateUp : NavigationEvent()
/**
* 返回上一页并携带类型安全的结果(使用 NavigationResultKey
*
* @param key 类型安全的结果 Key
* @param result 返回结果
* @author Joker.X
*/
data class PopBackStackWithResult<T>(
val key: NavigationResultKey<T>,
val result: T
) : NavigationEvent()
/**
* 返回到指定路由
*
* @param route 类型安全的路由对象(必须是 @Serializable
* @param inclusive 是否包含目标路由本身
* @author Joker.X
*/
data class NavigateBackTo(
val route: Any,
val inclusive: Boolean = false
) : NavigationEvent()
}

View File

@@ -1,23 +0,0 @@
package com.joker.kit.navigation
/**
* 通用的页面刷新结果 Key。
*
* 语义等价于以前的 "refresh" 布尔标记:
* - true 表示上一个页面需要刷新数据
* - false 或 null 表示不刷新
*
* 示例:
* ```kotlin
* // 子页面:操作成功后返回并通知上一个页面刷新
* popBackStackWithResult(RefreshResultKey, true)
*
* // 上一个页面ViewModel
* fun observeRefresh(backStackEntry: NavBackStackEntry?) {
* observeRefreshState(backStackEntry, RefreshResultKey)
* }
* ```
*
* @author Joker.X
*/
object RefreshResultKey : NavigationResultKey<Boolean>

View File

@@ -1,74 +0,0 @@
package com.joker.kit.navigation
import com.joker.kit.navigation.routes.AuthRoutes
import com.joker.kit.navigation.routes.UserRoutes
import kotlin.reflect.KClass
/**
* 路由拦截器(类型安全版本)
*
* 负责管理需要登录的页面配置和路由拦截逻辑
* 使用类型安全的方式处理路由拦截
*
* @author Joker.X
*/
class RouteInterceptor {
/**
* 需要登录的路由类型集合
* 在这里配置所有需要登录才能访问的页面类型
*/
private val loginRequiredRouteTypes: MutableSet<KClass<out Any>> = mutableSetOf(
UserRoutes.Info::class
)
/**
* 检查指定路由对象是否需要登录
*
* @param route 要检查的路由对象(类型安全)
* @return true表示需要登录false表示不需要登录
* @author Joker.X
*/
fun requiresLogin(route: Any): Boolean {
val routeClass = route::class
return loginRequiredRouteTypes.contains(routeClass)
}
/**
* 获取登录页面路由对象
*
* @return 登录页面的路由对象
* @author Joker.X
*/
fun getLoginRoute(): Any = AuthRoutes.Login
/**
* 添加需要登录的路由类型
*
* @param routeClass 需要登录的路由类型
* @author Joker.X
*/
fun addLoginRequiredRoute(routeClass: KClass<*>) {
loginRequiredRouteTypes.add(routeClass)
}
/**
* 移除需要登录的路由类型
*
* @param routeClass 不再需要登录的路由类型
* @author Joker.X
*/
fun removeLoginRequiredRoute(routeClass: KClass<*>) {
loginRequiredRouteTypes.remove(routeClass)
}
/**
* 获取所有需要登录的路由类型
*
* @return 需要登录的路由类型集合
* @author Joker.X
*/
fun getLoginRequiredRoutes(): Set<KClass<*>> {
return loginRequiredRouteTypes.toSet()
}
}

View File

@@ -1,42 +0,0 @@
package com.joker.kit.navigation.extension
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.navigation.NavController
import com.joker.kit.navigation.NavigationResultKey
/**
* 监听返回结果扩展
*
* @param key 结果键,定义序列化/反序列化规则
* @param onResult 结果回调
* @param T 结果数据类型
* @author Joker.X
*/
@Composable
fun <T> NavController.observeResult(
key: NavigationResultKey<T>,
onResult: (T) -> Unit
) {
val backStackEntry = currentBackStackEntry ?: return
val savedStateHandle = backStackEntry.savedStateHandle
DisposableEffect(backStackEntry, key) {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME) {
val raw = savedStateHandle.get<Any?>(key.key)
if (raw != null) {
val result = key.deserialize(raw)
onResult(result)
savedStateHandle.remove<Any?>(key.key)
}
}
}
backStackEntry.lifecycle.addObserver(observer)
onDispose {
backStackEntry.lifecycle.removeObserver(observer)
}
}
}

View File

@@ -1,16 +0,0 @@
package com.joker.kit.navigation.routes
import kotlinx.serialization.Serializable
/**
* 用户认证相关路由
* 这里只提供一个示例登录路由
*/
object AuthRoutes {
/**
* 登录页
*/
@Serializable
data object Login
}

View File

@@ -1,42 +0,0 @@
package com.joker.kit.navigation.routes
import kotlinx.serialization.Serializable
/**
* Demo 模块路由
*/
object DemoRoutes {
/** Network Demo 示例页 */
@Serializable
data object NetworkDemo
/** Network List Demo 示例页 */
@Serializable
data object NetworkListDemo
/** 数据库示例页 */
@Serializable
data object Database
/** 本地存储示例页 */
@Serializable
data object LocalStorage
/** 状态管理示例页 */
@Serializable
data object StateManagement
/** 通用网络请求示例页 */
@Serializable
data object NetworkRequest
/** 带参跳转示例页 */
@Serializable
data class NavigationWithArgs(
val goodsId: Long
)
/** 结果回传示例页 */
@Serializable
data object NavigationResult
}

View File

@@ -1,16 +0,0 @@
package com.joker.kit.navigation.routes
import kotlinx.serialization.Serializable
/**
* 用户相关路由
*/
object UserRoutes {
/**
* 用户信息页
* 登录后才能访问的用户信息页
*/
@Serializable
data object Info
}

View File

@@ -3,33 +3,34 @@
android:height="108dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<group android:scaleX="0.6068966"
android:scaleY="0.6068966"
android:translateX="235.79463"
android:translateY="204.1461">
<path
android:pathData="M373.8,202.3H189.6a67.8,67.8 0,0 0,-67.6 67.8,65.6 65.6,0 0,0 65.6,67.6h188.2l-2,-135.5z"
android:fillColor="#7585FF"/>
<path
android:pathData="M793.5,805.2H189.4a67.8,67.8 0,0 1,-67.4 -67.6V269.7a67.8,67.8 0,0 0,72 68.1h501.1s98.5,-8.5 98.5,65.6v401.8z"
android:fillColor="#465CFF"/>
<path
android:fillColor="#FF000000"
android:pathData="M474.5,479.8a37.6,37.6 0,1 0,14.4 72.2,37.4 37.4,0 0,0 -14.4,-72.2z"/>
<path
android:pathData="M511.7,565.6a60.8,60.8 0,0 0,-34.8 -109.4h2a57.6,57.6 0,0 1,10.7 0L489.6,418.5a12,12 0,0 0,-5.7 -10.7,11.8 11.8,0 0,0 -12,0 12,12 0,0 0,-5.7 10.7v38.3a61.1,61.1 0,0 0,-26.7 109.4L328.1,804.3h72.6l51.4,-109.4a26.7,26.7 0,0 1,24.1 -15.1,26.3 26.3,0 0,1 23.9,15.1l53,109.4h73.5L511.7,565.6zM589.4,793.6a37.6,37.6 0,0 1,-26.5 -64.1,37.4 37.4,0 0,1 64.1,26.5 37.6,37.6 0,0 1,-37.6 37.6z"
android:fillColor="#3870B2"/>
<path
android:pathData="M334,235.6h258.4v47.5H334v-47.5z"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M454.4,479.8a37.4,37.4 0,1 1,-26.7 10.9,38.1 38.1,0 0,1 26.7,-10.9zM469.2,457.9L469.2,418.5a12,12 0,0 0,-5.7 -10.7,11.8 11.8,0 0,0 -12,0 12,12 0,0 0,-5.7 10.7v38.3a61.1,61.1 0,0 0,-26.7 109.4L279.3,867.1a33.3,33.3 0,0 0,2.4 33.5,33.7 33.7,0 0,0 30.4,14.2 33,33 0,0 0,27.1 -19.7l93.2,-200.2a26.9,26.9 0,0 1,24.1 -15.1,26.3 26.3,0 0,1 23.9,15.1l95.6,199.1a33,33 0,1 0,59.5 -28.9L491.6,565.6a60.6,60.6 0,0 0,21.9 -60.6,61.3 61.3,0 0,0 -43.8,-46.8"
android:fillColor="#073042"/>
<path
android:pathData="M555,277.4a17.5,17.5 0,0 1,-12.7 -30,17.7 17.7,0 0,1 30.2,12.5 17.5,17.5 0,0 1,-17.5 17.5m-194.5,0a17.5,17.5 0,0 1,-12.5 -30,17.7 17.7,0 1,1 24.9,24.9 17.3,17.3 0,0 1,-12.5 5M561.4,171.3l35,-60.8a7.4,7.4 0,0 0,-12.7 -7.4l-35.4,61.7a220.6,220.6 0,0 0,-180.8 0l-35.7,-61.7a7.4,7.4 0,0 0,-6.3 -3.7,8.1 8.1,0 0,0 -6.3,3.7 7.4,7.4 0,0 0,0 7.4l35.2,60.8a208.3,208.3 0,0 0,-107.7 166.5h422.3A208.3,208.3 0,0 0,561.4 171.3"
android:fillColor="#3DDC84"/>
<path
android:pathData="M732.9,467.6h-27.8a3.5,3.5 0,0 0,-3.5 3.5v438.7a3.5,3.5 0,0 0,3.3 3.5h28.7a60.8,60.8 0,0 0,60.6 -60.8V406.7a60.8,60.8 0,0 1,-61.3 60.8z"
android:fillColor="#7585FF"/>
</group>
<group
android:scaleX="0.6068966"
android:scaleY="0.6068966"
android:translateX="235.79463"
android:translateY="204.1461">
<path
android:fillColor="#7585FF"
android:pathData="M373.8,202.3H189.6a67.8,67.8 0,0 0,-67.6 67.8,65.6 65.6,0 0,0 65.6,67.6h188.2l-2,-135.5z" />
<path
android:fillColor="#465CFF"
android:pathData="M793.5,805.2H189.4a67.8,67.8 0,0 1,-67.4 -67.6V269.7a67.8,67.8 0,0 0,72 68.1h501.1s98.5,-8.5 98.5,65.6v401.8z" />
<path
android:fillColor="#FF000000"
android:pathData="M474.5,479.8a37.6,37.6 0,1 0,14.4 72.2,37.4 37.4,0 0,0 -14.4,-72.2z" />
<path
android:fillColor="#3870B2"
android:pathData="M511.7,565.6a60.8,60.8 0,0 0,-34.8 -109.4h2a57.6,57.6 0,0 1,10.7 0L489.6,418.5a12,12 0,0 0,-5.7 -10.7,11.8 11.8,0 0,0 -12,0 12,12 0,0 0,-5.7 10.7v38.3a61.1,61.1 0,0 0,-26.7 109.4L328.1,804.3h72.6l51.4,-109.4a26.7,26.7 0,0 1,24.1 -15.1,26.3 26.3,0 0,1 23.9,15.1l53,109.4h73.5L511.7,565.6zM589.4,793.6a37.6,37.6 0,0 1,-26.5 -64.1,37.4 37.4,0 0,1 64.1,26.5 37.6,37.6 0,0 1,-37.6 37.6z" />
<path
android:fillColor="#FFFFFF"
android:pathData="M334,235.6h258.4v47.5H334v-47.5z" />
<path
android:fillColor="#073042"
android:pathData="M454.4,479.8a37.4,37.4 0,1 1,-26.7 10.9,38.1 38.1,0 0,1 26.7,-10.9zM469.2,457.9L469.2,418.5a12,12 0,0 0,-5.7 -10.7,11.8 11.8,0 0,0 -12,0 12,12 0,0 0,-5.7 10.7v38.3a61.1,61.1 0,0 0,-26.7 109.4L279.3,867.1a33.3,33.3 0,0 0,2.4 33.5,33.7 33.7,0 0,0 30.4,14.2 33,33 0,0 0,27.1 -19.7l93.2,-200.2a26.9,26.9 0,0 1,24.1 -15.1,26.3 26.3,0 0,1 23.9,15.1l95.6,199.1a33,33 0,1 0,59.5 -28.9L491.6,565.6a60.6,60.6 0,0 0,21.9 -60.6,61.3 61.3,0 0,0 -43.8,-46.8" />
<path
android:fillColor="#3DDC84"
android:pathData="M555,277.4a17.5,17.5 0,0 1,-12.7 -30,17.7 17.7,0 0,1 30.2,12.5 17.5,17.5 0,0 1,-17.5 17.5m-194.5,0a17.5,17.5 0,0 1,-12.5 -30,17.7 17.7,0 1,1 24.9,24.9 17.3,17.3 0,0 1,-12.5 5M561.4,171.3l35,-60.8a7.4,7.4 0,0 0,-12.7 -7.4l-35.4,61.7a220.6,220.6 0,0 0,-180.8 0l-35.7,-61.7a7.4,7.4 0,0 0,-6.3 -3.7,8.1 8.1,0 0,0 -6.3,3.7 7.4,7.4 0,0 0,0 7.4l35.2,60.8a208.3,208.3 0,0 0,-107.7 166.5h422.3A208.3,208.3 0,0 0,561.4 171.3" />
<path
android:fillColor="#7585FF"
android:pathData="M732.9,467.6h-27.8a3.5,3.5 0,0 0,-3.5 3.5v438.7a3.5,3.5 0,0 0,3.3 3.5h28.7a60.8,60.8 0,0 0,60.6 -60.8V406.7a60.8,60.8 0,0 1,-61.3 60.8z" />
</group>
</vector>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<background android:drawable="@color/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<background android:drawable="@color/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@@ -1,6 +1,6 @@
<resources>
<string name="app_name">AndroidProject-Compose</string>
<!-- Empty States -->
<string name="empty_cart_btn">Go Shopping</string>
<string name="empty_data">No Data</string>