A:
好的,請進行最核心的新增/編輯對話框,謝謝
Q:
好的,我們來完成最後、也是最核心的一塊拼圖——建立一個能同時處理「新增/編輯主分類」與「新增/編輯子分類」這四種情況的通用對話框。
為了優雅地處理這四種不同的狀態,我們將使用一個非常強大的 Kotlin 特性:sealed class (密封類別)。這能幫助我們清晰地管理對話框該顯示什麼內容、該執行什麼動作。
第十二步:完成通用的新增/編輯對話框
第一步:定義對話框的狀態 (Dialog State)
我們建立一個 sealed class 來代表對話框所有可能的狀態。這比使用多個布林值 (showAddDialog, showEditDialog...) 要乾淨得多。
在 CategoryManagementScreen.kt 檔案的頂部(import 下方)或一個新的 state 套件中加入以下程式碼:
Kotlin
// 可以直接放在 CategoryManagementScreen.kt 檔案的頂部
package com.yourcompany.cashflow.ui.screens
import com.yourcompany.cashflow.data.local.entity.MainCategory
import com.yourcompany.cashflow.data.local.entity.SubCategory
// 定義對話框的所有可能狀態
sealed class CategoryDialogState {
object Hidden : CategoryDialogState() // 隱藏狀態
data class AddingMainCategory(val type: TransactionType) : CategoryDialogState()
data class EditingMainCategory(val mainCategory: MainCategory) : CategoryDialogState()
data class AddingSubCategory(val parentCategory: MainCategory) : CategoryDialogState()
data class EditingSubCategory(val subCategory: SubCategory) : CategoryDialogState()
}
第二步:建立通用的分類對話框 Composable
現在我們來建立一個新的 Composable,它會根據傳入的 CategoryDialogState 來改變自己的外觀和行為。
Kotlin
// /ui/components/CategoryDialog.kt (建議建立新檔案)
package com.yourcompany.cashflow.ui.components
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Arrangement
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 // 預設值,但不會顯示
}
)
}
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()) {
// 如果是主分類,需要傳遞 type
val typeToConfirm = if (state is CategoryDialogState.AddingMainCategory || state is CategoryDialogState.EditingMainCategory) {
transactionType
} else {
null // 子分類不需要 type
}
onConfirm(name, typeToConfirm)
}
}
) { Text("儲存") }
},
dismissButton = {
TextButton(onClick = onDismiss) { Text("取消") }
}
)
}
// 這是我們在 AddTransactionScreen 中建立過的,可以把它移到 /ui/components/ 方便共用
@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) }
)
}
}
}
第三步:整合到 CategoryManagementScreen
最後,我們在主畫面中管理 dialogState 並呼叫我們剛剛建立的 CategoryDialog。
Kotlin
// /ui/screens/CategoryManagementScreen.kt (更新此檔案)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CategoryManagementScreen(
viewModel: CategoryViewModel = hiltViewModel(),
onNavigateBack: () -> Unit
) {
val categories by viewModel.categories.collectAsStateWithLifecycle()
// 用我們定義的 Sealed Class 來管理 Dialog 狀態
var dialogState by remember { mutableStateOf<CategoryDialogState>(CategoryDialogState.Hidden) }
Scaffold(
topBar = { ... },
floatingActionButton = {
FloatingActionButton(onClick = {
// 打開 Dialog,並設定為「新增主分類」狀態
dialogState = CategoryDialogState.AddingMainCategory(TransactionType.EXPENSE)
}) {
Icon(Icons.Default.Add, "新增主分類")
}
}
) { innerPadding ->
LazyColumn( ... ) {
items(categories) { categoryWithSubs ->
ExpandableMainCategoryItem(
categoryWithSubCategories = categoryWithSubs,
// 點擊按鈕時,更新 dialogState
onAddSubCategory = { mainCategory ->
dialogState = CategoryDialogState.AddingSubCategory(mainCategory)
},
onEditMainCategory = { mainCategory ->
dialogState = CategoryDialogState.EditingMainCategory(mainCategory)
},
onDeleteMainCategory = { viewModel.deleteMainCategory(it) },
onEditSubCategory = { subCategory ->
dialogState = CategoryDialogState.EditingSubCategory(subCategory)
},
onDeleteSubCategory = { viewModel.deleteSubCategory(it) }
)
}
}
}
// 在 Scaffold 的範圍內呼叫 Dialog
CategoryDialog(
state = dialogState,
onDismiss = { dialogState = CategoryDialogState.Hidden }, // 關閉 Dialog
onConfirm = { name, type ->
// 根據目前的 state,執行對應的 ViewModel 操作
when (val currentState = dialogState) {
is CategoryDialogState.AddingMainCategory -> {
viewModel.addMainCategory(name, type!!)
}
is CategoryDialogState.EditingMainCategory -> {
viewModel.updateMainCategory(currentState.mainCategory.copy(name = name, type = type!!))
}
is CategoryDialogState.AddingSubCategory -> {
viewModel.addSubCategory(name, currentState.parentCategory.id)
}
is CategoryDialogState.EditingSubCategory -> {
viewModel.updateSubCategory(currentState.subCategory.copy(name = name))
}
else -> {}
}
dialogState = CategoryDialogState.Hidden // 操作完成後關閉 Dialog
}
)
}
大功告成!
現在,執行你的 App!進入「設定」->「管理分類」,你將擁有一個功能齊全的管理介面:
點擊右下角的
+按鈕可以新增主分類。點擊主分類項目上的
+圖示可以為它新增子分類。點擊主分類和子分類上的「編輯」圖示,可以修改它們的名稱。
所有的新增、修改、刪除操作,都會即時反應在畫面上。
到這裡,你的 App 在「個人化設定」方面已經非常完善了。使用者可以完全根據自己的習慣來建立帳務的結構。
接下來的開發方向非常令人期待!我們是要:
回頭去實作「編輯/刪除交易」的功能? 讓記錯的帳有修改的機會。
還是要開始進行最有趣的「統計圖表」開發? 將數據視覺化,展現財務分析的價值。
你覺得哪個更有吸引力?