mirror of
https://github.com/Joker-x-dev/AndroidProject-Compose.git
synced 2025-12-27 15:47:11 +00:00
初步实现首页 demo 示例布局
This commit is contained in:
@@ -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 卡片默认样式。"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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 = "登录拦截流程:未登录跳登录页,登录成功后才能进入用户详情。"
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<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 = 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 = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
Reference in New Issue
Block a user