A:
要請幫忙一步一步詳細仔細的列出最終版的下列程式碼,謝謝
ui/screens/*
MainScreen.kt (App 主框架)
HomeScreen.kt
StatisticsScreen.kt
BalanceSheetScreen.kt
BudgetPlanningScreen.kt
SettingsScreen.kt
AccountManagementScreen.kt
CategoryManagementScreen.kt
ProjectListScreen.kt
ProjectDetailScreen.kt
AddEditProjectScreen.kt
LinkTransactionsScreen.kt
AddTransactionScreen.kt
Q:
好的,我們來進行這次完整程式碼回顧的最後一個,也是內容最豐富的部分。
screens 套件是我們 App 的心臟地帶,它將所有後端邏輯 (ViewModel)、數據 (StateFlows) 和可重複使用的積木 (Components) 組合成一個個使用者可以實際看到並互動的功能頁面。
以下是 ui/screens/ 套件底下所有畫面 Composable 的最終版程式碼。
第廿三步:最終程式碼回顧 (UI Screens 篇)
檔案路徑: app/src/main/java/com/yourcompany/cashflow/ui/screens/MainScreen.kt
<details>
<summary><strong>點此展開 MainScreen.kt 程式碼</strong></summary>
package com.yourcompany.cashflow.ui.screens
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.navigation.NavController
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import com.yourcompany.cashflow.ui.navigation.AppNavHost
import com.yourcompany.cashflow.ui.navigation.BottomNavItem
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainScreen() {
val navController = rememberNavController()
Scaffold(
bottomBar = { BottomNavigationBar(navController = navController) }
) { innerPadding ->
AppNavHost(
navController = navController,
modifier = Modifier.padding(innerPadding)
)
}
}
@Composable
fun BottomNavigationBar(navController: NavController) {
val items = listOf(
BottomNavItem.Transactions,
BottomNavItem.BalanceSheet,
BottomNavItem.Planning,
BottomNavItem.Settings
)
NavigationBar {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
items.forEach { item ->
NavigationBarItem(
icon = { Icon(item.icon, contentDescription = item.title) },
label = { Text(item.title) },
selected = currentRoute == item.route,
onClick = {
navController.navigate(item.route) {
navController.graph.startDestinationRoute?.let { route ->
popUpTo(route) { saveState = true }
}
launchSingleTop = true
restoreState = true
}
}
)
}
}
}
</details>
檔案路徑: app/src/main/java/com/yourcompany/cashflow/ui/screens/HomeScreen.kt
<details>
<summary><strong>點此展開 HomeScreen.kt 程式碼</strong></summary>
package com.yourcompany.cashflow.ui.screens
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.PieChart
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.yourcompany.cashflow.ui.components.BudgetSummaryCard
import com.yourcompany.cashflow.ui.components.TransactionItem
import com.yourcompany.cashflow.ui.viewmodel.TransactionViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomeScreen(
viewModel: TransactionViewModel = hiltViewModel(),
onNavigateToTransaction: (Long) -> Unit,
onNavigateToStatistics: () -> Unit
) {
val transactions by viewModel.allTransactions.collectAsStateWithLifecycle()
val userBudget by viewModel.userBudget.collectAsStateWithLifecycle()
val currentMonthExpenses by viewModel.currentMonthExpenses.collectAsStateWithLifecycle()
Scaffold(
topBar = {
TopAppBar(
title = { Text("交易明細") },
actions = {
IconButton(onClick = onNavigateToStatistics) {
Icon(Icons.Default.PieChart, contentDescription = "統計")
}
}
)
},
floatingActionButton = {
FloatingActionButton(onClick = { onNavigateToTransaction(0L) }) {
Icon(Icons.Default.Add, contentDescription = "新增交易")
}
}
) { innerPadding ->
LazyColumn(
modifier = Modifier.padding(innerPadding),
contentPadding = PaddingValues(8.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
// 只在使用者設定過預算時,才顯示卡片
if (userBudget.annualBudget > 0) {
item {
Box(modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)) {
BudgetSummaryCard(
userBudget = userBudget,
currentMonthExpenses = currentMonthExpenses
)
}
}
}
items(transactions) { transactionDetail ->
Box(modifier = Modifier.padding(horizontal = 8.dp)) {
TransactionItem(
transactionDetails = transactionDetail,
onClick = { onNavigateToTransaction(transactionDetail.transaction.id) }
)
}
}
}
}
}
</details>
檔案路徑: app/src/main/java/com/yourcompany/cashflow/ui/screens/StatisticsScreen.kt
<details>
<summary><strong>點此展開 StatisticsScreen.kt 程式碼</strong></summary>
package com.yourcompany.cashflow.ui.screens
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
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.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.yourcompany.cashflow.data.model.CategorySum
import com.yourcompany.cashflow.data.model.SubCategorySum
import com.yourcompany.cashflow.ui.components.SegmentedButton
import com.yourcompany.cashflow.ui.components.TransactionTypeTabs
import com.yourcompany.cashflow.ui.viewmodel.StatisticsViewModel
import com.yourcompany.cashflow.ui.viewmodel.StatisticsViewMode
import com.patrykandpatrick.vico.compose.axis.horizontal.rememberBottomAxis
import com.patrykandpatrick.vico.compose.axis.vertical.rememberStartAxis
import com.patrykandpatrick.vico.compose.chart.Chart
import com.patrykandpatrick.vico.compose.chart.column.columnChart
import com.patrykandpatrick.vico.core.axis.AxisPosition
import com.patrykandpatrick.vico.core.axis.formatter.AxisValueFormatter
import com.patrykandpatrick.vico.core.entry.ChartEntryModelProducer
import com.patrykandpatrick.vico.core.entry.entryOf
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun StatisticsScreen(
viewModel: StatisticsViewModel = hiltViewModel(),
onNavigateBack: () -> Unit
) {
val viewMode by viewModel.viewMode.collectAsStateWithLifecycle()
val transactionType by viewModel.transactionType.collectAsStateWithLifecycle()
val mainChartData by viewModel.mainChartData.collectAsStateWithLifecycle()
val selectedProject by viewModel.selectedProject.collectAsStateWithLifecycle()
val drillDownLevel1Data by viewModel.drillDownLevel1Data.collectAsStateWithLifecycle()
val selectedMainCategoryInProject by viewModel.selectedMainCategoryInProject.collectAsStateWithLifecycle()
val drillDownLevel2Data by viewModel.drillDownLevel2Data.collectAsStateWithLifecycle()
Scaffold(
topBar = {
TopAppBar(
title = { Text("財務統計") },
navigationIcon = { IconButton(onClick = onNavigateBack) { Icon(Icons.Default.ArrowBack, "返回") } }
)
}
) { innerPadding ->
LazyColumn(
modifier = Modifier.padding(innerPadding),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
item {
SegmentedButton(
options = listOf("依分類", "依專案"),
selectedIndex = viewMode.ordinal,
onSelectedIndex = { index ->
viewModel.setViewMode(if (index == 0) StatisticsViewMode.BY_CATEGORY else StatisticsViewMode.BY_PROJECT)
}
)
}
item {
MainChartCard(
title = if (viewMode == StatisticsViewMode.BY_CATEGORY) "主分類消費統計" else "專案消費統計",
data = mainChartData,
transactionType = transactionType,
selectedItem = if (viewMode == StatisticsViewMode.BY_PROJECT) selectedProject else null, // Only highlight in project mode
onTransactionTypeChange = { viewModel.setTransactionType(it) },
onItemClick = { viewModel.selectItem(it) }
)
}
item {
AnimatedVisibility(visible = drillDownLevel1Data.isNotEmpty() && selectedProject != null) {
DrillDownCard(
title = "『${selectedProject?.categoryName}』內部分類",
data = drillDownLevel1Data,
selectedItem = selectedMainCategoryInProject,
onItemClick = { viewModel.selectDrillDownLevel1Item(it) }
)
}
}
item {
AnimatedVisibility(visible = drillDownLevel2Data.isNotEmpty() && selectedMainCategoryInProject != null) {
SubCategoryAllocationSection(
title = "『${selectedMainCategoryInProject?.categoryName}』消費佔比",
data = drillDownLevel2Data
)
}
}
}
}
}
@Composable
fun MainChartCard(
title: String,
data: List<CategorySum>,
transactionType: com.yourcompany.cashflow.data.local.entity.TransactionType,
selectedItem: CategorySum?,
onTransactionTypeChange: (com.yourcompany.cashflow.data.local.entity.TransactionType) -> Unit,
onItemClick: (CategorySum) -> Unit
) {
val chartProducer = remember { ChartEntryModelProducer() }
val entries = data.mapIndexed { index, categorySum -> entryOf(index.toFloat(), categorySum.amount.toFloat()) }
chartProducer.setEntries(entries)
val bottomAxisFormatter = AxisValueFormatter<AxisPosition.Horizontal.Bottom> { value, _ ->
data.getOrNull(value.toInt())?.categoryName ?: ""
}
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp)) {
Text(title, style = MaterialTheme.typography.titleLarge)
TransactionTypeTabs(
selectedType = transactionType,
onTypeSelected = onTransactionTypeChange
)
Spacer(modifier = Modifier.height(16.dp))
if (entries.isNotEmpty()) {
Chart(
chart = columnChart(),
chartModelProducer = chartProducer,
startAxis = rememberStartAxis(),
bottomAxis = rememberBottomAxis(
valueFormatter = bottomAxisFormatter,
labelRotationDegrees = 45f
),
modifier = Modifier.height(250.dp)
)
Divider(modifier = Modifier.padding(vertical = 8.dp))
data.forEach { item ->
ListItem(
headlineText = { Text(item.categoryName) },
trailingContent = { Text("NT$ ${"%,.0f".format(item.amount)}") },
modifier = Modifier.clickable { onItemClick(item) },
colors = if (item == selectedItem) {
ListItemDefaults.colors(containerColor = MaterialTheme.colorScheme.primaryContainer)
} else {
ListItemDefaults.colors()
}
)
}
} else {
Box(modifier = Modifier.height(250.dp).fillMaxWidth(), contentAlignment = Alignment.Center) {
Text("本月尚無相關紀錄")
}
}
}
}
}
@Composable
fun DrillDownCard(
title: String,
data: List<CategorySum>,
selectedItem: CategorySum?,
onItemClick: (CategorySum) -> Unit
) {
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp)) {
Text(title, style = MaterialTheme.typography.titleMedium)
Spacer(modifier = Modifier.height(8.dp))
data.forEach { item ->
ListItem(
headlineText = { Text(item.categoryName) },
trailingContent = { Text("NT$ ${"%,.0f".format(item.amount)}") },
modifier = Modifier.clickable { onItemClick(item) },
colors = if (item == selectedItem) {
ListItemDefaults.colors(containerColor = MaterialTheme.colorScheme.secondaryContainer)
} else {
ListItemDefaults.colors()
}
)
}
}
}
}
@Composable
fun SubCategoryAllocationSection(title: String, data: List<SubCategorySum>) {
val totalAmount = data.sumOf { it.amount }
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp)) {
Text(title, style = MaterialTheme.typography.titleMedium)
Spacer(modifier = Modifier.height(16.dp))
data.forEach { subCategory ->
val percentage = (subCategory.amount / totalAmount * 100)
Row(
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(text = subCategory.subCategoryName, modifier = Modifier.weight(1f))
LinearProgressIndicator(
progress = { percentage.toFloat() / 100f },
modifier = Modifier.weight(2f).padding(horizontal = 8.dp)
)
Text(text = "%.1f%%".format(percentage), modifier = Modifier.weight(1f), textAlign = TextAlign.End)
}
}
}
}
}
</details>
(由於檔案數量和程式碼長度非常多,為了避免回應過於龐大,我將在下一個回應中繼續提供剩餘的 Screen 檔案。)