初步实现首页 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 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.layout.padding
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable 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.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import com.joker.kit.core.designsystem.theme.AppTheme 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.AppText
import com.joker.kit.core.ui.component.text.TextSize 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 import com.joker.kit.feature.main.viewmodel.MainViewModel
/** /**
@@ -22,40 +40,113 @@ import com.joker.kit.feature.main.viewmodel.MainViewModel
internal fun MainRoute( internal fun MainRoute(
viewModel: MainViewModel = hiltViewModel() 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 * @author Joker.X
*/ */
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
internal fun MainScreen( internal fun MainScreen(
onBackClick: () -> Unit = {}, uiState: MainUiState,
onTabSelected: (MainTab) -> Unit,
) { ) {
Scaffold { innerPadding -> MainScreenContent(
MainContentView( uiState = uiState,
modifier = Modifier.padding(innerPadding) 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 @Composable
private fun MainContentView(modifier: Modifier = Modifier) { private fun MainBottomBar(
AppText( tabs: List<MainTab>,
text = "主页面", currentTab: MainTab,
size = TextSize.TITLE_MEDIUM, onTabSelected: (MainTab) -> Unit
modifier = modifier ) {
) 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 @Composable
internal fun MainScreenPreview() { internal fun MainScreenPreview() {
AppTheme { AppTheme {
MainScreen() MainScreen(
uiState = MainUiState(),
onTabSelected = {}
)
} }
} }
@@ -80,6 +174,9 @@ internal fun MainScreenPreview() {
@Composable @Composable
internal fun MainScreenPreviewDark() { internal fun MainScreenPreviewDark() {
AppTheme(darkTheme = true) { 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.core.state.UserState
import com.joker.kit.navigation.AppNavigator import com.joker.kit.navigation.AppNavigator
import dagger.hilt.android.lifecycle.HiltViewModel 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 import javax.inject.Inject
/** /**
@@ -20,4 +23,43 @@ class MainViewModel @Inject constructor(
) : BaseViewModel( ) : BaseViewModel(
navigator = navigator, navigator = navigator,
userState = userState 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()
}