From 2f07944d4b61ea57791715dc94a50479c623afff Mon Sep 17 00:00:00 2001 From: "Joker.X" Date: Mon, 1 Dec 2025 23:19:52 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E6=AD=A5=E5=AE=9E=E7=8E=B0=E9=A6=96?= =?UTF-8?q?=E9=A1=B5=20demo=20=E7=A4=BA=E4=BE=8B=E5=B8=83=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kit/feature/main/component/DemoCard.kt | 53 +++++++ .../kit/feature/main/data/DemoCardData.kt | 51 +++++++ .../kit/feature/main/model/DemoCardInfo.kt | 12 ++ .../kit/feature/main/view/CoreDemoScreen.kt | 81 +++++++++++ .../joker/kit/feature/main/view/MainScreen.kt | 137 +++++++++++++++--- .../feature/main/view/NavigationDemoScreen.kt | 81 +++++++++++ .../main/viewmodel/CoreDemoViewModel.kt | 28 ++++ .../feature/main/viewmodel/MainViewModel.kt | 44 +++++- .../main/viewmodel/NavigationDemoViewModel.kt | 28 ++++ 9 files changed, 494 insertions(+), 21 deletions(-) create mode 100644 app/src/main/java/com/joker/kit/feature/main/component/DemoCard.kt create mode 100644 app/src/main/java/com/joker/kit/feature/main/data/DemoCardData.kt create mode 100644 app/src/main/java/com/joker/kit/feature/main/model/DemoCardInfo.kt create mode 100644 app/src/main/java/com/joker/kit/feature/main/view/CoreDemoScreen.kt create mode 100644 app/src/main/java/com/joker/kit/feature/main/view/NavigationDemoScreen.kt create mode 100644 app/src/main/java/com/joker/kit/feature/main/viewmodel/CoreDemoViewModel.kt create mode 100644 app/src/main/java/com/joker/kit/feature/main/viewmodel/NavigationDemoViewModel.kt 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 new file mode 100644 index 0000000..bcd4c33 --- /dev/null +++ b/app/src/main/java/com/joker/kit/feature/main/component/DemoCard.kt @@ -0,0 +1,53 @@ +package com.joker.kit.feature.main.component + +import androidx.compose.material3.ListItem +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.tooling.preview.Preview +import com.joker.kit.core.designsystem.theme.AppTheme +import com.joker.kit.core.designsystem.theme.ShapeMedium +import com.joker.kit.core.ui.component.text.AppText +import com.joker.kit.core.ui.component.text.TextSize +import com.joker.kit.feature.main.model.DemoCardInfo + +/** + * Demo 卡片组件,统一 ListItem 样式 + * + * @param info 卡片数据 + * @param modifier 修饰符 + */ +@Composable +fun DemoCard( + info: DemoCardInfo, + modifier: Modifier = Modifier +) { + ListItem( + modifier = modifier.clip(ShapeMedium), + headlineContent = { + AppText( + text = info.title, + size = TextSize.TITLE_MEDIUM + ) + }, + supportingContent = { + AppText( + text = info.description, + size = TextSize.BODY_MEDIUM + ) + } + ) +} + +@Preview(showBackground = true) +@Composable +private fun DemoCardPreview() { + AppTheme { + DemoCard( + info = DemoCardInfo( + title = "示例组件", + description = "预览展示 Demo 卡片默认样式。" + ) + ) + } +} 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 new file mode 100644 index 0000000..136636b --- /dev/null +++ b/app/src/main/java/com/joker/kit/feature/main/data/DemoCardData.kt @@ -0,0 +1,51 @@ +package com.joker.kit.feature.main.data + +import com.joker.kit.feature.main.model.DemoCardInfo + +/** + * Demo 卡片静态数据源 + */ +object DemoCardData { + + val coreCards: List = listOf( + DemoCardInfo( + title = "Base Network", + description = "网络状态切换,包含加载、错误、重试等流程。" + ), + DemoCardInfo( + title = "Base Network List", + description = "下拉刷新与分页加载的统一列表模板,内置空状态与重试。" + ), + DemoCardInfo( + title = "数据库", + description = "Room 的增删改查示例,含简单的列表展示与数据观察。" + ), + DemoCardInfo( + title = "本地存储", + description = "DataStore / MMKV 的写入与清除示例,演示单值增删改查。" + ), + DemoCardInfo( + title = "状态管理", + description = "全局 UserState 登陆态同步、跨页面数据推送与状态流更新。" + ), + DemoCardInfo( + title = "网络请求", + description = "结合 ResultHandler 的通用接口请求、加载状态与错误提示。" + ) + ) + + val navigationCards: List = listOf( + DemoCardInfo( + title = "带参跳转", + description = "类型安全路由参数,包含必填/可选参数与目标页接收方式。" + ), + DemoCardInfo( + title = "结果回传", + description = "NavigationResultKey 返回数据,包含刷新信号与数据实体回传。" + ), + DemoCardInfo( + title = "导航拦截", + description = "登录拦截流程:未登录跳登录页,登录成功后才能进入用户详情。" + ) + ) +} 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 new file mode 100644 index 0000000..1f62efb --- /dev/null +++ b/app/src/main/java/com/joker/kit/feature/main/model/DemoCardInfo.kt @@ -0,0 +1,12 @@ +package com.joker.kit.feature.main.model + +/** + * Demo 卡片信息 + * + * @param title 标题 + * @param description 描述内容 + */ +data class DemoCardInfo( + val title: String, + val description: String +) 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 new file mode 100644 index 0000000..782484e --- /dev/null +++ b/app/src/main/java/com/joker/kit/feature/main/view/CoreDemoScreen.kt @@ -0,0 +1,81 @@ +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.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +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.designsystem.theme.AppTheme +import com.joker.kit.core.designsystem.theme.SpacePaddingLarge +import com.joker.kit.core.designsystem.theme.SpaceVerticalMedium +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.CoreDemoViewModel + +/** + * Core Demo 路由 + * + * @param viewModel Core Demo ViewModel + */ +@Composable +internal fun CoreDemoRoute( + viewModel: CoreDemoViewModel = hiltViewModel() +) { + val cards by viewModel.cards.collectAsState() + CoreDemoScreen(cards = cards) +} + +/** + * Core Demo 界面 + * + * @param cards Demo 卡片列表 + */ +@Composable +internal fun CoreDemoScreen( + cards: List = emptyList() +) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(SpacePaddingLarge), + verticalArrangement = Arrangement.spacedBy(SpaceVerticalMedium) + ) { + items(cards) { info -> + DemoCard(info = info) + } + } +} + +/** + * Core Demo 浅色预览 + */ +@Preview(showBackground = true) +@Composable +private fun CoreDemoPreview() { + AppTheme { + Surface(color = MaterialTheme.colorScheme.background) { + CoreDemoScreen(cards = DemoCardData.coreCards) + } + } +} + +/** + * Core Demo 深色预览 + */ +@Preview(showBackground = true) +@Composable +private fun CoreDemoPreviewDark() { + AppTheme(darkTheme = true) { + Surface(color = MaterialTheme.colorScheme.background) { + CoreDemoScreen(cards = DemoCardData.coreCards) + } + } +} 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 6f93750..647342a 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 @@ -1,15 +1,33 @@ package com.joker.kit.feature.main.view +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +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 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 +import com.joker.kit.feature.main.viewmodel.MainTab +import com.joker.kit.feature.main.viewmodel.MainUiState import com.joker.kit.feature.main.viewmodel.MainViewModel /** @@ -22,40 +40,113 @@ import com.joker.kit.feature.main.viewmodel.MainViewModel internal fun MainRoute( viewModel: MainViewModel = hiltViewModel() ) { - MainScreen() + val uiState by viewModel.uiState.collectAsState() + MainScreen( + uiState = uiState, + onTabSelected = viewModel::selectTab + ) } /** * 主页面 * - * @param onBackClick 返回按钮回调 + * @param uiState UI 状态 + * @param onTabSelected Tab 切换回调 * @author Joker.X */ @OptIn(ExperimentalMaterial3Api::class) @Composable internal fun MainScreen( - onBackClick: () -> Unit = {}, + uiState: MainUiState, + onTabSelected: (MainTab) -> Unit, ) { - Scaffold { innerPadding -> - MainContentView( - modifier = Modifier.padding(innerPadding) - ) + MainScreenContent( + uiState = uiState, + onTabSelected = onTabSelected + ) +} + +/** + * 主页面内容视图,包含底部导航和横向 Pager + */ +@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) +@Composable +private fun MainScreenContent( + uiState: MainUiState, + onTabSelected: (MainTab) -> Unit +) { + val pagerState = rememberPagerState(pageCount = { MainTab.allTabs.size }) + val currentPage = pagerState.currentPage + + LaunchedEffect(uiState.currentTab) { + val targetPage = uiState.currentTab.index + if (pagerState.currentPage != targetPage) { + pagerState.animateScrollToPage(targetPage) + } + } + + LaunchedEffect(currentPage) { + val tab = MainTab.fromIndex(currentPage) + if (tab != uiState.currentTab) { + onTabSelected(tab) + } + } + + Scaffold( + bottomBar = { + MainBottomBar( + tabs = MainTab.allTabs, + currentTab = uiState.currentTab, + onTabSelected = onTabSelected + ) + } + ) { innerPadding -> + HorizontalPager( + state = pagerState, + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + ) { page -> + when (MainTab.fromIndex(page)) { + MainTab.Core -> CoreDemoRoute() + MainTab.Navigation -> NavigationDemoRoute() + } + } } } /** - * 主页面内容视图 - * - * @param modifier 修饰符 - * @author Joker.X + * 自定义纯文字底部导航栏 */ @Composable -private fun MainContentView(modifier: Modifier = Modifier) { - AppText( - text = "主页面", - size = TextSize.TITLE_MEDIUM, - modifier = modifier - ) +private fun MainBottomBar( + tabs: List, + currentTab: MainTab, + onTabSelected: (MainTab) -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.surface) + .navigationBarsPadding() + .padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + tabs.forEach { tab -> + val selected = tab == currentTab + AppText( + text = tab.title, + size = TextSize.BODY_MEDIUM, + color = if (selected) MaterialTheme.colorScheme.primary + else MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + modifier = Modifier + .weight(1f) + .clickable { onTabSelected(tab) } + .padding(vertical = 10.dp) + ) + } + } } /** @@ -67,7 +158,10 @@ private fun MainContentView(modifier: Modifier = Modifier) { @Composable internal fun MainScreenPreview() { AppTheme { - MainScreen() + MainScreen( + uiState = MainUiState(), + onTabSelected = {} + ) } } @@ -80,6 +174,9 @@ internal fun MainScreenPreview() { @Composable internal fun MainScreenPreviewDark() { AppTheme(darkTheme = true) { - MainScreen() + MainScreen( + uiState = MainUiState(), + 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 new file mode 100644 index 0000000..9dba5f5 --- /dev/null +++ b/app/src/main/java/com/joker/kit/feature/main/view/NavigationDemoScreen.kt @@ -0,0 +1,81 @@ +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.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +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.designsystem.theme.AppTheme +import com.joker.kit.core.designsystem.theme.SpacePaddingLarge +import com.joker.kit.core.designsystem.theme.SpaceVerticalMedium +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 + +/** + * Navigation Demo 路由 + * + * @param viewModel Navigation Demo ViewModel + */ +@Composable +internal fun NavigationDemoRoute( + viewModel: NavigationDemoViewModel = hiltViewModel() +) { + val cards by viewModel.cards.collectAsState() + NavigationDemoScreen(cards = cards) +} + +/** + * Navigation Demo 界面 + * + * @param cards Demo 卡片列表 + */ +@Composable +internal fun NavigationDemoScreen( + cards: List = emptyList() +) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(SpacePaddingLarge), + verticalArrangement = Arrangement.spacedBy(SpaceVerticalMedium) + ) { + items(cards) { info -> + DemoCard(info = info) + } + } +} + +/** + * Navigation Demo 浅色预览 + */ +@Preview(showBackground = true) +@Composable +private fun NavigationDemoPreview() { + AppTheme { + Surface(color = MaterialTheme.colorScheme.background) { + NavigationDemoScreen(cards = DemoCardData.navigationCards) + } + } +} + +/** + * Navigation Demo 深色预览 + */ +@Preview(showBackground = true) +@Composable +private fun NavigationDemoPreviewDark() { + AppTheme(darkTheme = true) { + Surface(color = MaterialTheme.colorScheme.background) { + NavigationDemoScreen(cards = DemoCardData.navigationCards) + } + } +} 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 new file mode 100644 index 0000000..08c2efa --- /dev/null +++ b/app/src/main/java/com/joker/kit/feature/main/viewmodel/CoreDemoViewModel.kt @@ -0,0 +1,28 @@ +package com.joker.kit.feature.main.viewmodel + +import com.joker.kit.core.base.viewmodel.BaseViewModel +import com.joker.kit.core.state.UserState +import com.joker.kit.feature.main.data.DemoCardData +import com.joker.kit.feature.main.model.DemoCardInfo +import com.joker.kit.navigation.AppNavigator +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import javax.inject.Inject + +/** + * Core Demo ViewModel + */ +@HiltViewModel +class CoreDemoViewModel @Inject constructor( + navigator: AppNavigator, + userState: UserState, +) : BaseViewModel( + navigator = navigator, + userState = userState +) { + + private val _cards = MutableStateFlow(DemoCardData.coreCards) + val cards: StateFlow> = _cards.asStateFlow() +} diff --git a/app/src/main/java/com/joker/kit/feature/main/viewmodel/MainViewModel.kt b/app/src/main/java/com/joker/kit/feature/main/viewmodel/MainViewModel.kt index e0e377c..a4c0a5f 100644 --- a/app/src/main/java/com/joker/kit/feature/main/viewmodel/MainViewModel.kt +++ b/app/src/main/java/com/joker/kit/feature/main/viewmodel/MainViewModel.kt @@ -4,6 +4,9 @@ import com.joker.kit.core.base.viewmodel.BaseViewModel import com.joker.kit.core.state.UserState import com.joker.kit.navigation.AppNavigator import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import javax.inject.Inject /** @@ -20,4 +23,43 @@ class MainViewModel @Inject constructor( ) : BaseViewModel( navigator = navigator, userState = userState -) {} +) { + + private val _uiState = MutableStateFlow(MainUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + /** + * 切换底部导航 tab + */ + fun selectTab(tab: MainTab) { + if (tab == _uiState.value.currentTab) return + _uiState.value = _uiState.value.copy(currentTab = tab) + } +} + +/** + * Main 页面 UI 状态 + * + * @param currentTab 当前底部栏 tab + */ +data class MainUiState( + val currentTab: MainTab = MainTab.Core +) + +/** + * Main 页面底部栏 Tab + */ +enum class MainTab(val title: String) { + Core("Core"), + Navigation("Navigation"); + + val index: Int get() = ordinal + + companion object { + val allTabs: List = values().toList() + + fun fromIndex(index: Int): MainTab { + return allTabs.getOrElse(index) { Core } + } + } +} 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 new file mode 100644 index 0000000..952f95c --- /dev/null +++ b/app/src/main/java/com/joker/kit/feature/main/viewmodel/NavigationDemoViewModel.kt @@ -0,0 +1,28 @@ +package com.joker.kit.feature.main.viewmodel + +import com.joker.kit.core.base.viewmodel.BaseViewModel +import com.joker.kit.core.state.UserState +import com.joker.kit.feature.main.data.DemoCardData +import com.joker.kit.feature.main.model.DemoCardInfo +import com.joker.kit.navigation.AppNavigator +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import javax.inject.Inject + +/** + * Navigation Demo ViewModel + */ +@HiltViewModel +class NavigationDemoViewModel @Inject constructor( + navigator: AppNavigator, + userState: UserState, +) : BaseViewModel( + navigator = navigator, + userState = userState +) { + + private val _cards = MutableStateFlow(DemoCardData.navigationCards) + val cards: StateFlow> = _cards.asStateFlow() +}