From a26f25fdaa043d72291d2b72b665264da9a755eb Mon Sep 17 00:00:00 2001 From: "Joker.X" Date: Tue, 2 Dec 2025 23:40:10 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E6=AD=A5=E5=AE=9E=E7=8E=B0=E5=9F=BA?= =?UTF-8?q?=E6=9C=AC=E5=8A=9F=E8=83=BD=E7=A4=BA=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 4 +- app/src/main/AndroidManifest.xml | 9 +- .../joker/kit/core/state/DemoCounterState.kt | 56 +++ .../core/ui/component/appbar/BackButton.kt | 32 ++ .../ui/component/appbar/CenterTopAppBar.kt | 68 ++++ .../ui/component/appbar/LargeTopAppBar.kt | 81 ++++ .../kit/core/ui/component/loading/LoadMore.kt | 61 ++- .../kit/core/ui/component/loading/Loading.kt | 27 +- .../core/ui/component/scaffold/AppScaffold.kt | 129 ++++++ .../kit/feature/auth/view/LoginScreen.kt | 42 +- .../feature/auth/viewmodel/LoginViewModel.kt | 42 +- .../demo/navigation/DatabaseNavigation.kt | 18 + .../kit/feature/demo/navigation/DemoGraph.kt | 24 ++ .../demo/navigation/LocalStorageNavigation.kt | 18 + .../navigation/NavigationResultNavigation.kt | 18 + .../NavigationWithArgsNavigation.kt | 18 + .../demo/navigation/NetworkDemoNavigation.kt | 18 + .../navigation/NetworkListDemoNavigation.kt | 18 + .../navigation/NetworkRequestNavigation.kt | 18 + .../navigation/StateManagementNavigation.kt | 18 + .../kit/feature/demo/view/DatabaseScreen.kt | 375 ++++++++++++++++++ .../feature/demo/view/LocalStorageScreen.kt | 300 ++++++++++++++ .../demo/view/NavigationResultScreen.kt | 85 ++++ .../demo/view/NavigationWithArgsScreen.kt | 72 ++++ .../feature/demo/view/NetworkDemoScreen.kt | 112 ++++++ .../demo/view/NetworkListDemoScreen.kt | 184 +++++++++ .../feature/demo/view/NetworkRequestScreen.kt | 153 +++++++ .../demo/view/StateManagementScreen.kt | 224 +++++++++++ .../demo/viewmodel/DatabaseViewModel.kt | 106 +++++ .../demo/viewmodel/LocalStorageViewModel.kt | 123 ++++++ .../viewmodel/NavigationResultViewModel.kt | 28 ++ .../viewmodel/NavigationWithArgsViewModel.kt | 33 ++ .../demo/viewmodel/NetworkDemoViewModel.kt | 37 ++ .../viewmodel/NetworkListDemoViewModel.kt | 47 +++ .../demo/viewmodel/NetworkRequestViewModel.kt | 46 +++ .../viewmodel/StateManagementViewModel.kt | 40 ++ .../kit/feature/main/component/DemoCard.kt | 9 +- .../kit/feature/main/data/DemoCardData.kt | 35 +- .../kit/feature/main/model/DemoCardInfo.kt | 3 +- .../kit/feature/main/navigation/MainGraph.kt | 2 +- .../feature/main/navigation/MainNavigation.kt | 8 +- .../kit/feature/main/view/CoreDemoScreen.kt | 52 ++- .../joker/kit/feature/main/view/MainScreen.kt | 23 +- .../feature/main/view/NavigationDemoScreen.kt | 80 +++- .../main/viewmodel/CoreDemoViewModel.kt | 9 + .../main/viewmodel/NavigationDemoViewModel.kt | 15 + .../kit/feature/user/view/UserInfoScreen.kt | 39 +- .../user/viewmodel/UserInfoViewModel.kt | 17 +- .../com/joker/kit/navigation/AppNavHost.kt | 2 + .../extension/NavigationResultExt.kt | 37 ++ .../kit/navigation/results/DemoResultKey.kt | 19 + .../joker/kit/navigation/routes/DemoRoutes.kt | 42 ++ .../joker/kit/navigation/routes/UserRoutes.kt | 2 + app/src/main/res/drawable/ic_left.xml | 9 + 54 files changed, 2990 insertions(+), 97 deletions(-) create mode 100644 app/src/main/java/com/joker/kit/core/state/DemoCounterState.kt create mode 100644 app/src/main/java/com/joker/kit/core/ui/component/appbar/BackButton.kt create mode 100644 app/src/main/java/com/joker/kit/core/ui/component/appbar/CenterTopAppBar.kt create mode 100644 app/src/main/java/com/joker/kit/core/ui/component/appbar/LargeTopAppBar.kt create mode 100644 app/src/main/java/com/joker/kit/core/ui/component/scaffold/AppScaffold.kt create mode 100644 app/src/main/java/com/joker/kit/feature/demo/navigation/DatabaseNavigation.kt create mode 100644 app/src/main/java/com/joker/kit/feature/demo/navigation/DemoGraph.kt create mode 100644 app/src/main/java/com/joker/kit/feature/demo/navigation/LocalStorageNavigation.kt create mode 100644 app/src/main/java/com/joker/kit/feature/demo/navigation/NavigationResultNavigation.kt create mode 100644 app/src/main/java/com/joker/kit/feature/demo/navigation/NavigationWithArgsNavigation.kt create mode 100644 app/src/main/java/com/joker/kit/feature/demo/navigation/NetworkDemoNavigation.kt create mode 100644 app/src/main/java/com/joker/kit/feature/demo/navigation/NetworkListDemoNavigation.kt create mode 100644 app/src/main/java/com/joker/kit/feature/demo/navigation/NetworkRequestNavigation.kt create mode 100644 app/src/main/java/com/joker/kit/feature/demo/navigation/StateManagementNavigation.kt create mode 100644 app/src/main/java/com/joker/kit/feature/demo/view/DatabaseScreen.kt create mode 100644 app/src/main/java/com/joker/kit/feature/demo/view/LocalStorageScreen.kt create mode 100644 app/src/main/java/com/joker/kit/feature/demo/view/NavigationResultScreen.kt create mode 100644 app/src/main/java/com/joker/kit/feature/demo/view/NavigationWithArgsScreen.kt create mode 100644 app/src/main/java/com/joker/kit/feature/demo/view/NetworkDemoScreen.kt create mode 100644 app/src/main/java/com/joker/kit/feature/demo/view/NetworkListDemoScreen.kt create mode 100644 app/src/main/java/com/joker/kit/feature/demo/view/NetworkRequestScreen.kt create mode 100644 app/src/main/java/com/joker/kit/feature/demo/view/StateManagementScreen.kt create mode 100644 app/src/main/java/com/joker/kit/feature/demo/viewmodel/DatabaseViewModel.kt create mode 100644 app/src/main/java/com/joker/kit/feature/demo/viewmodel/LocalStorageViewModel.kt create mode 100644 app/src/main/java/com/joker/kit/feature/demo/viewmodel/NavigationResultViewModel.kt create mode 100644 app/src/main/java/com/joker/kit/feature/demo/viewmodel/NavigationWithArgsViewModel.kt create mode 100644 app/src/main/java/com/joker/kit/feature/demo/viewmodel/NetworkDemoViewModel.kt create mode 100644 app/src/main/java/com/joker/kit/feature/demo/viewmodel/NetworkListDemoViewModel.kt create mode 100644 app/src/main/java/com/joker/kit/feature/demo/viewmodel/NetworkRequestViewModel.kt create mode 100644 app/src/main/java/com/joker/kit/feature/demo/viewmodel/StateManagementViewModel.kt create mode 100644 app/src/main/java/com/joker/kit/navigation/extension/NavigationResultExt.kt create mode 100644 app/src/main/java/com/joker/kit/navigation/results/DemoResultKey.kt create mode 100644 app/src/main/java/com/joker/kit/navigation/routes/DemoRoutes.kt create mode 100644 app/src/main/res/drawable/ic_left.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4ca3af1..2bdddfb 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -97,7 +97,7 @@ android { // debug 模式下包名后缀 applicationIdSuffix = ".debug" // debug 模式下的请求 url 地址 - buildConfigField("String", "BASE_URL", "\"https://box.dusksnow.top/app/\"") + buildConfigField("String", "BASE_URL", "\"https://mall.dusksnow.top/app/\"") // 根据当前构建类型是否为 debug 模式来判断是否开启 debug 模式 buildConfigField("Boolean", "DEBUG", "true") } @@ -111,7 +111,7 @@ android { // 正式发布模式下的签名配置(配置完 common 签名配置后,取消注释以下行) // signingConfig = signingConfigs.getByName("common") // 正式发布模式下的请求 url 地址 - buildConfigField("String", "BASE_URL", "\"https://box.dusksnow.top/app/\"") + buildConfigField("String", "BASE_URL", "\"https://mall.dusksnow.top/app/\"") // 根据当前构建类型是否为 debug 模式来判断是否开启 debug 模式 buildConfigField("Boolean", "DEBUG", "false") // 混淆规则文件 diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 224a5ab..471639a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,11 @@ + + + + + + android:theme="@style/Theme.AndroidProjectCompose" + android:usesCleartextTraffic="true"> diff --git a/app/src/main/java/com/joker/kit/core/state/DemoCounterState.kt b/app/src/main/java/com/joker/kit/core/state/DemoCounterState.kt new file mode 100644 index 0000000..d463dd9 --- /dev/null +++ b/app/src/main/java/com/joker/kit/core/state/DemoCounterState.kt @@ -0,0 +1,56 @@ +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 + +/** + * Demo 计数器状态 + * + * 通过全局 StateFlow 演示如何在任意页面共享一个简单的计数器。 + * + * @author Joker.X + */ +@Singleton +class DemoCounterState @Inject constructor( + @param:ApplicationScope private val appScope: CoroutineScope +) { + + /** + * 计数器值 + */ + private val _count = MutableStateFlow(0) + val count: StateFlow = _count.asStateFlow() + + /** + * +1 + */ + fun increase() { + appScope.launch { + _count.value += 1 + } + } + + /** + * -1,最低为 0 + */ + fun decrease() { + appScope.launch { + _count.value = (_count.value - 1).coerceAtLeast(0) + } + } + + /** + * 重置 + */ + fun reset() { + appScope.launch { + _count.value = 0 + } + } +} diff --git a/app/src/main/java/com/joker/kit/core/ui/component/appbar/BackButton.kt b/app/src/main/java/com/joker/kit/core/ui/component/appbar/BackButton.kt new file mode 100644 index 0000000..ca513d0 --- /dev/null +++ b/app/src/main/java/com/joker/kit/core/ui/component/appbar/BackButton.kt @@ -0,0 +1,32 @@ +package com.joker.kit.core.ui.component.appbar + +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import com.joker.kit.R + +/** + * 回退按钮组件 + * + * @param modifier 修饰符,用于自定义组件样式 + * @param tint 图标颜色,默认使用Unspecified颜色 + * @param onClick 点击回调函数 + * @author Joker.X + */ +@Composable +fun BackButton( + modifier: Modifier = Modifier, + tint: Color = Color.Unspecified, + onClick: () -> Unit = {} +) { + IconButton(modifier = modifier, onClick = onClick) { + Icon( + painter = painterResource(id = R.drawable.ic_left), + contentDescription = "Back", + tint = tint + ) + } +} diff --git a/app/src/main/java/com/joker/kit/core/ui/component/appbar/CenterTopAppBar.kt b/app/src/main/java/com/joker/kit/core/ui/component/appbar/CenterTopAppBar.kt new file mode 100644 index 0000000..a19be33 --- /dev/null +++ b/app/src/main/java/com/joker/kit/core/ui/component/appbar/CenterTopAppBar.kt @@ -0,0 +1,68 @@ +package com.joker.kit.core.ui.component.appbar + +import androidx.compose.foundation.layout.RowScope +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarColors +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview + + +/** + * 居中对齐的顶部应用栏组件 + * + * 该组件是对 Material3 CenterAlignedTopAppBar 的封装,提供了一个居中标题的顶部应用栏, + * 支持显示返回按钮、自定义操作按钮等功能。主要用于二级页面的导航栏。 + * + * @param title 顶部应用栏标题的资源ID,如果为null则不显示标题 + * @param titleText 顶部应用栏标题的字符串,如果为null则不显示标题 + * @param colors 顶部应用栏的颜色配置,默认使用 Material3 的居中对齐顶部应用栏颜色 + * @param actions 顶部应用栏右侧的操作按钮区域,默认为空 + * @param onBackClick 点击返回按钮时的回调函数 + * @param showBackIcon 是否显示返回按钮,默认为true + * @author Joker.X + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CenterTopAppBar( + title: Int? = null, + titleText: String? = null, + colors: TopAppBarColors = TopAppBarDefaults.topAppBarColors(), + actions: @Composable (RowScope.() -> Unit) = {}, + onBackClick: () -> Unit = {}, + showBackIcon: Boolean = true +) { + CenterAlignedTopAppBar( + navigationIcon = { + if (showBackIcon) BackButton(onClick = onBackClick) + }, + title = { + val finalTitle = titleText ?: title?.let { stringResource(it) } ?: "" + if (finalTitle.isNotBlank()) { + Text( + text = finalTitle, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + }, + actions = actions, + colors = colors + ) +} + +/** + * 居中顶部应用栏预览 + * + * @author Joker.X + */ +@OptIn(ExperimentalMaterial3Api::class) +@Preview(showBackground = true) +@Composable +fun CenterTopAppBarPreview() { + CenterTopAppBar() +} \ No newline at end of file diff --git a/app/src/main/java/com/joker/kit/core/ui/component/appbar/LargeTopAppBar.kt b/app/src/main/java/com/joker/kit/core/ui/component/appbar/LargeTopAppBar.kt new file mode 100644 index 0000000..e510e71 --- /dev/null +++ b/app/src/main/java/com/joker/kit/core/ui/component/appbar/LargeTopAppBar.kt @@ -0,0 +1,81 @@ +package com.joker.kit.core.ui.component.appbar + +import androidx.compose.foundation.layout.RowScope +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MediumTopAppBar +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.lerp +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.lerp +import androidx.compose.ui.unit.sp + +/** + * 大标题顶部应用栏组件 + * + * 该组件是对 Material3 MediumTopAppBar 的封装,提供了一个带有动态字体大小的大标题顶部应用栏, + * 支持滚动时标题字体大小的动态变化。主要用于页面主标题。 + * + * @param title 顶部应用栏标题的资源ID,如果为null则不显示标题 + * @param titleText 顶部应用栏标题的字符串,如果为null则不显示标题 + * @param actions 顶部应用栏右侧的操作按钮区域 + * @param onBackClick 点击返回按钮时的回调函数 + * @param showBackIcon 是否显示返回按钮,默认为true + * @param scrollBehavior 滚动行为,用于控制标题动画 + * @param expandedBackgroundColor 展开状态下的背景色,默认为 MaterialTheme.colorScheme.background + * @param collapsedBackgroundColor 收起状态下的背景色,默认为 MaterialTheme.colorScheme.background + * @author Joker.X + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LargeTopAppBar( + title: Int? = null, + titleText: String? = null, + actions: @Composable (RowScope.() -> Unit) = {}, + onBackClick: () -> Unit = {}, + showBackIcon: Boolean = true, + scrollBehavior: TopAppBarScrollBehavior? = null, + expandedBackgroundColor: Color = MaterialTheme.colorScheme.background, + collapsedBackgroundColor: Color = MaterialTheme.colorScheme.background +) { + val scrollFraction = scrollBehavior?.state?.collapsedFraction ?: 0f + val titleFontSize = lerp( + start = 30.sp, + stop = 16.sp, + fraction = scrollFraction + ) + + // 根据滚动状态动态计算背景色 + val backgroundColor = lerp( + start = expandedBackgroundColor, + stop = collapsedBackgroundColor, + fraction = scrollFraction + ) + + MediumTopAppBar( + navigationIcon = { + if (showBackIcon) BackButton(onClick = onBackClick) + }, + title = { + val finalTitle = titleText ?: title?.let { stringResource(it) } ?: "" + if (finalTitle.isNotBlank()) { + Text( + text = finalTitle, + fontSize = titleFontSize, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + }, + actions = actions, + scrollBehavior = scrollBehavior, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = backgroundColor, + ) + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/joker/kit/core/ui/component/loading/LoadMore.kt b/app/src/main/java/com/joker/kit/core/ui/component/loading/LoadMore.kt index d7c403e..d14857c 100644 --- a/app/src/main/java/com/joker/kit/core/ui/component/loading/LoadMore.kt +++ b/app/src/main/java/com/joker/kit/core/ui/component/loading/LoadMore.kt @@ -44,7 +44,6 @@ import com.joker.kit.core.ui.component.text.TextType * @param state 当前加载状态,默认为可上拉加载状态 * @param listState LazyList的状态,用于自动滚动到底部,可为空 * @param onRetry 加载失败时的重试回调,为空时不可点击重试 - * @author Joker.X */ @Composable fun LoadMore( @@ -63,20 +62,28 @@ fun LoadMore( when (state) { // 可上拉加载更多状态:显示提示文本 LoadMoreState.PullToLoad -> { - Divider(modifier = Modifier.weight(1f)) + LoadMoreDivider(modifier = Modifier.weight(1f)) AppText( text = stringResource(R.string.load_more_pull), modifier = Modifier.padding(horizontal = 8.dp), type = TextType.SECONDARY ) - Divider(modifier = Modifier.weight(1f)) + LoadMoreDivider(modifier = Modifier.weight(1f)) } // 加载中状态:显示加载动画和提示文本 LoadMoreState.Loading -> { - MiLoadingWeb() // 显示加载动画 + // 显示加载动画 + MiLoadingWeb( + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.2f), + borderWidth = 2.dp, + loadingSize = 12.dp + ) Spacer(modifier = Modifier.width(8.dp)) - AppText(text = stringResource(R.string.load_more_loading)) + AppText( + text = stringResource(R.string.load_more_loading), + color = MaterialTheme.colorScheme.onSurface + ) // 如果提供了列表状态,自动滚动到底部 if (listState != null) { LaunchedEffect(Unit) { @@ -87,17 +94,18 @@ fun LoadMore( // 加载成功状态:显示加载成功提示 LoadMoreState.Success -> { - Divider(modifier = Modifier.weight(1f)) + LoadMoreDivider(modifier = Modifier.weight(1f)) AppText( text = stringResource(R.string.load_more_success), - modifier = Modifier.padding(horizontal = 8.dp) + modifier = Modifier.padding(horizontal = 8.dp), + color = MaterialTheme.colorScheme.onSurface ) - Divider(modifier = Modifier.weight(1f)) + LoadMoreDivider(modifier = Modifier.weight(1f)) } // 加载失败状态:显示错误提示和分割线 LoadMoreState.Error -> { - Divider(modifier = Modifier.weight(1f)) + LoadMoreDivider(modifier = Modifier.weight(1f)) if (onRetry != null) { AppText( text = stringResource(R.string.load_more_error_retry), @@ -115,36 +123,41 @@ fun LoadMore( type = TextType.ERROR ) } - Divider(modifier = Modifier.weight(1f)) + LoadMoreDivider(modifier = Modifier.weight(1f)) } // 没有更多数据状态:显示分割线和中间的圆点 LoadMoreState.NoMore -> { // 左侧分割线 - Divider( - modifier = Modifier.weight(1f), - color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.8f) - ) + LoadMoreDivider(modifier = Modifier.weight(1f)) // 中间圆点 Box( modifier = Modifier .padding(horizontal = 8.dp) .size(4.dp) .background( - MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.8f), + MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f), CircleShape ) ) // 右侧分割线 - Divider( - modifier = Modifier.weight(1f), - color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.8f) - ) + LoadMoreDivider(modifier = Modifier.weight(1f)) } } } } +/** + * 加载更多分割线 + */ +@Composable +private fun LoadMoreDivider(modifier: Modifier = Modifier) { + Divider( + modifier = modifier, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f) + ) +} + /** * OrderLoadMore 组件预览 * @@ -154,7 +167,9 @@ fun LoadMore( @Composable private fun OrderLoadMorePreview() { AppTheme { - Column { + Column( + modifier = Modifier.background(MaterialTheme.colorScheme.background) + ) { // 可上拉加载更多状态预览 LoadMore(state = LoadMoreState.PullToLoad) Spacer(modifier = Modifier.height(8.dp)) @@ -193,7 +208,9 @@ private fun OrderLoadMorePreview() { @Composable private fun OrderLoadMorePreviewDark() { AppTheme(darkTheme = true) { - Column { + Column( + modifier = Modifier.background(MaterialTheme.colorScheme.background) + ) { // 可上拉加载更多状态预览 LoadMore(state = LoadMoreState.PullToLoad) Spacer(modifier = Modifier.height(8.dp)) @@ -221,4 +238,4 @@ private fun OrderLoadMorePreviewDark() { LoadMore(state = LoadMoreState.NoMore) } } -} \ No newline at end of file +} \ No newline at end of file diff --git a/app/src/main/java/com/joker/kit/core/ui/component/loading/Loading.kt b/app/src/main/java/com/joker/kit/core/ui/component/loading/Loading.kt index b172b36..8c1f6d7 100644 --- a/app/src/main/java/com/joker/kit/core/ui/component/loading/Loading.kt +++ b/app/src/main/java/com/joker/kit/core/ui/component/loading/Loading.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.geometry.center import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.scale import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.joker.kit.core.designsystem.theme.AppTheme import com.joker.kit.core.designsystem.theme.Primary @@ -30,9 +31,15 @@ import kotlin.math.sin * 小米风格Web加载动画 - 3条竖线交替缩放 * * @param color 竖线颜色,默认使用Primary主题色 + * @param loadingSize 加载动画大小,默认28dp + * @param borderWidth 边框宽度,默认4dp */ @Composable -fun MiLoadingWeb(color: Color = Primary) { +fun MiLoadingWeb( + color: Color = Primary, + loadingSize: Dp = 24.dp, + borderWidth: Dp = 4.dp +) { val infiniteTransition = rememberInfiniteTransition(label = "") val animations = List(3) { index -> val alpha by infiniteTransition.animateFloat( @@ -64,9 +71,9 @@ fun MiLoadingWeb(color: Color = Primary) { Pair(alpha, scaleY) } - Canvas(modifier = Modifier.size(24.dp)) { + Canvas(modifier = Modifier.size(loadingSize)) { animations.forEachIndexed { index, item -> - val strokeWidth = 4.dp.toPx() + val strokeWidth = borderWidth.toPx() val spacing = (size.width - (3 * strokeWidth)) / 2 scale(scaleX = 1f, scaleY = item.second) { @@ -93,15 +100,21 @@ fun MiLoadingWeb(color: Color = Primary) { * @param borderColor 圆形轨道边框颜色,默认使用onSurface颜色 * @param dotColor 旋转圆点的颜色,默认与边框颜色相同 * @param animationSpec 动画规格配置,默认1200ms线性动画 + * @param loadingSize 加载动画大小,默认28dp + * @param borderWidth 圆形轨道边框宽度,默认2dp + * @param dotRadiusSize 旋转圆点半径大小,默认3dp */ @Composable fun MiLoadingMobile( borderColor: Color = MaterialTheme.colorScheme.onSurface, dotColor: Color = borderColor, + loadingSize: Dp = 28.dp, + borderWidth: Dp = 2.dp, + dotRadiusSize: Dp = 3.dp, animationSpec: DurationBasedAnimationSpec = tween( durationMillis = 1200, easing = LinearEasing - ) + ), ) { val infiniteTransition = rememberInfiniteTransition(label = "") val angle = infiniteTransition.animateFloat( @@ -116,11 +129,11 @@ fun MiLoadingMobile( Canvas( modifier = Modifier - .size(28.dp) - .border(2.dp, borderColor, CircleShape) + .size(loadingSize) + .border(borderWidth, borderColor, CircleShape) ) { val circleRadius = size.minDimension / 2 - 8.dp.toPx() - val dotRadius = 3.dp.toPx() + val dotRadius = dotRadiusSize.toPx() val center = size.center val dotX = cos(Math.toRadians(angle.value.toDouble())) * circleRadius + center.x val dotY = sin(Math.toRadians(angle.value.toDouble())) * circleRadius + center.y diff --git a/app/src/main/java/com/joker/kit/core/ui/component/scaffold/AppScaffold.kt b/app/src/main/java/com/joker/kit/core/ui/component/scaffold/AppScaffold.kt new file mode 100644 index 0000000..45b7862 --- /dev/null +++ b/app/src/main/java/com/joker/kit/core/ui/component/scaffold/AppScaffold.kt @@ -0,0 +1,129 @@ +package com.joker.kit.core.ui.component.scaffold + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.TopAppBarColors +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.nestedscroll.nestedScroll +import com.joker.kit.core.ui.component.appbar.CenterTopAppBar +import com.joker.kit.core.ui.component.appbar.LargeTopAppBar + +/** + * 应用通用Scaffold组件 + * + * 该组件封装了Material3 Scaffold,使用CenterTopAppBar作为顶部应用栏, + * 提供了一个统一的页面框架结构,简化常见页面布局的创建。 + * 默认情况下,它会自动处理因顶部和底部栏产生的安全区域padding。 + * + * @param modifier 应用于Scaffold的修饰符 + * @param title 顶部应用栏标题的资源ID,如果为null则不显示标题 + * @param titleText 顶部应用栏标题的字符串,如果为null则不显示标题 + * @param topBarColors 顶部应用栏的颜色配置 + * @param topBarActions 顶部应用栏右侧的操作按钮区域 + * @param showBackIcon 是否显示返回按钮,默认为true + * @param onBackClick 点击返回按钮时的回调函数 + * @param snackbarHostState Snackbar宿主状态 + * @param backgroundColor 页面背景颜色,默认为 MaterialTheme 的 surface 颜色 + * @param bottomBar 底部导航栏的内容,默认为null + * @param floatingActionButton 浮动操作按钮的内容,默认为null + * @param topBar 自定义顶部应用栏,如果提供则优先使用此应用栏 + * @param useLargeTopBar 是否使用大标题样式的顶部应用栏,如果为true则会自动启用滚动行为 + * @param largeTopBarExpandedBackgroundColor 大标题应用栏展开状态下的背景色,仅在useLargeTopBar为true时生效 + * @param largeTopBarCollapsedBackgroundColor 大标题应用栏收起状态下的背景色,仅在useLargeTopBar为true时生效 + * @param contentShouldConsumePadding 是否由内容区域(content)来消费padding。默认为false,即Scaffold的根Box会消费padding。 + * 如果设为true,根Box将不应用padding,而是由content自行处理。 + * @param content 页面主体内容,接收PaddingValues参数以适应顶部应用栏的空间 + * @author Joker.X + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AppScaffold( + modifier: Modifier = Modifier, + title: Int? = null, + titleText: String? = null, + topBarColors: TopAppBarColors = TopAppBarDefaults.topAppBarColors(), + topBarActions: @Composable (RowScope.() -> Unit) = {}, + showBackIcon: Boolean = true, + onBackClick: () -> Unit = {}, + snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }, + backgroundColor: Color = MaterialTheme.colorScheme.background, + bottomBar: @Composable () -> Unit = {}, + floatingActionButton: @Composable () -> Unit = {}, + topBar: @Composable (() -> Unit)? = null, + useLargeTopBar: Boolean = false, + largeTopBarExpandedBackgroundColor: Color = MaterialTheme.colorScheme.background, + largeTopBarCollapsedBackgroundColor: Color = MaterialTheme.colorScheme.background, + contentShouldConsumePadding: Boolean = false, // New parameter + content: @Composable (PaddingValues) -> Unit +) { + val scrollBehavior = if (useLargeTopBar) { + TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()) + } else null + + val finalModifier = if (scrollBehavior != null) { + modifier.nestedScroll(scrollBehavior.nestedScrollConnection) + } else modifier + + Scaffold( + topBar = { + if (topBar != null) { + topBar() + } else if (useLargeTopBar) { + LargeTopAppBar( + title = title, + titleText = titleText, + actions = topBarActions, + onBackClick = onBackClick, + showBackIcon = showBackIcon, + scrollBehavior = scrollBehavior, + expandedBackgroundColor = largeTopBarExpandedBackgroundColor, + collapsedBackgroundColor = largeTopBarCollapsedBackgroundColor + ) + } else { + CenterTopAppBar( + title = title, + titleText = titleText, + colors = topBarColors, + actions = topBarActions, + onBackClick = onBackClick, + showBackIcon = showBackIcon + ) + } + }, + snackbarHost = { SnackbarHost(snackbarHostState) }, + bottomBar = bottomBar, + floatingActionButton = floatingActionButton, + modifier = finalModifier, + content = { paddingValues -> + val boxModifier = if (contentShouldConsumePadding) { + Modifier + .fillMaxSize() + .background(backgroundColor) + } else { + Modifier + .fillMaxSize() + .background(backgroundColor) + .padding(paddingValues) + } + Box( + modifier = boxModifier + ) { + content(paddingValues) + } + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/joker/kit/feature/auth/view/LoginScreen.kt b/app/src/main/java/com/joker/kit/feature/auth/view/LoginScreen.kt index 843c641..8cc5a1a 100644 --- a/app/src/main/java/com/joker/kit/feature/auth/view/LoginScreen.kt +++ b/app/src/main/java/com/joker/kit/feature/auth/view/LoginScreen.kt @@ -1,15 +1,18 @@ package com.joker.kit.feature.auth.view +import androidx.compose.foundation.layout.Column +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.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.ui.component.scaffold.AppScaffold import com.joker.kit.core.ui.component.text.AppText -import com.joker.kit.core.ui.component.text.TextSize import com.joker.kit.feature.auth.viewmodel.LoginViewModel /** @@ -22,23 +25,31 @@ import com.joker.kit.feature.auth.viewmodel.LoginViewModel internal fun LoginRoute( viewModel: LoginViewModel = hiltViewModel() ) { - LoginScreen() + LoginScreen( + onLoginClick = viewModel::login, + onBackClick = viewModel::navigateBack + ) } /** * 登录页面 * * @param onBackClick 返回按钮回调 + * @param onLoginClick 登录按钮回调 * @author Joker.X */ @OptIn(ExperimentalMaterial3Api::class) @Composable internal fun LoginScreen( onBackClick: () -> Unit = {}, + onLoginClick: () -> Unit = {}, ) { - Scaffold { innerPadding -> + AppScaffold( + titleText = "登录", + onBackClick = onBackClick, + ) { LoginContentView( - modifier = Modifier.padding(innerPadding) + onLoginClick = onLoginClick ) } } @@ -46,16 +57,23 @@ internal fun LoginScreen( /** * 登录页面内容 * - * @param modifier 修饰符 + * @param onLoginClick 登录按钮回调 * @author Joker.X */ @Composable -private fun LoginContentView(modifier: Modifier = Modifier) { - AppText( - text = "登录页面", - size = TextSize.TITLE_MEDIUM, - modifier = modifier - ) +private fun LoginContentView( + onLoginClick: () -> Unit = {}, +) { + Column( + modifier = Modifier.padding(SpacePaddingLarge), + ) { + Button( + onClick = onLoginClick, + modifier = Modifier.fillMaxWidth() + ) { + AppText(text = "一键登录") + } + } } /** diff --git a/app/src/main/java/com/joker/kit/feature/auth/viewmodel/LoginViewModel.kt b/app/src/main/java/com/joker/kit/feature/auth/viewmodel/LoginViewModel.kt index 5399f1e..48c9e9b 100644 --- a/app/src/main/java/com/joker/kit/feature/auth/viewmodel/LoginViewModel.kt +++ b/app/src/main/java/com/joker/kit/feature/auth/viewmodel/LoginViewModel.kt @@ -1,9 +1,14 @@ package com.joker.kit.feature.auth.viewmodel +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.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,4 +25,39 @@ class LoginViewModel @Inject constructor( ) : BaseViewModel( navigator = navigator, userState = userState -) +) { + + /** + * 模拟登录:构造假的 Auth/User,写入 UserState,演示路由拦截放行。 + * 真实项目中,这里会是网络请求,登录成功后会有 token 等信息返回。 + */ + fun login() { + viewModelScope.launch { + val fakeAuth = Auth( + token = "demo-token", + refreshToken = "demo-refresh", + expire = 3600, + refreshExpire = 7200, + ) + val fakeUser = User( + id = 1, + nickName = "演示用户", + phone = "18800000000", + ) + userState.updateUserState(fakeAuth, fakeUser) + ToastUtils.show("登录成功") + navigateBack() + } + } + + /** + * 模拟退出登录,清空全局 UserState + */ + fun logout() { + viewModelScope.launch { + userState.logout() + ToastUtils.show("已退出登录") + navigateBack() + } + } +} diff --git a/app/src/main/java/com/joker/kit/feature/demo/navigation/DatabaseNavigation.kt b/app/src/main/java/com/joker/kit/feature/demo/navigation/DatabaseNavigation.kt new file mode 100644 index 0000000..81ec31a --- /dev/null +++ b/app/src/main/java/com/joker/kit/feature/demo/navigation/DatabaseNavigation.kt @@ -0,0 +1,18 @@ +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 + +/** + * 数据库示例页面导航 + */ +@OptIn(ExperimentalSharedTransitionApi::class) +fun NavGraphBuilder.databaseScreen(sharedTransitionScope: SharedTransitionScope) { + composable { + DatabaseRoute() + } +} diff --git a/app/src/main/java/com/joker/kit/feature/demo/navigation/DemoGraph.kt b/app/src/main/java/com/joker/kit/feature/demo/navigation/DemoGraph.kt new file mode 100644 index 0000000..ac99f88 --- /dev/null +++ b/app/src/main/java/com/joker/kit/feature/demo/navigation/DemoGraph.kt @@ -0,0 +1,24 @@ +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 + +/** + * Demo 模块导航图 + */ +@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) +} diff --git a/app/src/main/java/com/joker/kit/feature/demo/navigation/LocalStorageNavigation.kt b/app/src/main/java/com/joker/kit/feature/demo/navigation/LocalStorageNavigation.kt new file mode 100644 index 0000000..b18f21d --- /dev/null +++ b/app/src/main/java/com/joker/kit/feature/demo/navigation/LocalStorageNavigation.kt @@ -0,0 +1,18 @@ +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 + +/** + * 本地存储示例页面导航 + */ +@OptIn(ExperimentalSharedTransitionApi::class) +fun NavGraphBuilder.localStorageScreen(sharedTransitionScope: SharedTransitionScope) { + composable { + LocalStorageRoute() + } +} diff --git a/app/src/main/java/com/joker/kit/feature/demo/navigation/NavigationResultNavigation.kt b/app/src/main/java/com/joker/kit/feature/demo/navigation/NavigationResultNavigation.kt new file mode 100644 index 0000000..d03ed7d --- /dev/null +++ b/app/src/main/java/com/joker/kit/feature/demo/navigation/NavigationResultNavigation.kt @@ -0,0 +1,18 @@ +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 + +/** + * 结果回传示例页面导航 + */ +@OptIn(ExperimentalSharedTransitionApi::class) +fun NavGraphBuilder.navigationResultScreen(sharedTransitionScope: SharedTransitionScope) { + composable { + NavigationResultRoute() + } +} diff --git a/app/src/main/java/com/joker/kit/feature/demo/navigation/NavigationWithArgsNavigation.kt b/app/src/main/java/com/joker/kit/feature/demo/navigation/NavigationWithArgsNavigation.kt new file mode 100644 index 0000000..e39bbd5 --- /dev/null +++ b/app/src/main/java/com/joker/kit/feature/demo/navigation/NavigationWithArgsNavigation.kt @@ -0,0 +1,18 @@ +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 + +/** + * 带参跳转示例页面导航 + */ +@OptIn(ExperimentalSharedTransitionApi::class) +fun NavGraphBuilder.navigationWithArgsScreen(sharedTransitionScope: SharedTransitionScope) { + composable { + NavigationWithArgsRoute() + } +} diff --git a/app/src/main/java/com/joker/kit/feature/demo/navigation/NetworkDemoNavigation.kt b/app/src/main/java/com/joker/kit/feature/demo/navigation/NetworkDemoNavigation.kt new file mode 100644 index 0000000..4121aa3 --- /dev/null +++ b/app/src/main/java/com/joker/kit/feature/demo/navigation/NetworkDemoNavigation.kt @@ -0,0 +1,18 @@ +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 页面导航 + */ +@OptIn(ExperimentalSharedTransitionApi::class) +fun NavGraphBuilder.networkDemoScreen(sharedTransitionScope: SharedTransitionScope) { + composable { + NetworkDemoRoute() + } +} diff --git a/app/src/main/java/com/joker/kit/feature/demo/navigation/NetworkListDemoNavigation.kt b/app/src/main/java/com/joker/kit/feature/demo/navigation/NetworkListDemoNavigation.kt new file mode 100644 index 0000000..2e2975b --- /dev/null +++ b/app/src/main/java/com/joker/kit/feature/demo/navigation/NetworkListDemoNavigation.kt @@ -0,0 +1,18 @@ +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 页面导航 + */ +@OptIn(ExperimentalSharedTransitionApi::class) +fun NavGraphBuilder.networkListDemoScreen(sharedTransitionScope: SharedTransitionScope) { + composable { + NetworkListDemoRoute() + } +} diff --git a/app/src/main/java/com/joker/kit/feature/demo/navigation/NetworkRequestNavigation.kt b/app/src/main/java/com/joker/kit/feature/demo/navigation/NetworkRequestNavigation.kt new file mode 100644 index 0000000..968bb2d --- /dev/null +++ b/app/src/main/java/com/joker/kit/feature/demo/navigation/NetworkRequestNavigation.kt @@ -0,0 +1,18 @@ +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 + +/** + * 网络请求示例页面导航 + */ +@OptIn(ExperimentalSharedTransitionApi::class) +fun NavGraphBuilder.networkRequestScreen(sharedTransitionScope: SharedTransitionScope) { + composable { + NetworkRequestRoute() + } +} diff --git a/app/src/main/java/com/joker/kit/feature/demo/navigation/StateManagementNavigation.kt b/app/src/main/java/com/joker/kit/feature/demo/navigation/StateManagementNavigation.kt new file mode 100644 index 0000000..3be3695 --- /dev/null +++ b/app/src/main/java/com/joker/kit/feature/demo/navigation/StateManagementNavigation.kt @@ -0,0 +1,18 @@ +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 + +/** + * 状态管理示例页面导航 + */ +@OptIn(ExperimentalSharedTransitionApi::class) +fun NavGraphBuilder.stateManagementScreen(sharedTransitionScope: SharedTransitionScope) { + composable { + StateManagementRoute() + } +} diff --git a/app/src/main/java/com/joker/kit/feature/demo/view/DatabaseScreen.kt b/app/src/main/java/com/joker/kit/feature/demo/view/DatabaseScreen.kt new file mode 100644 index 0000000..404a68b --- /dev/null +++ b/app/src/main/java/com/joker/kit/feature/demo/view/DatabaseScreen.kt @@ -0,0 +1,375 @@ +package com.joker.kit.feature.demo.view + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ListItem +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import com.joker.kit.core.database.entity.DemoEntity +import com.joker.kit.core.designsystem.component.CenterColumn +import com.joker.kit.core.designsystem.theme.AppTheme +import com.joker.kit.core.designsystem.theme.ShapeMedium +import com.joker.kit.core.designsystem.theme.SpaceHorizontalSmall +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.designsystem.theme.SpaceVerticalSmall +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 +import com.joker.kit.core.ui.component.text.TextSize +import com.joker.kit.core.ui.component.text.TextType +import com.joker.kit.feature.demo.viewmodel.DatabaseViewModel +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +/** + * 数据库示例路由 + * + * @param viewModel Hilt 注入的 DatabaseViewModel + */ +@Composable +internal fun DatabaseRoute( + viewModel: DatabaseViewModel = hiltViewModel() +) { + // 列表数据(Demo 表 Flow -> State) + val items by viewModel.items.collectAsState() + // 标题输入状态 + val title by viewModel.title.collectAsState() + // 描述输入状态 + val description by viewModel.description.collectAsState() + + DatabaseScreen( + title = title, + description = description, + items = items, + onTitleChange = viewModel::onTitleChange, + onDescriptionChange = viewModel::onDescriptionChange, + onAddClick = viewModel::addItem, + onDeleteItem = viewModel::deleteItem, + onClearAll = viewModel::clearAll, + onBackClick = viewModel::navigateBack + ) +} + +/** + * 数据库示例界面 + * + * @param title 标题输入框内容 + * @param description 描述输入框内容 + * @param items Demo 表数据 + * @param onTitleChange 标题输入变化回调 + * @param onDescriptionChange 描述输入变化回调 + * @param onAddClick 点击新增记录 + * @param onDeleteItem 删除指定记录 + * @param onClearAll 清空所有记录 + * @param onBackClick 返回按钮回调 + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun DatabaseScreen( + title: String = "", + description: String = "", + items: List = emptyList(), + onTitleChange: (String) -> Unit = {}, + onDescriptionChange: (String) -> Unit = {}, + onAddClick: () -> Unit = {}, + onDeleteItem: (Long) -> Unit = {}, + onClearAll: () -> Unit = {}, + onBackClick: () -> Unit = {}, +) { + AppScaffold( + titleText = "数据库", + onBackClick = onBackClick + ) { + DatabaseContent( + title = title, + description = description, + items = items, + onTitleChange = onTitleChange, + onDescriptionChange = onDescriptionChange, + onAddClick = onAddClick, + onDeleteItem = onDeleteItem, + onClearAll = onClearAll + ) + } +} + +/** + * 数据库内容视图 + * + * @param title 标题输入 + * @param description 描述输入 + * @param items Demo 列表数据 + * @param onTitleChange 标题更新回调 + * @param onDescriptionChange 描述更新回调 + * @param onAddClick 新增记录回调 + * @param onDeleteItem 删除记录回调 + * @param onClearAll 清空列表回调 + */ +@Composable +private fun DatabaseContent( + title: String, + description: String, + items: List, + onTitleChange: (String) -> Unit, + onDescriptionChange: (String) -> Unit, + onAddClick: () -> Unit, + onDeleteItem: (Long) -> Unit, + onClearAll: () -> Unit +) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(SpacePaddingMedium), + verticalArrangement = Arrangement.spacedBy(SpaceVerticalLarge) + ) { + InputCard( + title = title, + description = description, + onTitleChange = onTitleChange, + onDescriptionChange = onDescriptionChange, + onAddClick = onAddClick, + onClearAll = onClearAll, + canClear = items.isNotEmpty() + ) + + DemoListCard( + items = items, + onDeleteItem = onDeleteItem + ) + } +} + +/** + * 新增/清空操作区域 + * + * @param title 标题输入 + * @param description 描述输入 + * @param onTitleChange 标题变化回调 + * @param onDescriptionChange 描述变化回调 + * @param onAddClick 新增记录 + * @param onClearAll 清空全部 + * @param canClear 是否允许清空按钮启用 + */ +@Composable +private fun InputCard( + title: String, + description: String, + onTitleChange: (String) -> Unit, + onDescriptionChange: (String) -> Unit, + onAddClick: () -> Unit, + onClearAll: () -> Unit, + canClear: Boolean +) { + Card { + Column( + modifier = Modifier.padding(SpacePaddingLarge), + verticalArrangement = Arrangement.spacedBy(SpaceVerticalMedium) + ) { + AppText( + text = "用 DemoRepository 做增删改查", + size = TextSize.TITLE_MEDIUM + ) + AppText( + text = "输入标题即可新增一条记录,描述可选;列表来自 DemoRepository.observeItems() 的 Flow。", + type = TextType.TERTIARY, + size = TextSize.BODY_MEDIUM + ) + + OutlinedTextField( + value = title, + onValueChange = onTitleChange, + label = { Text("标题(必填)") }, + singleLine = true, + shape = ShapeMedium, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), + modifier = Modifier.fillMaxWidth() + ) + + OutlinedTextField( + value = description, + onValueChange = onDescriptionChange, + label = { Text("描述(可选)") }, + shape = ShapeMedium, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + modifier = Modifier.fillMaxWidth() + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(SpaceHorizontalSmall, Alignment.End), + verticalAlignment = Alignment.CenterVertically + ) { + TextButton( + onClick = onClearAll, + enabled = canClear + ) { + Text("清空全部") + } + Button( + onClick = onAddClick, + enabled = title.isNotBlank() + ) { + Text("新增记录") + } + } + } + } +} + +/** + * Demo 列表卡片 + * + * @param items Demo 数据列表 + * @param onDeleteItem 删除单条记录回调 + */ +@Composable +private fun DemoListCard( + items: List, + onDeleteItem: (Long) -> Unit +) { + Card { + AppText( + text = "Demo 表当前 ${items.size} 条", + size = TextSize.TITLE_MEDIUM, + modifier = Modifier.padding( + horizontal = SpacePaddingLarge, + vertical = SpacePaddingMedium + ) + ) + + Divider() + + if (items.isEmpty()) { + CenterColumn( + modifier = Modifier + .fillMaxSize() + .padding(SpacePaddingLarge), + ) { + AppText(text = "列表为空,先添加一条吧", type = TextType.SECONDARY) + } + } else { + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(SpacePaddingMedium), + verticalArrangement = Arrangement.spacedBy(SpaceVerticalSmall) + ) { + items(items, key = { it.id }) { item -> + DemoListItem( + item = item, + onDeleteItem = onDeleteItem + ) + } + item { Spacer(modifier = Modifier.height(SpaceVerticalLarge)) } + } + } + } +} + +/** + * 单条 Demo 展示 + * + * @param item Demo 实体 + * @param onDeleteItem 删除该条记录回调 + */ +@Composable +private fun DemoListItem( + item: DemoEntity, + onDeleteItem: (Long) -> Unit +) { + val formattedTime = remember(item.updatedAt) { + SimpleDateFormat("MM-dd HH:mm", Locale.getDefault()).format(Date(item.updatedAt)) + } + + ListItem( + headlineContent = { + AppText( + text = item.title.ifBlank { "未命名记录" }, + size = TextSize.TITLE_MEDIUM, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + }, + supportingContent = { + val desc = item.description.ifBlank { "暂无描述" } + AppText( + text = "$desc · 更新于 $formattedTime", + type = TextType.SECONDARY, + size = TextSize.BODY_MEDIUM, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + }, + trailingContent = { + TextButton(onClick = { onDeleteItem(item.id) }) { + Text("删除") + } + } + ) +} + +/** + * 数据库界面浅色主题预览 + */ +@Preview(showBackground = true) +@Composable +private fun DatabasePreview() { + AppTheme { + DatabaseScreen( + title = "标题", + description = "描述", + items = previewDemoItems() + ) + } +} + +/** + * 数据库界面深色主题预览 + */ +@Preview(showBackground = true) +@Composable +private fun DatabasePreviewDark() { + AppTheme(darkTheme = true) { + DatabaseScreen( + title = "标题", + description = "描述", + items = previewDemoItems() + ) + } +} + +/** + * 预览用 Demo 数据 + */ +private fun previewDemoItems() = listOf( + DemoEntity(id = 1, title = "演示标题 A", description = "这是第一条记录"), + DemoEntity(id = 2, title = "演示标题 B", description = "描述可以留空"), + DemoEntity(id = 3, title = "演示标题 C", description = "点击右侧图标删除") +) diff --git a/app/src/main/java/com/joker/kit/feature/demo/view/LocalStorageScreen.kt b/app/src/main/java/com/joker/kit/feature/demo/view/LocalStorageScreen.kt new file mode 100644 index 0000000..ac945d1 --- /dev/null +++ b/app/src/main/java/com/joker/kit/feature/demo/view/LocalStorageScreen.kt @@ -0,0 +1,300 @@ +package com.joker.kit.feature.demo.view + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +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.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.ImeAction +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.ShapeMedium +import com.joker.kit.core.designsystem.theme.SpaceHorizontalSmall +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.model.entity.User +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 +import com.joker.kit.core.ui.component.text.TextType +import com.joker.kit.feature.demo.viewmodel.LocalStorageViewModel + +/** + * 本地存储示例路由 + * + * @param viewModel Hilt 注入的 LocalStorageViewModel + */ +@Composable +internal fun LocalStorageRoute( + viewModel: LocalStorageViewModel = hiltViewModel() +) { + // 用户 ID 输入状态 + val userId by viewModel.userId.collectAsState() + // 昵称输入状态 + val nickName by viewModel.nickName.collectAsState() + // 头像链接输入状态 + val avatar by viewModel.avatar.collectAsState() + // 当前已保存的用户信息 + val user by viewModel.user.collectAsState() + + LocalStorageScreen( + userId = userId, + nickName = nickName, + avatar = avatar, + user = user, + onUserIdChange = viewModel::onUserIdChange, + onNickNameChange = viewModel::onNickNameChange, + onAvatarChange = viewModel::onAvatarChange, + onSaveUser = viewModel::saveUser, + onClearUser = viewModel::clearUser, + onReloadUser = viewModel::loadUser, + onBackClick = viewModel::navigateBack + ) +} + +/** + * 本地存储示例界面 + * + * @param userId 用户 ID 输入值 + * @param nickName 用户昵称输入值 + * @param avatar 头像链接输入值 + * @param user 当前已保存的用户信息 + * @param onUserIdChange 用户 ID 文本变化回调 + * @param onNickNameChange 昵称文本变化回调 + * @param onAvatarChange 头像文本变化回调 + * @param onSaveUser 保存用户信息 + * @param onClearUser 清除用户信息 + * @param onReloadUser 重新读取用户信息 + * @param onBackClick 返回按钮回调 + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun LocalStorageScreen( + userId: String = "", + nickName: String = "", + avatar: String = "", + user: User? = null, + onUserIdChange: (String) -> Unit = {}, + onNickNameChange: (String) -> Unit = {}, + onAvatarChange: (String) -> Unit = {}, + onSaveUser: () -> Unit = {}, + onClearUser: () -> Unit = {}, + onReloadUser: () -> Unit = {}, + onBackClick: () -> Unit = {}, +) { + AppScaffold( + titleText = "本地存储", + onBackClick = onBackClick, + contentShouldConsumePadding = true + ) { + LocalStorageContent( + userId = userId, + nickName = nickName, + avatar = avatar, + user = user, + onUserIdChange = onUserIdChange, + onNickNameChange = onNickNameChange, + onAvatarChange = onAvatarChange, + onSaveUser = onSaveUser, + onClearUser = onClearUser, + onReloadUser = onReloadUser + ) + } +} + +/** + * 本地存储内容视图 + * + * @param userId 用户 ID 输入 + * @param nickName 昵称输入 + * @param avatar 头像输入 + * @param user 已保存的用户信息 + * @param onUserIdChange 用户 ID 更新回调 + * @param onNickNameChange 昵称更新回调 + * @param onAvatarChange 头像更新回调 + * @param onSaveUser 保存用户 + * @param onClearUser 清除用户 + * @param onReloadUser 重新读取用户 + */ +@Composable +private fun LocalStorageContent( + userId: String, + nickName: String, + avatar: String, + user: User?, + onUserIdChange: (String) -> Unit, + onNickNameChange: (String) -> Unit, + onAvatarChange: (String) -> Unit, + onSaveUser: () -> Unit, + onClearUser: () -> Unit, + onReloadUser: () -> Unit +) { + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(SpacePaddingMedium), + verticalArrangement = Arrangement.spacedBy(SpaceVerticalLarge) + ) { + UserCard( + userId = userId, + nickName = nickName, + avatar = avatar, + user = user, + onUserIdChange = onUserIdChange, + onNickNameChange = onNickNameChange, + onAvatarChange = onAvatarChange, + onSaveUser = onSaveUser, + onClearUser = onClearUser, + onReloadUser = onReloadUser + ) + } +} + +/** + * 用户信息存储卡片 + * + * @param userId 用户 ID 输入 + * @param nickName 用户昵称输入 + * @param avatar 头像链接输入 + * @param user 已保存的用户信息 + * @param onUserIdChange 用户 ID 更新 + * @param onNickNameChange 昵称更新 + * @param onAvatarChange 头像更新 + * @param onSaveUser 保存用户 + * @param onClearUser 清除用户 + * @param onReloadUser 重新读取用户 + */ +@Composable +private fun UserCard( + userId: String, + nickName: String, + avatar: String, + user: User?, + onUserIdChange: (String) -> Unit, + onNickNameChange: (String) -> Unit, + onAvatarChange: (String) -> Unit, + onSaveUser: () -> Unit, + onClearUser: () -> Unit, + onReloadUser: () -> Unit +) { + Card( + modifier = Modifier.fillMaxWidth(), + shape = ShapeMedium + ) { + Column( + modifier = Modifier.padding(SpacePaddingLarge), + verticalArrangement = Arrangement.spacedBy(SpaceVerticalMedium) + ) { + AppText(text = "UserInfoStoreDataSource 示例", size = TextSize.TITLE_MEDIUM) + AppText( + text = "保存用户 id / 昵称 / 头像,并可随时清理与重读。", + type = TextType.TERTIARY, + size = TextSize.BODY_MEDIUM + ) + + OutlinedTextField( + value = userId, + onValueChange = onUserIdChange, + label = { Text("用户 ID (数字)") }, + singleLine = true, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), + modifier = Modifier.fillMaxWidth() + ) + + OutlinedTextField( + value = nickName, + onValueChange = onNickNameChange, + label = { Text("昵称") }, + singleLine = true, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), + modifier = Modifier.fillMaxWidth() + ) + + OutlinedTextField( + value = avatar, + onValueChange = onAvatarChange, + label = { Text("头像链接 (可选)") }, + singleLine = true, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + modifier = Modifier.fillMaxWidth() + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(SpaceHorizontalSmall, Alignment.End) + ) { + TextButton(onClick = onReloadUser) { Text("重新读取") } + TextButton(onClick = onClearUser) { Text("清除") } + Button(onClick = onSaveUser, enabled = userId.isNotBlank()) { + Text("保存用户") + } + } + + Divider() + + val userText = user?.let { + val name = it.nickName ?: "未设置昵称" + val avatarUrl = it.avatarUrl ?: "无头像" + "当前用户: id=${it.id}, 昵称=$name\n头像=$avatarUrl" + } ?: "暂无本地用户信息" + + AppText( + text = userText, + type = TextType.SECONDARY, + size = TextSize.BODY_MEDIUM + ) + } + } +} + +/** + * 本地存储界面浅色主题预览 + */ +@Preview(showBackground = true) +@Composable +private fun LocalStoragePreview() { + AppTheme { + LocalStorageScreen( + userId = "1", + nickName = "预览用户", + avatar = "https://example.com/avatar.png", + user = User(id = 1, nickName = "预览用户", avatarUrl = null) + ) + } +} + +/** + * 本地存储界面深色主题预览 + */ +@Preview(showBackground = true) +@Composable +private fun LocalStoragePreviewDark() { + AppTheme(darkTheme = true) { + LocalStorageScreen( + userId = "1", + nickName = "预览用户", + avatar = "https://example.com/avatar.png", + user = User(id = 1, nickName = "预览用户", avatarUrl = null) + ) + } +} diff --git a/app/src/main/java/com/joker/kit/feature/demo/view/NavigationResultScreen.kt b/app/src/main/java/com/joker/kit/feature/demo/view/NavigationResultScreen.kt new file mode 100644 index 0000000..c31d0f6 --- /dev/null +++ b/app/src/main/java/com/joker/kit/feature/demo/view/NavigationResultScreen.kt @@ -0,0 +1,85 @@ +package com.joker.kit.feature.demo.view + +import androidx.compose.foundation.layout.Column +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.runtime.Composable +import androidx.compose.ui.Modifier +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.ui.component.scaffold.AppScaffold +import com.joker.kit.core.ui.component.text.AppText +import com.joker.kit.feature.demo.viewmodel.NavigationResultViewModel + +/** + * 结果回传示例路由 + */ +@Composable +internal fun NavigationResultRoute( + viewModel: NavigationResultViewModel = hiltViewModel() +) { + NavigationResultScreen( + onBackClick = viewModel::navigateBack, + onSendResult = viewModel::sendResultAndBack + ) +} + +/** + * 结果回传示例界面 + * + * @param onBackClick 返回按钮回调 + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun NavigationResultScreen( + onBackClick: () -> Unit = {}, + onSendResult: () -> Unit = {}, +) { + AppScaffold( + titleText = "结果回传", + onBackClick = onBackClick + ) { + NavigationResultContent(onSendResult = onSendResult) + } +} + +/** + * 结果回传内容视图 + */ +@Composable +private fun NavigationResultContent(onSendResult: () -> Unit) { + Column(modifier = Modifier.padding(SpacePaddingLarge)) { + Button( + onClick = onSendResult, + modifier = Modifier.fillMaxWidth() + ) { + AppText(text = "回传结果并返回") + } + } +} + +/** + * 结果回传界面浅色主题预览 + */ +@Preview(showBackground = true) +@Composable +private fun NavigationResultPreview() { + AppTheme { + NavigationResultScreen() + } +} + +/** + * 结果回传界面深色主题预览 + */ +@Preview(showBackground = true) +@Composable +private fun NavigationResultPreviewDark() { + AppTheme(darkTheme = true) { + NavigationResultScreen() + } +} diff --git a/app/src/main/java/com/joker/kit/feature/demo/view/NavigationWithArgsScreen.kt b/app/src/main/java/com/joker/kit/feature/demo/view/NavigationWithArgsScreen.kt new file mode 100644 index 0000000..a3413d2 --- /dev/null +++ b/app/src/main/java/com/joker/kit/feature/demo/view/NavigationWithArgsScreen.kt @@ -0,0 +1,72 @@ +package com.joker.kit.feature.demo.view + +import androidx.compose.material3.ExperimentalMaterial3Api +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.ui.component.scaffold.AppScaffold +import com.joker.kit.core.ui.component.text.AppText +import com.joker.kit.feature.demo.viewmodel.NavigationWithArgsViewModel + +/** + * 带参跳转示例路由 + */ +@Composable +internal fun NavigationWithArgsRoute( + viewModel: NavigationWithArgsViewModel = hiltViewModel() +) { + NavigationWithArgsScreen( + goodsId = viewModel.goodsId, + onBackClick = viewModel::navigateBack + ) +} + +/** + * 带参跳转示例界面 + * + * @param onBackClick 返回按钮回调 + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun NavigationWithArgsScreen( + goodsId: Long = 0, + onBackClick: () -> Unit = {}, +) { + AppScaffold( + titleText = "带参跳转", + onBackClick = onBackClick + ) { + NavigationWithArgsContent(goodsId = goodsId) + } +} + +/** + * 带参跳转内容视图 + */ +@Composable +private fun NavigationWithArgsContent(goodsId: Long) { + AppText(text = "传递的商品ID:$goodsId") +} + +/** + * 带参跳转界面浅色主题预览 + */ +@Preview(showBackground = true) +@Composable +private fun NavigationWithArgsPreview() { + AppTheme { + NavigationWithArgsScreen(goodsId = 1) + } +} + +/** + * 带参跳转界面深色主题预览 + */ +@Preview(showBackground = true) +@Composable +private fun NavigationWithArgsPreviewDark() { + AppTheme(darkTheme = true) { + NavigationWithArgsScreen(goodsId = 1) + } +} diff --git a/app/src/main/java/com/joker/kit/feature/demo/view/NetworkDemoScreen.kt b/app/src/main/java/com/joker/kit/feature/demo/view/NetworkDemoScreen.kt new file mode 100644 index 0000000..1c450ea --- /dev/null +++ b/app/src/main/java/com/joker/kit/feature/demo/view/NetworkDemoScreen.kt @@ -0,0 +1,112 @@ +package com.joker.kit.feature.demo.view + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +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 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.ui.component.network.BaseNetWorkView +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.NetworkDemoViewModel + +/** + * Network Demo 路由 + */ +@Composable +internal fun NetworkDemoRoute( + viewModel: NetworkDemoViewModel = hiltViewModel() +) { + // 收集 UI 状态 + val uiState by viewModel.uiState.collectAsState() + + NetworkDemoScreen( + uiState = uiState, + onBackClick = viewModel::navigateBack, + onRetry = viewModel::retryRequest + ) +} + +/** + * Network Demo 界面 + * + * @param uiState UI 状态 + * @param onBackClick 返回按钮回调 + * @param onRetry 重试回调 + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun NetworkDemoScreen( + uiState: BaseNetWorkUiState = BaseNetWorkUiState.Loading, + onBackClick: () -> Unit = {}, + onRetry: () -> Unit = {}, +) { + AppScaffold( + titleText = "Network Demo", + onBackClick = onBackClick, + ) { + BaseNetWorkView( + uiState = uiState, + onRetry = onRetry + ) { data -> + NetworkDemoContent(data = data) + } + } +} + +/** + * Network Demo 内容视图 + * + * @param data 商品数据 + */ +@Composable +private fun NetworkDemoContent(data: Goods) { + Column(modifier = Modifier.padding(SpacePaddingMedium)) { + AppText(text = "商品名称:${data.title}") + AppText(text = "副标题:${data.subTitle ?: "暂无"}") + AppText(text = "价格:¥${data.price}") + AppText(text = "已售:${data.sold} 件") + } +} + +/** + * Network Demo 界面浅色主题预览 + */ +@Preview(showBackground = true) +@Composable +private fun NetworkDemoPreview() { + AppTheme { + NetworkDemoScreen(uiState = BaseNetWorkUiState.Success(mockGoods())) + } +} + +/** + * Network Demo 界面深色主题预览 + */ +@Preview(showBackground = true) +@Composable +private fun NetworkDemoPreviewDark() { + AppTheme(darkTheme = true) { + NetworkDemoScreen(uiState = BaseNetWorkUiState.Success(mockGoods())) + } +} + +/** + * 模拟商品数据 + */ +private fun mockGoods() = Goods( + id = 1, + title = "手机", + subTitle = "示例副标题", + mainPic = "", + price = 199, + sold = 88, +) diff --git a/app/src/main/java/com/joker/kit/feature/demo/view/NetworkListDemoScreen.kt b/app/src/main/java/com/joker/kit/feature/demo/view/NetworkListDemoScreen.kt new file mode 100644 index 0000000..c79c3b2 --- /dev/null +++ b/app/src/main/java/com/joker/kit/feature/demo/view/NetworkListDemoScreen.kt @@ -0,0 +1,184 @@ +package com.joker.kit.feature.demo.view + +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ListItem +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.tooling.preview.Preview +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import com.joker.kit.core.base.state.BaseNetWorkListUiState +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.ui.component.network.BaseNetWorkListView +import com.joker.kit.core.ui.component.refresh.RefreshLayout +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.NetworkListDemoViewModel + +/** + * Network List Demo 路由 + */ +@Composable +internal fun NetworkListDemoRoute( + viewModel: NetworkListDemoViewModel = hiltViewModel() +) { + // 收集 ui 状态 + val uiState by viewModel.uiState.collectAsState() + // 收集列表数据 + val listData by viewModel.listData.collectAsState() + // 收集刷新状态 + val isRefreshing by viewModel.isRefreshing.collectAsState() + // 收集加载更多状态 + val loadMoreState by viewModel.loadMoreState.collectAsState() + + NetworkListDemoScreen( + uiState = uiState, + list = listData, + isRefreshing = isRefreshing, + loadMoreState = loadMoreState, + onRefresh = viewModel::onRefresh, + onLoadMore = viewModel::onLoadMore, + shouldTriggerLoadMore = viewModel::shouldTriggerLoadMore, + onBackClick = viewModel::navigateBack, + onRetry = viewModel::retryRequest, + ) +} + +/** + * Network List Demo 界面 + * + * @param uiState 网络列表 UI 状态 + * @param list 商品列表数据 + * @param isRefreshing 是否正在刷新 + * @param loadMoreState 加载更多状态 + * @param onRefresh 刷新回调 + * @param onLoadMore 加载更多回调 + * @param shouldTriggerLoadMore 是否触发加载更多 + * @param onBackClick 返回回调 + * @param onRetry 重试回调 + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun NetworkListDemoScreen( + uiState: BaseNetWorkListUiState = BaseNetWorkListUiState.Loading, + list: List = emptyList(), + isRefreshing: Boolean = false, + loadMoreState: LoadMoreState = LoadMoreState.PullToLoad, + onRefresh: () -> Unit = {}, + onLoadMore: () -> Unit = {}, + shouldTriggerLoadMore: (lastIndex: Int, totalCount: Int) -> Boolean = { _, _ -> false }, + onBackClick: () -> Unit = {}, + onRetry: () -> Unit = {}, +) { + AppScaffold( + titleText = "Network List Demo", + onBackClick = onBackClick + ) { + BaseNetWorkListView( + uiState = uiState, + onRetry = onRetry + ) { + NetworkListDemoContent( + list = list, + isRefreshing = isRefreshing, + loadMoreState = loadMoreState, + onRefresh = onRefresh, + onLoadMore = onLoadMore, + shouldTriggerLoadMore = shouldTriggerLoadMore + ) + } + } +} + +/** + * Network List Demo 内容视图 + * + * @param list 商品列表数据 + * @param isRefreshing 是否正在刷新 + * @param loadMoreState 加载更多状态 + * @param onRefresh 刷新回调 + * @param onLoadMore 加载更多回调 + * @param shouldTriggerLoadMore 是否触发加载更多 + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun NetworkListDemoContent( + list: List, + isRefreshing: Boolean, + loadMoreState: LoadMoreState, + onRefresh: () -> Unit, + onLoadMore: () -> Unit, + shouldTriggerLoadMore: (lastIndex: Int, totalCount: Int) -> Boolean +) { + RefreshLayout( + isRefreshing = isRefreshing, + loadMoreState = loadMoreState, + onRefresh = onRefresh, + onLoadMore = onLoadMore, + shouldTriggerLoadMore = shouldTriggerLoadMore + ) { + itemsIndexed(list) { _, item -> + GoodsListItem(goods = item) + } + } +} + +/** + * 简单展示商品信息的列表项 + */ +@Composable +private fun GoodsListItem(goods: Goods) { + ListItem( + modifier = Modifier.clip(ShapeMedium), + headlineContent = { AppText(text = goods.title.ifBlank { "未命名商品" }) }, + supportingContent = { + AppText(text = goods.subTitle?.ifBlank { "暂无描述" } ?: "暂无描述") + }, + trailingContent = { AppText(text = "¥${goods.price}") }, + ) +} + +/** + * Network List Demo 界面浅色主题预览 + */ +@Preview(showBackground = true) +@Composable +private fun NetworkListDemoPreview() { + AppTheme { + NetworkListDemoScreen( + uiState = BaseNetWorkListUiState.Success, + list = previewGoodsList(), + loadMoreState = LoadMoreState.PullToLoad + ) + } +} + +/** + * Network List Demo 界面深色主题预览 + */ +@Preview(showBackground = true) +@Composable +private fun NetworkListDemoPreviewDark() { + AppTheme(darkTheme = true) { + NetworkListDemoScreen( + uiState = BaseNetWorkListUiState.Success, + list = previewGoodsList(), + loadMoreState = LoadMoreState.PullToLoad + ) + } +} + +/** + * 预览用商品列表数据 + */ +private fun previewGoodsList() = listOf( + Goods(id = 1, title = "小米手机 14", subTitle = "直屏旗舰", price = 3999, sold = 5000), + Goods(id = 2, title = "Apple AirPods", subTitle = "二代", price = 1299, sold = 8000), + Goods(id = 3, title = "Switch OLED", subTitle = "游戏机", price = 2599, sold = 3000), +) diff --git a/app/src/main/java/com/joker/kit/feature/demo/view/NetworkRequestScreen.kt b/app/src/main/java/com/joker/kit/feature/demo/view/NetworkRequestScreen.kt new file mode 100644 index 0000000..f5bf85e --- /dev/null +++ b/app/src/main/java/com/joker/kit/feature/demo/view/NetworkRequestScreen.kt @@ -0,0 +1,153 @@ +package com.joker.kit.feature.demo.view + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +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.designsystem.theme.SpacePaddingMedium +import com.joker.kit.core.model.entity.Goods +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 + +/** + * 网络请求示例路由 + */ +@Composable +internal fun NetworkRequestRoute( + viewModel: NetworkRequestViewModel = hiltViewModel() +) { + // 商品信息 + val goods by viewModel.goods.collectAsState() + + NetworkRequestScreen( + goods = goods, + onBackClick = viewModel::navigateBack, + onRequestClick = viewModel::onRequestClick + ) +} + +/** + * 网络请求示例界面 + * + * @param goods 商品信息 + * @param onBackClick 返回按钮回调 + * @param onRequestClick 请求按钮回调 + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun NetworkRequestScreen( + goods: Goods? = null, + onBackClick: () -> Unit = {}, + onRequestClick: () -> Unit = {}, +) { + AppScaffold( + titleText = "网络请求", + onBackClick = onBackClick + ) { + NetworkRequestContent( + goods = goods, + onRequestClick = onRequestClick + ) + } +} + +/** + * 网络请求内容视图 + * + * @param goods 商品信息 + * @param onRequestClick 请求按钮回调 + */ +@Composable +private fun NetworkRequestContent( + goods: Goods?, + onRequestClick: () -> Unit, +) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(SpacePaddingLarge), + verticalArrangement = Arrangement.spacedBy(SpacePaddingMedium), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Button( + onClick = onRequestClick, + modifier = Modifier.fillMaxWidth() + ) { + AppText(text = "发起网络请求") + } + + if (goods != null) { + CardResult(goods = goods) + } + } +} + +/** + * 网络请求结果卡片视图 + * + * @param goods 商品信息 + */ +@Composable +private fun CardResult(goods: Goods) { + Card( + modifier = Modifier.fillMaxWidth() + ) { + Column(modifier = Modifier.padding(SpacePaddingMedium)) { + AppText(text = "商品名称:${goods.title}") + AppText(text = "副标题:${goods.subTitle ?: "暂无"}") + AppText(text = "价格:¥${goods.price}") + AppText(text = "已售:${goods.sold} 件") + } + } +} + +/** + * 模拟商品信息 + */ +private fun mockGoods() = Goods( + id = 1, + title = "手机", + subTitle = "示例副标题", + mainPic = "", + price = 199, + sold = 88, +) + +/** + * 网络请求界面浅色主题预览 + */ +@Preview(showBackground = true) +@Composable +private fun NetworkRequestPreview() { + AppTheme { + NetworkRequestScreen( + goods = mockGoods(), + ) + } +} + +/** + * 网络请求界面深色主题预览 + */ +@Preview(showBackground = true) +@Composable +private fun NetworkRequestPreviewDark() { + AppTheme(darkTheme = true) { + NetworkRequestScreen( + goods = mockGoods(), + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/joker/kit/feature/demo/view/StateManagementScreen.kt b/app/src/main/java/com/joker/kit/feature/demo/view/StateManagementScreen.kt new file mode 100644 index 0000000..d7e6334 --- /dev/null +++ b/app/src/main/java/com/joker/kit/feature/demo/view/StateManagementScreen.kt @@ -0,0 +1,224 @@ +package com.joker.kit.feature.demo.view + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +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.ShapeMedium +import com.joker.kit.core.designsystem.theme.SpaceHorizontalSmall +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.ui.component.scaffold.AppScaffold +import com.joker.kit.core.ui.component.text.AppText +import com.joker.kit.core.ui.component.text.TextSize +import com.joker.kit.core.ui.component.text.TextType +import com.joker.kit.feature.demo.viewmodel.StateManagementViewModel + +/** + * 状态管理示例路由 + * + * @param viewModel Hilt 注入的 StateManagementViewModel + */ +@Composable +internal fun StateManagementRoute( + viewModel: StateManagementViewModel = hiltViewModel() +) { + // 从 ViewModel 中收集 count 状态 + val count by viewModel.count.collectAsState() + + StateManagementScreen( + count = count, + onIncrease = viewModel::increase, + onDecrease = viewModel::decrease, + onReset = viewModel::reset, + onBackClick = viewModel::navigateBack + ) +} + +/** + * 状态管理示例界面 + * + * @param count 计数器值 + * @param onIncrease +1 回调 + * @param onDecrease -1 回调 + * @param onReset 重置回调 + * @param onBackClick 返回按钮回调 + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun StateManagementScreen( + count: Int = 0, + onIncrease: () -> Unit = {}, + onDecrease: () -> Unit = {}, + onReset: () -> Unit = {}, + onBackClick: () -> Unit = {}, +) { + AppScaffold( + titleText = "状态管理", + onBackClick = onBackClick, + ) { + StateManagementContent( + count = count, + onIncrease = onIncrease, + onDecrease = onDecrease, + onReset = onReset + ) + } +} + +/** + * 状态管理内容视图 + * + * @param count 当前计数 + * @param onIncrease 递增回调 + * @param onDecrease 递减回调 + * @param onReset 重置回调 + */ +@Composable +private fun StateManagementContent( + count: Int, + onIncrease: () -> Unit, + onDecrease: () -> Unit, + onReset: () -> Unit +) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(SpacePaddingMedium), + verticalArrangement = Arrangement.spacedBy(SpaceVerticalLarge) + ) { + IntroCard() + CounterCard( + count = count, + onIncrease = onIncrease, + onDecrease = onDecrease, + onReset = onReset + ) + } +} + +@Composable +private fun IntroCard() { + Card( + modifier = Modifier.fillMaxWidth(), + shape = ShapeMedium + ) { + Column( + modifier = Modifier.padding(SpacePaddingLarge), + verticalArrangement = Arrangement.spacedBy(SpaceVerticalMedium) + ) { + AppText( + text = "为什么要有 DemoCounterState?", + size = TextSize.TITLE_LARGE, + type = TextType.PRIMARY + ) + AppText( + text = "它是一个 @Singleton + ApplicationScope 的 StateFlow,任意页面都能订阅同一份 count 并保持同步。这里的计数器示例演示了“状态放在状态持有者里,UI 只收集 StateFlow”。", + type = TextType.SECONDARY, + size = TextSize.BODY_MEDIUM + ) + } + } +} + +/** + * 计数器卡片 + * + * @param count 当前计数 + * @param onIncrease 递增回调 + * @param onDecrease 递减回调 + * @param onReset 重置回调 + */ +@Composable +private fun CounterCard( + count: Int, + onIncrease: () -> Unit, + onDecrease: () -> Unit, + onReset: () -> Unit +) { + Card { + Column( + modifier = Modifier.padding(SpacePaddingLarge), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(SpaceVerticalMedium) + ) { + AppText( + text = "全局计数器", + size = TextSize.TITLE_LARGE + ) + AppText( + text = count.toString(), + size = TextSize.DISPLAY_LARGE, + type = TextType.PRIMARY + ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy( + SpaceHorizontalSmall, + Alignment.CenterHorizontally + ), + verticalAlignment = Alignment.CenterVertically + ) { + OutlinedButton( + onClick = onDecrease, + enabled = count > 0 + ) { + Text(text = "-1") + } + TextButton(onClick = onReset) { + Text(text = "重置") + } + Button(onClick = onIncrease) { + Text(text = "+1") + } + } + AppText( + text = "操作委托给 DemoCounterState,UI 不直接改值,这样多个页面共享同一份状态也能保持一致。", + type = TextType.TERTIARY, + size = TextSize.BODY_SMALL, + textAlign = TextAlign.Center + ) + } + } +} + +/** + * 状态管理界面浅色主题预览 + */ +@Preview(showBackground = true) +@Composable +private fun StateManagementPreview() { + AppTheme { + StateManagementScreen(count = 5) + } +} + +/** + * 状态管理界面深色主题预览 + */ +@Preview(showBackground = true) +@Composable +private fun StateManagementPreviewDark() { + AppTheme(darkTheme = true) { + StateManagementScreen(count = 5) + } +} diff --git a/app/src/main/java/com/joker/kit/feature/demo/viewmodel/DatabaseViewModel.kt b/app/src/main/java/com/joker/kit/feature/demo/viewmodel/DatabaseViewModel.kt new file mode 100644 index 0000000..75b571e --- /dev/null +++ b/app/src/main/java/com/joker/kit/feature/demo/viewmodel/DatabaseViewModel.kt @@ -0,0 +1,106 @@ +package com.joker.kit.feature.demo.viewmodel + +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 +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import javax.inject.Inject + +/** + * 数据库示例页 ViewModel + * + * @param navigator 导航管理器 + * @param userState 用户状态管理 + * @param demoRepository Demo 仓库,封装 DemoDataSource 的增删改查 + */ +@HiltViewModel +class DatabaseViewModel @Inject constructor( + navigator: AppNavigator, + userState: UserState, + private val demoRepository: DemoRepository +) : BaseViewModel(navigator, userState) { + + /** 标题输入 */ + private val _title = MutableStateFlow("") + val title: StateFlow = _title.asStateFlow() + + /** 描述输入 */ + private val _description = MutableStateFlow("") + val description: StateFlow = _description.asStateFlow() + + /** + * Demo 表数据流 + * UI 侧直接 collectAsState() 获取最新列表 + */ + val items: StateFlow> = demoRepository + .observeItems() + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = emptyList() + ) + + /** + * 更新标题输入 + * + * @param value 文本框中的标题 + */ + fun onTitleChange(value: String) { + _title.value = value + } + + /** + * 更新描述输入 + * + * @param value 文本框中的描述 + */ + fun onDescriptionChange(value: String) { + _description.value = value + } + + /** + * 新增一条记录(仅当标题非空) + */ + fun addItem() { + val title = _title.value.trim() + if (title.isBlank()) return + viewModelScope.launch { + demoRepository.createItem( + title = title, + description = _description.value.trim() + ) + // 重置输入框 + _title.value = "" + _description.value = "" + } + } + + /** + * 删除指定记录 + * + * @param id 记录主键 + */ + fun deleteItem(id: Long) { + viewModelScope.launch { + demoRepository.deleteItem(id) + } + } + + /** + * 清空全部记录 + */ + fun clearAll() { + viewModelScope.launch { + demoRepository.clearAll() + } + } +} diff --git a/app/src/main/java/com/joker/kit/feature/demo/viewmodel/LocalStorageViewModel.kt b/app/src/main/java/com/joker/kit/feature/demo/viewmodel/LocalStorageViewModel.kt new file mode 100644 index 0000000..ee32941 --- /dev/null +++ b/app/src/main/java/com/joker/kit/feature/demo/viewmodel/LocalStorageViewModel.kt @@ -0,0 +1,123 @@ +package com.joker.kit.feature.demo.viewmodel + +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 + +/** + * 本地存储示例页 ViewModel + * + * 通过本地仓库 (UserInfoStoreRepository) 演示“用户信息” 的保存 / 读取 / 清除。 + * + * @param navigator 导航管理器 + * @param userState 用户状态管理 + * @param userInfoStoreRepository 用户信息本地存储仓库 + */ +@HiltViewModel +class LocalStorageViewModel @Inject constructor( + navigator: AppNavigator, + userState: UserState, + private val userInfoStoreRepository: UserInfoStoreRepository +) : BaseViewModel(navigator, userState) { + + /** 用户 id 输入 */ + private val _userId = MutableStateFlow("1") + val userId: StateFlow = _userId.asStateFlow() + + /** 昵称输入 */ + private val _nickName = MutableStateFlow("") + val nickName: StateFlow = _nickName.asStateFlow() + + /** 头像输入 */ + private val _avatar = MutableStateFlow("") + val avatar: StateFlow = _avatar.asStateFlow() + + /** 当前用户信息 */ + private val _userStateFlow = MutableStateFlow(null) + val user: StateFlow = _userStateFlow.asStateFlow() + + init { + loadUser() + } + + /** + * 用户 id 文本更新 + * + * @param value 输入的 id 字符串 + */ + fun onUserIdChange(value: String) { + _userId.value = value + } + + /** + * 用户昵称输入更新 + * + * @param value 昵称文本 + */ + fun onNickNameChange(value: String) { + _nickName.value = value + } + + /** + * 头像链接输入更新 + * + * @param value 头像 URL + */ + fun onAvatarChange(value: String) { + _avatar.value = value + } + + /** + * 保存用户信息到本地 + */ + fun saveUser() { + viewModelScope.launch { + val idLong = _userId.value.toLongOrNull() ?: 0L + val user = User( + id = idLong, + nickName = _nickName.value.ifBlank { "未命名" }, + avatarUrl = _avatar.value.ifBlank { null }, + unionid = "demo-unionid-$idLong" + ) + userInfoStoreRepository.saveUserInfo(user) + _userStateFlow.value = user + } + } + + /** + * 清除本地用户信息 + */ + fun clearUser() { + viewModelScope.launch { + userInfoStoreRepository.clearUserInfo() + _userStateFlow.value = null + _userId.value = "1" + _nickName.value = "" + _avatar.value = "" + } + } + + /** + * 重新读取用户信息 + */ + fun loadUser() { + viewModelScope.launch { + val saved = userInfoStoreRepository.getUserInfo() + _userStateFlow.value = saved + if (saved != null) { + _userId.value = saved.id.toString() + _nickName.value = saved.nickName.orEmpty() + _avatar.value = saved.avatarUrl.orEmpty() + } + } + } +} diff --git a/app/src/main/java/com/joker/kit/feature/demo/viewmodel/NavigationResultViewModel.kt b/app/src/main/java/com/joker/kit/feature/demo/viewmodel/NavigationResultViewModel.kt new file mode 100644 index 0000000..ec40c20 --- /dev/null +++ b/app/src/main/java/com/joker/kit/feature/demo/viewmodel/NavigationResultViewModel.kt @@ -0,0 +1,28 @@ +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 dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +/** + * 结果回传示例页 ViewModel + */ +@HiltViewModel +class NavigationResultViewModel @Inject constructor( + navigator: AppNavigator, + userState: UserState +) : BaseViewModel( + navigator = navigator, + userState = userState +) { + fun sendResultAndBack() { + popBackStackWithResult( + DemoResultKey, + DemoResult(id = 9527, message = "这是回传的结果") + ) + } +} diff --git a/app/src/main/java/com/joker/kit/feature/demo/viewmodel/NavigationWithArgsViewModel.kt b/app/src/main/java/com/joker/kit/feature/demo/viewmodel/NavigationWithArgsViewModel.kt new file mode 100644 index 0000000..c05c73f --- /dev/null +++ b/app/src/main/java/com/joker/kit/feature/demo/viewmodel/NavigationWithArgsViewModel.kt @@ -0,0 +1,33 @@ +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 dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +/** + * 带参跳转示例页 ViewModel + */ +@HiltViewModel +class NavigationWithArgsViewModel @Inject constructor( + navigator: AppNavigator, + userState: UserState, + savedStateHandle: SavedStateHandle +) : BaseViewModel( + navigator = navigator, + userState = userState +) { + /** + * 路由参数 + * */ + private val route = savedStateHandle.toRoute() + + /** + * 商品ID + * */ + val goodsId: Long = route.goodsId +} \ No newline at end of file diff --git a/app/src/main/java/com/joker/kit/feature/demo/viewmodel/NetworkDemoViewModel.kt b/app/src/main/java/com/joker/kit/feature/demo/viewmodel/NetworkDemoViewModel.kt new file mode 100644 index 0000000..ee94496 --- /dev/null +++ b/app/src/main/java/com/joker/kit/feature/demo/viewmodel/NetworkDemoViewModel.kt @@ -0,0 +1,37 @@ +package com.joker.kit.feature.demo.viewmodel + +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 + +/** + * 网络状态 Demo 页面 ViewModel + * + * @param navigator 导航管理器 + * @param userState 用户状态管理 + * @param goodsRepository 商品数据仓库 + */ +@HiltViewModel +class NetworkDemoViewModel @Inject constructor( + navigator: AppNavigator, + userState: UserState, + private val goodsRepository: GoodsRepository +) : BaseNetWorkViewModel(navigator, userState) { + + init { + super.executeRequest() + } + + /** + * 重写请求API Flow,获取商品信息 + */ + override fun requestApiFlow(): Flow> { + return goodsRepository.getGoodsInfo("1") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/joker/kit/feature/demo/viewmodel/NetworkListDemoViewModel.kt b/app/src/main/java/com/joker/kit/feature/demo/viewmodel/NetworkListDemoViewModel.kt new file mode 100644 index 0000000..0a1353b --- /dev/null +++ b/app/src/main/java/com/joker/kit/feature/demo/viewmodel/NetworkListDemoViewModel.kt @@ -0,0 +1,47 @@ +package com.joker.kit.feature.demo.viewmodel + +import com.joker.kit.core.base.viewmodel.BaseNetWorkListViewModel +import com.joker.kit.core.data.repository.GoodsRepository +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 + +/** + * Network List Demo 示例页 ViewModel + * + * @param navigator 导航器 + * @param userState 用户状态管理 + * @param goodsRepository 商品数据仓库 + */ +@HiltViewModel +class NetworkListDemoViewModel @Inject constructor( + navigator: AppNavigator, + userState: UserState, + private val goodsRepository: GoodsRepository +) : BaseNetWorkListViewModel( + navigator = navigator, + userState = userState +) { + + init { + initLoad() + } + + /** + * 重写请求API Flow,获取商品列表 + */ + override fun requestListData(): Flow>> { + return goodsRepository.getGoodsPage( + GoodsSearchRequest( + page = currentPage, + size = pageSize + ) + ) + } +} diff --git a/app/src/main/java/com/joker/kit/feature/demo/viewmodel/NetworkRequestViewModel.kt b/app/src/main/java/com/joker/kit/feature/demo/viewmodel/NetworkRequestViewModel.kt new file mode 100644 index 0000000..4cffedc --- /dev/null +++ b/app/src/main/java/com/joker/kit/feature/demo/viewmodel/NetworkRequestViewModel.kt @@ -0,0 +1,46 @@ +package com.joker.kit.feature.demo.viewmodel + +import androidx.lifecycle.viewModelScope +import com.joker.kit.core.base.viewmodel.BaseViewModel +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 +import kotlinx.coroutines.flow.asStateFlow +import javax.inject.Inject + +/** + * 网络请求示例页 ViewModel + */ +@HiltViewModel +class NetworkRequestViewModel @Inject constructor( + navigator: AppNavigator, + userState: UserState, + private val goodsRepository: GoodsRepository +) : BaseViewModel( + navigator = navigator, + userState = userState +) { + + /** + * 商品信息 + */ + private val _goods = MutableStateFlow(null) + val goods: StateFlow = _goods.asStateFlow() + + /** + * 发起商品信息请求,示例中固定传 id = 1 + */ + fun onRequestClick() { + ResultHandler.handleResultWithData( + scope = viewModelScope, + flow = goodsRepository.getGoodsInfo("1").asResult(), + onData = { goods -> _goods.value = goods } + ) + } +} diff --git a/app/src/main/java/com/joker/kit/feature/demo/viewmodel/StateManagementViewModel.kt b/app/src/main/java/com/joker/kit/feature/demo/viewmodel/StateManagementViewModel.kt new file mode 100644 index 0000000..0e1fb79 --- /dev/null +++ b/app/src/main/java/com/joker/kit/feature/demo/viewmodel/StateManagementViewModel.kt @@ -0,0 +1,40 @@ +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 + +/** + * 状态管理示例页 ViewModel + */ +@HiltViewModel +class StateManagementViewModel @Inject constructor( + navigator: AppNavigator, + userState: UserState, + private val counterState: DemoCounterState +) : BaseViewModel(navigator, userState) { + + /** + * 对外暴露的计数器 StateFlow + */ + val count: StateFlow = counterState.count + + /** + * +1 + */ + fun increase() = counterState.increase() + + /** + * -1 + */ + fun decrease() = counterState.decrease() + + /** + * 重置为 0 + */ + fun reset() = counterState.reset() +} diff --git a/app/src/main/java/com/joker/kit/feature/main/component/DemoCard.kt b/app/src/main/java/com/joker/kit/feature/main/component/DemoCard.kt index bcd4c33..a237e90 100644 --- a/app/src/main/java/com/joker/kit/feature/main/component/DemoCard.kt +++ b/app/src/main/java/com/joker/kit/feature/main/component/DemoCard.kt @@ -5,6 +5,7 @@ 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 @@ -16,14 +17,18 @@ import com.joker.kit.feature.main.model.DemoCardInfo * * @param info 卡片数据 * @param modifier 修饰符 + * @param onClick 点击回调 */ @Composable fun DemoCard( info: DemoCardInfo, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + onClick: () -> Unit = {} ) { ListItem( - modifier = modifier.clip(ShapeMedium), + modifier = modifier + .clip(ShapeMedium) + .clickable(onClick = onClick), headlineContent = { AppText( text = info.title, diff --git a/app/src/main/java/com/joker/kit/feature/main/data/DemoCardData.kt b/app/src/main/java/com/joker/kit/feature/main/data/DemoCardData.kt index 136636b..1ec7aed 100644 --- a/app/src/main/java/com/joker/kit/feature/main/data/DemoCardData.kt +++ b/app/src/main/java/com/joker/kit/feature/main/data/DemoCardData.kt @@ -1,6 +1,8 @@ package com.joker.kit.feature.main.data import com.joker.kit.feature.main.model.DemoCardInfo +import com.joker.kit.navigation.routes.DemoRoutes +import com.joker.kit.navigation.routes.UserRoutes /** * Demo 卡片静态数据源 @@ -9,43 +11,52 @@ object DemoCardData { val coreCards: List = listOf( DemoCardInfo( - title = "Base Network", - description = "网络状态切换,包含加载、错误、重试等流程。" + title = "Network Demo", + description = "网络状态切换,包含加载、错误、重试等流程。", + route = DemoRoutes.NetworkDemo ), DemoCardInfo( - title = "Base Network List", - description = "下拉刷新与分页加载的统一列表模板,内置空状态与重试。" + title = "Network List Demo", + description = "下拉刷新与分页加载的统一列表模板,内置空状态与重试。", + route = DemoRoutes.NetworkListDemo ), DemoCardInfo( title = "数据库", - description = "Room 的增删改查示例,含简单的列表展示与数据观察。" + description = "Room 的增删改查示例,含简单的列表展示与数据观察。", + route = DemoRoutes.Database ), DemoCardInfo( title = "本地存储", - description = "DataStore / MMKV 的写入与清除示例,演示单值增删改查。" + description = "DataStore / MMKV 的写入与清除示例,演示单值增删改查。", + route = DemoRoutes.LocalStorage ), DemoCardInfo( title = "状态管理", - description = "全局 UserState 登陆态同步、跨页面数据推送与状态流更新。" + description = "全局 DemoCounterState 计数器共享示例,展示跨页面 StateFlow 同步。", + route = DemoRoutes.StateManagement ), DemoCardInfo( title = "网络请求", - description = "结合 ResultHandler 的通用接口请求、加载状态与错误提示。" + description = "结合 ResultHandler 的通用接口请求、加载状态与错误提示。", + route = DemoRoutes.NetworkRequest ) ) val navigationCards: List = listOf( DemoCardInfo( title = "带参跳转", - description = "类型安全路由参数,包含必填/可选参数与目标页接收方式。" + description = "类型安全路由参数,包含必填/可选参数与目标页接收方式。", + route = DemoRoutes.NavigationWithArgs(123) ), DemoCardInfo( title = "结果回传", - description = "NavigationResultKey 返回数据,包含刷新信号与数据实体回传。" + description = "NavigationResultKey 返回数据,包含刷新信号与数据实体回传。", + route = DemoRoutes.NavigationResult ), DemoCardInfo( title = "导航拦截", - description = "登录拦截流程:未登录跳登录页,登录成功后才能进入用户详情。" + description = "登录拦截流程:未登录跳登录页,登录成功后才能进入用户详情。", + route = UserRoutes.Info ) ) -} +} \ No newline at end of file diff --git a/app/src/main/java/com/joker/kit/feature/main/model/DemoCardInfo.kt b/app/src/main/java/com/joker/kit/feature/main/model/DemoCardInfo.kt index 1f62efb..ba75d05 100644 --- a/app/src/main/java/com/joker/kit/feature/main/model/DemoCardInfo.kt +++ b/app/src/main/java/com/joker/kit/feature/main/model/DemoCardInfo.kt @@ -8,5 +8,6 @@ package com.joker.kit.feature.main.model */ data class DemoCardInfo( val title: String, - val description: String + val description: String, + val route: Any? = null ) diff --git a/app/src/main/java/com/joker/kit/feature/main/navigation/MainGraph.kt b/app/src/main/java/com/joker/kit/feature/main/navigation/MainGraph.kt index 81a449f..51544c5 100644 --- a/app/src/main/java/com/joker/kit/feature/main/navigation/MainGraph.kt +++ b/app/src/main/java/com/joker/kit/feature/main/navigation/MainGraph.kt @@ -18,5 +18,5 @@ fun NavGraphBuilder.mainGraph( sharedTransitionScope: SharedTransitionScope ) { // 只调用页面级导航函数,不包含其他逻辑 - mainScreen(sharedTransitionScope) + mainScreen(navController, sharedTransitionScope) } diff --git a/app/src/main/java/com/joker/kit/feature/main/navigation/MainNavigation.kt b/app/src/main/java/com/joker/kit/feature/main/navigation/MainNavigation.kt index baaa0b4..9846a3f 100644 --- a/app/src/main/java/com/joker/kit/feature/main/navigation/MainNavigation.kt +++ b/app/src/main/java/com/joker/kit/feature/main/navigation/MainNavigation.kt @@ -3,6 +3,7 @@ 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 @@ -14,8 +15,11 @@ import com.joker.kit.navigation.routes.MainRoutes * @author Joker.X */ @OptIn(ExperimentalSharedTransitionApi::class) -fun NavGraphBuilder.mainScreen(sharedTransitionScope: SharedTransitionScope) { +fun NavGraphBuilder.mainScreen( + navController: NavHostController, + sharedTransitionScope: SharedTransitionScope +) { composable { - MainRoute() + MainRoute(navController = navController) } } diff --git a/app/src/main/java/com/joker/kit/feature/main/view/CoreDemoScreen.kt b/app/src/main/java/com/joker/kit/feature/main/view/CoreDemoScreen.kt index 782484e..1b14be2 100644 --- a/app/src/main/java/com/joker/kit/feature/main/view/CoreDemoScreen.kt +++ b/app/src/main/java/com/joker/kit/feature/main/view/CoreDemoScreen.kt @@ -3,8 +3,11 @@ package com.joker.kit.feature.main.view import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.Card import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.runtime.Composable @@ -16,6 +19,7 @@ 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.designsystem.theme.SpaceVerticalMedium +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 @@ -31,26 +35,60 @@ internal fun CoreDemoRoute( viewModel: CoreDemoViewModel = hiltViewModel() ) { val cards by viewModel.cards.collectAsState() - CoreDemoScreen(cards = cards) + val count by viewModel.count.collectAsState() + CoreDemoScreen( + cards = cards, + counter = count, + onCardClick = viewModel::onCardClick + ) } /** * Core Demo 界面 * * @param cards Demo 卡片列表 + * @param counter 全局计数器值,大于 0 时在列表顶部展示 + * @param onCardClick 卡片点击回调 */ @Composable internal fun CoreDemoScreen( - cards: List = emptyList() + cards: List = emptyList(), + counter: Int = 0, + onCardClick: (DemoCardInfo) -> Unit = {} ) { LazyColumn( modifier = Modifier.fillMaxSize(), contentPadding = PaddingValues(SpacePaddingLarge), verticalArrangement = Arrangement.spacedBy(SpaceVerticalMedium) ) { - items(cards) { info -> - DemoCard(info = info) + // 大于 0 才展示计数器 + if (counter > 0) { + item(key = "counter") { + CounterBanner(counter = counter) + } } + + itemsIndexed(cards) { _, info -> + DemoCard( + info = info, + onClick = { onCardClick(info) } + ) + } + } +} + +/** + * 主页计数器提示 + * + * @param counter 当前计数器值 + */ +@Composable +private fun CounterBanner(counter: Int) { + Card(modifier = Modifier.fillMaxWidth()) { + AppText( + modifier = Modifier.padding(SpacePaddingLarge), + text = "全局计数器:$counter", + ) } } @@ -62,7 +100,7 @@ internal fun CoreDemoScreen( private fun CoreDemoPreview() { AppTheme { Surface(color = MaterialTheme.colorScheme.background) { - CoreDemoScreen(cards = DemoCardData.coreCards) + CoreDemoScreen(cards = DemoCardData.coreCards, counter = 3) } } } @@ -75,7 +113,7 @@ private fun CoreDemoPreview() { private fun CoreDemoPreviewDark() { AppTheme(darkTheme = true) { Surface(color = MaterialTheme.colorScheme.background) { - CoreDemoScreen(cards = DemoCardData.coreCards) + CoreDemoScreen(cards = DemoCardData.coreCards, counter = 3) } } } diff --git a/app/src/main/java/com/joker/kit/feature/main/view/MainScreen.kt b/app/src/main/java/com/joker/kit/feature/main/view/MainScreen.kt index 647342a..db5d3ae 100644 --- a/app/src/main/java/com/joker/kit/feature/main/view/MainScreen.kt +++ b/app/src/main/java/com/joker/kit/feature/main/view/MainScreen.kt @@ -19,10 +19,12 @@ 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 @@ -38,12 +40,14 @@ import com.joker.kit.feature.main.viewmodel.MainViewModel */ @Composable internal fun MainRoute( - viewModel: MainViewModel = hiltViewModel() + viewModel: MainViewModel = hiltViewModel(), + navController: NavController ) { val uiState by viewModel.uiState.collectAsState() MainScreen( uiState = uiState, - onTabSelected = viewModel::selectTab + onTabSelected = viewModel::selectTab, + navController = navController ) } @@ -57,12 +61,14 @@ internal fun MainRoute( @OptIn(ExperimentalMaterial3Api::class) @Composable internal fun MainScreen( - uiState: MainUiState, + uiState: MainUiState = MainUiState(), onTabSelected: (MainTab) -> Unit, + navController: NavController = NavController(LocalContext.current) ) { MainScreenContent( uiState = uiState, - onTabSelected = onTabSelected + onTabSelected = onTabSelected, + navController = navController ) } @@ -73,7 +79,8 @@ internal fun MainScreen( @Composable private fun MainScreenContent( uiState: MainUiState, - onTabSelected: (MainTab) -> Unit + onTabSelected: (MainTab) -> Unit, + navController: NavController ) { val pagerState = rememberPagerState(pageCount = { MainTab.allTabs.size }) val currentPage = pagerState.currentPage @@ -109,7 +116,9 @@ private fun MainScreenContent( ) { page -> when (MainTab.fromIndex(page)) { MainTab.Core -> CoreDemoRoute() - MainTab.Navigation -> NavigationDemoRoute() + MainTab.Navigation -> NavigationDemoRoute( + navController = navController + ) } } } @@ -160,7 +169,7 @@ internal fun MainScreenPreview() { AppTheme { MainScreen( uiState = MainUiState(), - onTabSelected = {} + onTabSelected = {}, ) } } diff --git a/app/src/main/java/com/joker/kit/feature/main/view/NavigationDemoScreen.kt b/app/src/main/java/com/joker/kit/feature/main/view/NavigationDemoScreen.kt index 9dba5f5..c4dfe7c 100644 --- a/app/src/main/java/com/joker/kit/feature/main/view/NavigationDemoScreen.kt +++ b/app/src/main/java/com/joker/kit/feature/main/view/NavigationDemoScreen.kt @@ -3,8 +3,11 @@ package com.joker.kit.feature.main.view import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.Card import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.runtime.Composable @@ -13,13 +16,18 @@ 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.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 路由 @@ -28,29 +36,85 @@ import com.joker.kit.feature.main.viewmodel.NavigationDemoViewModel */ @Composable internal fun NavigationDemoRoute( - viewModel: NavigationDemoViewModel = hiltViewModel() + viewModel: NavigationDemoViewModel = hiltViewModel(), + navController: NavController ) { val cards by viewModel.cards.collectAsState() - NavigationDemoScreen(cards = cards) + val isLoggedIn by viewModel.isLoggedIn.collectAsState() + val demoResult by viewModel.demoResult.collectAsState() + + NavigationDemoScreen( + cards = cards, + isLoggedIn = isLoggedIn, + demoResult = demoResult, + onCardClick = viewModel::onCardClick + ) + + navController.observeResult(DemoResultKey) { result -> + viewModel.onResultReceived(result) + } } /** * Navigation Demo 界面 * * @param cards Demo 卡片列表 + * @param isLoggedIn 是否已登录,登录后展示提示 */ @Composable internal fun NavigationDemoScreen( - cards: List = emptyList() + cards: List = emptyList(), + isLoggedIn: Boolean = false, + demoResult: DemoResult? = null, + onCardClick: (DemoCardInfo) -> Unit = {} ) { LazyColumn( modifier = Modifier.fillMaxSize(), contentPadding = PaddingValues(SpacePaddingLarge), verticalArrangement = Arrangement.spacedBy(SpaceVerticalMedium) ) { - items(cards) { info -> - DemoCard(info = info) + if (isLoggedIn) { + item(key = "login-status-nav") { + LoginStatusBanner() + } } + + demoResult?.let { + item(key = "demo-result") { + DemoResultBanner(it) + } + } + + itemsIndexed(cards) { _, info -> + DemoCard( + info = info, + onClick = { onCardClick(info) } + ) + } + } +} + +/** + * 登录状态提示卡片 + * */ +@Composable +private fun LoginStatusBanner() { + Card(modifier = Modifier.fillMaxWidth()) { + AppText( + modifier = Modifier.padding(SpacePaddingLarge), + text = "登录状态:已登录", + ) + } +} + +/** 回传结果提示卡片 */ +@Composable +private fun DemoResultBanner(result: DemoResult) { + Card(modifier = Modifier.fillMaxWidth()) { + AppText( + modifier = Modifier.padding(SpacePaddingLarge), + text = "回传结果:id=${result.id},message=${result.message}", + ) } } @@ -62,7 +126,7 @@ internal fun NavigationDemoScreen( private fun NavigationDemoPreview() { AppTheme { Surface(color = MaterialTheme.colorScheme.background) { - NavigationDemoScreen(cards = DemoCardData.navigationCards) + NavigationDemoScreen(cards = DemoCardData.navigationCards, isLoggedIn = true) } } } @@ -75,7 +139,7 @@ private fun NavigationDemoPreview() { private fun NavigationDemoPreviewDark() { AppTheme(darkTheme = true) { Surface(color = MaterialTheme.colorScheme.background) { - NavigationDemoScreen(cards = DemoCardData.navigationCards) + NavigationDemoScreen(cards = DemoCardData.navigationCards, isLoggedIn = true) } } } diff --git a/app/src/main/java/com/joker/kit/feature/main/viewmodel/CoreDemoViewModel.kt b/app/src/main/java/com/joker/kit/feature/main/viewmodel/CoreDemoViewModel.kt index 08c2efa..7ff10c3 100644 --- a/app/src/main/java/com/joker/kit/feature/main/viewmodel/CoreDemoViewModel.kt +++ b/app/src/main/java/com/joker/kit/feature/main/viewmodel/CoreDemoViewModel.kt @@ -1,6 +1,7 @@ package com.joker.kit.feature.main.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.feature.main.data.DemoCardData import com.joker.kit.feature.main.model.DemoCardInfo @@ -18,6 +19,7 @@ import javax.inject.Inject class CoreDemoViewModel @Inject constructor( navigator: AppNavigator, userState: UserState, + counterState: DemoCounterState ) : BaseViewModel( navigator = navigator, userState = userState @@ -25,4 +27,11 @@ class CoreDemoViewModel @Inject constructor( private val _cards = MutableStateFlow(DemoCardData.coreCards) val cards: StateFlow> = _cards.asStateFlow() + + /** 全局计数器值 */ + val count: StateFlow = counterState.count + + fun onCardClick(info: DemoCardInfo) { + info.route?.let { navigate(it) } + } } diff --git a/app/src/main/java/com/joker/kit/feature/main/viewmodel/NavigationDemoViewModel.kt b/app/src/main/java/com/joker/kit/feature/main/viewmodel/NavigationDemoViewModel.kt index 952f95c..0390428 100644 --- a/app/src/main/java/com/joker/kit/feature/main/viewmodel/NavigationDemoViewModel.kt +++ b/app/src/main/java/com/joker/kit/feature/main/viewmodel/NavigationDemoViewModel.kt @@ -5,6 +5,7 @@ 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 @@ -25,4 +26,18 @@ class NavigationDemoViewModel @Inject constructor( private val _cards = MutableStateFlow(DemoCardData.navigationCards) val cards: StateFlow> = _cards.asStateFlow() + + /** 全局登录状态 */ + val isLoggedIn: StateFlow = userState.isLoggedIn + + private val _demoResult = MutableStateFlow(null) + val demoResult: StateFlow = _demoResult.asStateFlow() + + fun onCardClick(info: DemoCardInfo) { + info.route?.let { navigate(it) } + } + + fun onResultReceived(result: DemoResult) { + _demoResult.value = result + } } diff --git a/app/src/main/java/com/joker/kit/feature/user/view/UserInfoScreen.kt b/app/src/main/java/com/joker/kit/feature/user/view/UserInfoScreen.kt index 9f1b425..37c3401 100644 --- a/app/src/main/java/com/joker/kit/feature/user/view/UserInfoScreen.kt +++ b/app/src/main/java/com/joker/kit/feature/user/view/UserInfoScreen.kt @@ -1,15 +1,20 @@ package com.joker.kit.feature.user.view +import androidx.compose.foundation.layout.Column +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.ui.component.scaffold.AppScaffold import com.joker.kit.core.ui.component.text.AppText -import com.joker.kit.core.ui.component.text.TextSize import com.joker.kit.feature.user.viewmodel.UserInfoViewModel /** @@ -22,7 +27,10 @@ import com.joker.kit.feature.user.viewmodel.UserInfoViewModel internal fun UserInfoRoute( viewModel: UserInfoViewModel = hiltViewModel() ) { - UserInfoScreen() + UserInfoScreen( + onLogoutClick = viewModel::logout, + onBackClick = viewModel::navigateBack + ) } /** @@ -35,10 +43,15 @@ internal fun UserInfoRoute( @Composable internal fun UserInfoScreen( onBackClick: () -> Unit = {}, + onLogoutClick: () -> Unit = {}, ) { - Scaffold { innerPadding -> + AppScaffold( + titleText = "用户信息", + onBackClick = onBackClick, + ) { UserInfoContentView( - modifier = Modifier.padding(innerPadding) + modifier = Modifier.padding(SpacePaddingLarge), + onLogoutClick = onLogoutClick ) } } @@ -50,12 +63,18 @@ internal fun UserInfoScreen( * @author Joker.X */ @Composable -private fun UserInfoContentView(modifier: Modifier = Modifier) { - AppText( - text = "用户信息页", - size = TextSize.TITLE_MEDIUM, - modifier = modifier - ) +private fun UserInfoContentView( + modifier: Modifier = Modifier, + onLogoutClick: () -> Unit = {}, +) { + Column(modifier = modifier) { + Button( + onClick = onLogoutClick, + modifier = Modifier.fillMaxWidth() + ) { + AppText(text = "退出登录") + } + } } /** diff --git a/app/src/main/java/com/joker/kit/feature/user/viewmodel/UserInfoViewModel.kt b/app/src/main/java/com/joker/kit/feature/user/viewmodel/UserInfoViewModel.kt index 5273ddf..f7ab1d1 100644 --- a/app/src/main/java/com/joker/kit/feature/user/viewmodel/UserInfoViewModel.kt +++ b/app/src/main/java/com/joker/kit/feature/user/viewmodel/UserInfoViewModel.kt @@ -1,9 +1,12 @@ package com.joker.kit.feature.user.viewmodel +import androidx.lifecycle.viewModelScope import com.joker.kit.core.base.viewmodel.BaseViewModel 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,4 +23,16 @@ class UserInfoViewModel @Inject constructor( ) : BaseViewModel( navigator = navigator, userState = userState -) +) { + + /** + * 一键退出登录(本地清空) + */ + fun logout() { + viewModelScope.launch { + userState.logout() + ToastUtils.show("已退出登录") + navigateBack() + } + } +} diff --git a/app/src/main/java/com/joker/kit/navigation/AppNavHost.kt b/app/src/main/java/com/joker/kit/navigation/AppNavHost.kt index 80398ac..c32d164 100644 --- a/app/src/main/java/com/joker/kit/navigation/AppNavHost.kt +++ b/app/src/main/java/com/joker/kit/navigation/AppNavHost.kt @@ -10,6 +10,7 @@ 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 @@ -73,6 +74,7 @@ fun AppNavHost( } ) { mainGraph(navController, this@SharedTransitionLayout) + demoGraph(navController, this@SharedTransitionLayout) authGraph(navController, this@SharedTransitionLayout) userGraph(navController, this@SharedTransitionLayout) } diff --git a/app/src/main/java/com/joker/kit/navigation/extension/NavigationResultExt.kt b/app/src/main/java/com/joker/kit/navigation/extension/NavigationResultExt.kt new file mode 100644 index 0000000..27af1e2 --- /dev/null +++ b/app/src/main/java/com/joker/kit/navigation/extension/NavigationResultExt.kt @@ -0,0 +1,37 @@ +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 + +/** + * 监听返回结果扩展 + */ +@Composable +fun NavController.observeResult( + key: NavigationResultKey, + 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(key.key) + if (raw != null) { + val result = key.deserialize(raw) + onResult(result) + savedStateHandle.remove(key.key) + } + } + } + backStackEntry.lifecycle.addObserver(observer) + onDispose { + backStackEntry.lifecycle.removeObserver(observer) + } + } +} diff --git a/app/src/main/java/com/joker/kit/navigation/results/DemoResultKey.kt b/app/src/main/java/com/joker/kit/navigation/results/DemoResultKey.kt new file mode 100644 index 0000000..c0d19be --- /dev/null +++ b/app/src/main/java/com/joker/kit/navigation/results/DemoResultKey.kt @@ -0,0 +1,19 @@ +package com.joker.kit.navigation.results + +import com.joker.kit.navigation.NavigationResultKey +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json + +/** + * Demo 结果回传示例:返回 DemoResult 数据类 + */ +object DemoResultKey : NavigationResultKey { + override fun serialize(value: DemoResult): Any = Json.encodeToString(value) + override fun deserialize(raw: Any): DemoResult = Json.decodeFromString(raw as String) +} + +@Serializable +data class DemoResult( + val id: Long, + val message: String +) diff --git a/app/src/main/java/com/joker/kit/navigation/routes/DemoRoutes.kt b/app/src/main/java/com/joker/kit/navigation/routes/DemoRoutes.kt new file mode 100644 index 0000000..6551130 --- /dev/null +++ b/app/src/main/java/com/joker/kit/navigation/routes/DemoRoutes.kt @@ -0,0 +1,42 @@ +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 +} diff --git a/app/src/main/java/com/joker/kit/navigation/routes/UserRoutes.kt b/app/src/main/java/com/joker/kit/navigation/routes/UserRoutes.kt index fb552a8..62fb2be 100644 --- a/app/src/main/java/com/joker/kit/navigation/routes/UserRoutes.kt +++ b/app/src/main/java/com/joker/kit/navigation/routes/UserRoutes.kt @@ -6,8 +6,10 @@ import kotlinx.serialization.Serializable * 用户相关路由 */ object UserRoutes { + /** * 用户信息页 + * 登录后才能访问的用户信息页 */ @Serializable data object Info diff --git a/app/src/main/res/drawable/ic_left.xml b/app/src/main/res/drawable/ic_left.xml new file mode 100644 index 0000000..958fe9c --- /dev/null +++ b/app/src/main/res/drawable/ic_left.xml @@ -0,0 +1,9 @@ + + +