A:
要請幫忙一步一步詳細仔細的列出最終版的下列程式碼,謝謝
ui/navigation/*
Routes.kt
BottomNavItem.kt
AppNavHost.kt
ui/components/*
BudgetSummaryCard.kt
CategoryDialog.kt
ParameterSlider.kt
SegmentedButton.kt
TransactionItem.kt
TransactionTypeTabs.kt
Q:
好的,我們繼續進行專案的完整程式碼回顧。
接下來的這部分是 App 的「骨架」與「積木」。navigation 套件定義了 App 的結構與畫面間的跳轉邏輯,是整個 UI 的骨架;components 套件則包含了所有我們精心打造、可在多個畫面重複使用的 UI 元件,是構成 App 視覺風格的積木。
以下是 ui/navigation/ 和 ui/components/ 兩個套件底下所有檔案的最終版程式碼。
第廿三步:最終程式碼回顧 (UI Navigation & Components 篇)
套件: ui/navigation/
檔案路徑: app/src/main/java/com/yourcompany/cashflow/ui/navigation/Routes.kt
<details>
<summary><strong>點此展開 Routes.kt 程式碼</strong></summary>
package com.yourcompany.cashflow.ui.navigation
/**
* 將所有導航路線定義為常數,避免在程式碼中使用字串,方便管理與重構。
*/
object Routes {
// Bottom Navigation Main Routes
const val HOME = "home"
const val BALANCE_SHEET = "balance_sheet"
const val PLANNING = "planning"
const val SETTINGS = "settings"
// Sub-screens
const val TRANSACTION_SCREEN = "transaction_screen" // For Add/Edit
const val STATISTICS = "statistics"
const val ACCOUNT_MANAGEMENT = "account_management"
const val CATEGORY_MANAGEMENT = "category_management"
const val PROJECT_LIST = "project_list"
const val ADD_EDIT_PROJECT = "add_edit_project"
const val PROJECT_DETAIL = "project_detail"
const val LINK_TRANSACTIONS = "link_transactions"
}
</details>
檔案路徑: app/src/main/java/com/yourcompany/cashflow/ui/navigation/BottomNavItem.kt
<details>
<summary><strong>點此展開 BottomNavItem.kt 程式碼</strong></summary>
package com.yourcompany.cashflow.ui.navigation
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AccountBalance
import androidx.compose.material.icons.filled.AutoGraph
import androidx.compose.material.icons.filled.List
import androidx.compose.material.icons.filled.Settings
import androidx.compose.ui.graphics.vector.ImageVector
/**
* 定義底部導覽列的每一個項目,包含其路線、圖示和標題。
*/
sealed class BottomNavItem(val route: String, val icon: ImageVector, val title: String) {
object Transactions : BottomNavItem(Routes.HOME, Icons.Default.List, "明細")
object BalanceSheet : BottomNavItem(Routes.BALANCE_SHEET, Icons.Default.AccountBalance, "資產")
object Planning : BottomNavItem(Routes.PLANNING, Icons.Default.AutoGraph, "規劃")
object Settings : BottomNavItem(Routes.SETTINGS, Icons.Default.Settings, "設定")
}
</details>
檔案路徑: app/src/main/java/com/yourcompany/cashflow/ui/navigation/AppNavHost.kt
<details>
<summary><strong>點此展開 AppNavHost.kt 程式碼</strong></summary>
package com.yourcompany.cashflow.ui.navigation
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.navigation.NavHostController
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.navArgument
import com.yourcompany.cashflow.ui.screens.*
/**
* App 的導航主機,定義了所有畫面與其對應的路線。
*/
@Composable
fun AppNavHost(navController: NavHostController, modifier: Modifier = Modifier) {
NavHost(
navController = navController,
startDestination = BottomNavItem.Transactions.route,
modifier = modifier
) {
// --- Bottom Navigation Screens ---
composable(BottomNavItem.Transactions.route) {
HomeScreen(
onNavigateToTransaction = { transactionId ->
navController.navigate("${Routes.TRANSACTION_SCREEN}?transactionId=$transactionId")
},
onNavigateToStatistics = { navController.navigate(Routes.STATISTICS) }
)
}
composable(BottomNavItem.BalanceSheet.route) { BalanceSheetScreen() }
composable(BottomNavItem.Planning.route) { BudgetPlanningScreen() }
composable(BottomNavItem.Settings.route) {
SettingsScreen(
onNavigateToAccountManagement = { navController.navigate(Routes.ACCOUNT_MANAGEMENT) },
onNavigateToCategoryManagement = { navController.navigate(Routes.CATEGORY_MANAGEMENT) },
onNavigateToProjectManagement = { navController.navigate(Routes.PROJECT_LIST) }
)
}
// --- Other Screens ---
composable(Routes.STATISTICS) { StatisticsScreen(onNavigateBack = { navController.popBackStack() }) }
composable(Routes.ACCOUNT_MANAGEMENT) { AccountManagementScreen(onNavigateBack = { navController.popBackStack() }) }
composable(Routes.CATEGORY_MANAGEMENT) { CategoryManagementScreen(onNavigateBack = { navController.popBackStack() }) }
composable(Routes.PROJECT_LIST) {
ProjectListScreen(
onNavigateBack = { navController.popBackStack() },
onNavigateToProject = { projectId ->
if (projectId == 0L) {
navController.navigate("${Routes.ADD_EDIT_PROJECT}?projectId=0")
} else {
navController.navigate("${Routes.PROJECT_DETAIL}/$projectId")
}
}
)
}
composable(
route = "${Routes.PROJECT_DETAIL}/{projectId}",
arguments = listOf(navArgument("projectId") { type = NavType.LongType })
) { backStackEntry ->
val projectId = backStackEntry.arguments?.getLong("projectId") ?: 0L
ProjectDetailScreen(
projectId = projectId,
onNavigateBack = { navController.popBackStack() },
onNavigateToEditProject = { id -> navController.navigate("${Routes.ADD_EDIT_PROJECT}?projectId=$id") },
onNavigateToLinkTransactions = { navController.navigate(Routes.LINK_TRANSACTIONS) }
)
}
composable(
route = "${Routes.ADD_EDIT_PROJECT}?projectId={projectId}",
arguments = listOf(navArgument("projectId") { type = NavType.LongType; defaultValue = 0L })
) { backStackEntry ->
val projectId = backStackEntry.arguments?.getLong("projectId") ?: 0L
AddEditProjectScreen(
projectId = projectId,
onNavigateBack = { navController.popBackStack() }
)
}
composable(Routes.LINK_TRANSACTIONS) { LinkTransactionsScreen(onNavigateBack = { navController.popBackStack() }) }
composable(
route = "${Routes.TRANSACTION_SCREEN}?transactionId={transactionId}",
arguments = listOf(navArgument("transactionId") { type = NavType.LongType; defaultValue = 0L })
) { backStackEntry ->
val transactionId = backStackEntry.arguments?.getLong("transactionId") ?: 0L
AddTransactionScreen(
transactionId = transactionId,
onNavigateBack = { navController.popBackStack() }
)
}
}
}
</details>
套件: ui/components/
檔案路徑: app/src/main/java/com/yourcompany/cashflow/ui/components/BudgetSummaryCard.kt
<details>
<summary><strong>點此展開 BudgetSummaryCard.kt 程式碼</strong></summary>
package com.yourcompany.cashflow.ui.components
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.yourcompany.cashflow.data.preferences.UserBudget
@Composable
fun BudgetSummaryCard(
userBudget: UserBudget,
currentMonthExpenses: Double
) {
val remaining = userBudget.monthlyBudget - currentMonthExpenses
val progress = if (userBudget.monthlyBudget > 0) (currentMonthExpenses / userBudget.monthlyBudget).toFloat() else 0f
val isOverBudget = remaining < 0
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp)) {
Text("本月預算追蹤", style = MaterialTheme.typography.titleMedium)
Spacer(modifier = Modifier.height(8.dp))
Row(modifier = Modifier.fillMaxWidth()) {
Text("剩餘", modifier = Modifier.weight(1f))
Text(
"NT$ ${"%,.0f".format(remaining)}",
color = if (isOverBudget) MaterialTheme.colorScheme.error else Color.Unspecified,
fontWeight = FontWeight.Bold
)
}
Spacer(modifier = Modifier.height(4.dp))
LinearProgressIndicator(
progress = { progress.coerceIn(0f, 1f) }, // Use lambda for state-derived values
modifier = Modifier.fillMaxWidth(),
color = if (isOverBudget) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.height(4.dp))
Row(modifier = Modifier.fillMaxWidth()) {
Text("支出: ${"%,.0f".format(currentMonthExpenses)}", style = MaterialTheme.typography.bodySmall)
Spacer(modifier = Modifier.weight(1f))
Text("預算: ${"%,.0f".format(userBudget.monthlyBudget)}", style = MaterialTheme.typography.bodySmall)
}
}
}
}
</details>
檔案路徑: app/src/main/java/com/yourcompany/cashflow/ui/components/CategoryDialog.kt
<details>
<summary><strong>點此展開 CategoryDialog.kt 程式碼</strong></summary>
package com.yourcompany.cashflow.ui.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.unit.dp
import com.yourcompany.cashflow.data.local.entity.TransactionType
import com.yourcompany.cashflow.ui.screens.CategoryDialogState
@Composable
fun CategoryDialog(
state: CategoryDialogState,
onDismiss: () -> Unit,
onConfirm: (name: String, type: TransactionType?) -> Unit
) {
if (state is CategoryDialogState.Hidden) return
var name by remember {
mutableStateOf(
when (state) {
is CategoryDialogState.EditingMainCategory -> state.mainCategory.name
is CategoryDialogState.EditingSubCategory -> state.subCategory.name
else -> ""
}
)
}
var transactionType by remember {
mutableStateOf(
when (state) {
is CategoryDialogState.AddingMainCategory -> state.type
is CategoryDialogState.EditingMainCategory -> state.mainCategory.type
else -> TransactionType.EXPENSE
}
)
}
// Ensure state resets when dialog reappears for a different purpose
LaunchedEffect(state) {
name = when (state) {
is CategoryDialogState.EditingMainCategory -> state.mainCategory.name
is CategoryDialogState.EditingSubCategory -> state.subCategory.name
else -> ""
}
transactionType = when (state) {
is CategoryDialogState.AddingMainCategory -> state.type
is CategoryDialogState.EditingMainCategory -> state.mainCategory.type
else -> TransactionType.EXPENSE
}
}
AlertDialog(
onDismissRequest = onDismiss,
title = {
Text(
when (state) {
is CategoryDialogState.AddingMainCategory -> "新增主分類"
is CategoryDialogState.EditingMainCategory -> "編輯主分類"
is CategoryDialogState.AddingSubCategory -> "新增子分類至 \"${state.parentCategory.name}\""
is CategoryDialogState.EditingSubCategory -> "編輯子分類"
is CategoryDialogState.Hidden -> ""
}
)
},
text = {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField(
value = name,
onValueChange = { name = it },
label = { Text("名稱") }
)
if (state is CategoryDialogState.AddingMainCategory || state is CategoryDialogState.EditingMainCategory) {
TransactionTypeTabs(
selectedType = transactionType,
onTypeSelected = { transactionType = it }
)
}
}
},
confirmButton = {
Button(
onClick = {
if (name.isNotBlank()) {
val typeToConfirm = if (state is CategoryDialogState.AddingMainCategory || state is CategoryDialogState.EditingMainCategory) {
transactionType
} else {
null
}
onConfirm(name, typeToConfirm)
}
}
) { Text("儲存") }
},
dismissButton = { TextButton(onClick = onDismiss) { Text("取消") } }
)
}
</details>
檔案路徑: app/src/main/java/com/yourcompany/cashflow/ui/components/ParameterSlider.kt
<details>
<summary><strong>點此展開 ParameterSlider.kt 程式碼</strong></summary>
package com.yourcompany.cashflow.ui.components
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
@Composable
fun ParameterSlider(
label: String,
value: Float,
onValueChange: (Float) -> Unit,
valueRange: ClosedFloatingPointRange<Float>,
steps: Int,
formatValue: (Float) -> String
) {
Column {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Text(label, style = MaterialTheme.typography.bodyLarge)
Spacer(modifier = Modifier.weight(1f))
Text(formatValue(value), style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.Bold)
}
Slider(
value = value,
onValueChange = onValueChange,
valueRange = valueRange,
steps = steps
)
}
}
</details>
檔案路徑: app/src/main/java/com/yourcompany/cashflow/ui/components/SegmentedButton.kt
<details>
<summary><strong>點此展開 SegmentedButton.kt 程式碼</strong></summary>
package com.yourcompany.cashflow.ui.components
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SegmentedButton(
modifier: Modifier = Modifier,
options: List<String>,
selectedIndex: Int,
onSelectedIndex: (Int) -> Unit
) {
SingleChoiceSegmentedButtonRow(modifier = modifier.fillMaxWidth()) {
options.forEachIndexed { index, label ->
SegmentedButton(
selected = index == selectedIndex,
onClick = { onSelectedIndex(index) },
shape = when (index) {
0 -> SegmentedButtonDefaults.itemShape(index = 0, count = options.size)
options.size - 1 -> SegmentedButtonDefaults.itemShape(index = index, count = options.size)
else -> SegmentedButtonDefaults.itemShape(index = index, count = options.size)
}
) {
Text(label)
}
}
}
}
</details>
檔案路徑: app/src/main/java/com/yourcompany/cashflow/ui/components/TransactionItem.kt
<details>
<summary><strong>點此展開 TransactionItem.kt 程式碼</strong></summary>
package com.yourcompany.cashflow.ui.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.yourcompany.cashflow.data.local.entity.TransactionType
import com.yourcompany.cashflow.data.model.TransactionWithDetails
import java.text.SimpleDateFormat
import java.util.*
@Composable
fun TransactionItem(
transactionDetails: TransactionWithDetails,
onClick: () -> Unit
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp)
.clickable(onClick = onClick),
shape = RoundedCornerShape(8.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Row(
modifier = Modifier
.padding(12.dp)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = transactionDetails.subCategory.name,
fontWeight = FontWeight.Bold,
fontSize = 16.sp
)
if (!transactionDetails.transaction.note.isNullOrEmpty()) {
Spacer(modifier = Modifier.height(4.dp))
Text(
text = transactionDetails.transaction.note,
style = MaterialTheme.typography.bodySmall,
color = Color.Gray
)
}
}
Column(horizontalAlignment = Alignment.End) {
val amountColor = if (transactionDetails.transaction.type == TransactionType.INCOME)
Color(0xFF2E7D32) // Dark Green
else
MaterialTheme.colorScheme.error
Text(
text = "%,.0f".format(transactionDetails.transaction.amount),
color = amountColor,
fontWeight = FontWeight.SemiBold,
fontSize = 18.sp
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = SimpleDateFormat("MM/dd", Locale.getDefault()).format(Date(transactionDetails.transaction.transactionDate)),
style = MaterialTheme.typography.bodySmall,
color = Color.Gray
)
}
}
}
}
</details>
檔案路徑: app/src/main/java/com/yourcompany/cashflow/ui/components/TransactionTypeTabs.kt
<details>
<summary><strong>點此展開 TransactionTypeTabs.kt 程式碼</strong></summary>
package com.yourcompany.cashflow.ui.components
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import com.yourcompany.cashflow.data.local.entity.TransactionType
@Composable
fun TransactionTypeTabs(
selectedType: TransactionType,
onTypeSelected: (TransactionType) -> Unit
) {
val types = listOf("支出", "收入")
val selectedIndex = if (selectedType == TransactionType.EXPENSE) 0 else 1
TabRow(selectedTabIndex = selectedIndex) {
types.forEachIndexed { index, title ->
Tab(
selected = selectedIndex == index,
onClick = {
val type = if (index == 0) TransactionType.EXPENSE else TransactionType.INCOME
onTypeSelected(type)
},
text = { Text(text = title) }
)
}
}
}
</details>
以上是我們專案 ui/navigation/ 和 ui/components/ 套件下所有檔案的最終版本。它們定義了 App 的脈絡和視覺基礎。