初步实现首页 demo 示例布局

This commit is contained in:
Joker.X
2025-12-01 23:19:52 +08:00
parent c46b9ead5b
commit 2f07944d4b
9 changed files with 494 additions and 21 deletions

View File

@@ -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 卡片默认样式。"
)
)
}
}

View File

@@ -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<DemoCardInfo> = 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<DemoCardInfo> = listOf(
DemoCardInfo(
title = "带参跳转",
description = "类型安全路由参数,包含必填/可选参数与目标页接收方式。"
),
DemoCardInfo(
title = "结果回传",
description = "NavigationResultKey 返回数据,包含刷新信号与数据实体回传。"
),
DemoCardInfo(
title = "导航拦截",
description = "登录拦截流程:未登录跳登录页,登录成功后才能进入用户详情。"
)
)
}

View File

@@ -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
)

View File

@@ -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<DemoCardInfo> = 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)
}
}
}

View File

@@ -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,41 +40,114 @@ 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) {
private fun MainBottomBar(
tabs: List<MainTab>,
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 = "主页面",
size = TextSize.TITLE_MEDIUM,
modifier = modifier
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 = {}
)
}
}

View File

@@ -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<DemoCardInfo> = 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)
}
}
}

View File

@@ -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<List<DemoCardInfo>> = _cards.asStateFlow()
}

View File

@@ -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<MainUiState> = _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<MainTab> = values().toList()
fun fromIndex(index: Int): MainTab {
return allTabs.getOrElse(index) { Core }
}
}
}

View File

@@ -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<List<DemoCardInfo>> = _cards.asStateFlow()
}