Vibe coding全紀錄-17

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 在「個人化設定」方面已經非常完善了。使用者可以完全根據自己的習慣來建立帳務的結構。

接下來的開發方向非常令人期待!我們是要:

  1. 回頭去實作「編輯/刪除交易」的功能? 讓記錯的帳有修改的機會。

  2. 還是要開始進行最有趣的「統計圖表」開發? 將數據視覺化,展現財務分析的價值。

你覺得哪個更有吸引力?