Vibe coding全紀錄-32

A:
好的,請開始下一步,第十七步,建立對應的 ProjectViewModel 和使用者介面 (ProjectListScreen, AddEditProjectScreen),讓使用者可以真正地在畫面上與這些專案數據進行互動,謝謝 

Q:

好的,我們立刻開始執行階段 A 的下一步。

現在我們的資料庫已經準備就緒,是時候建立使用者可以真正看到並互動的介面了。我們將建立 ProjectViewModel 作為畫面的大腦,並打造 ProjectListScreen(專案列表)和 AddEditProjectScreen(新增/編輯專案)兩個畫面。


第十七步:建立 ProjectViewModel 與管理介面

第一步:建立 ProjectViewModel

這個 ViewModel 將集中處理所有與專案管理相關的邏輯,例如過濾專案狀態、新增、更新等。

路徑: app/src/main/java/com/yourcompany/cashflow/ui/viewmodel/ProjectViewModel.kt (新檔案)

Kotlin
package com.yourcompany.cashflow.ui.viewmodel

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.yourcompany.cashflow.data.local.entity.Project
import com.yourcompany.cashflow.data.local.entity.ProjectStatus
import com.yourcompany.cashflow.data.repository.ProjectRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject

@HiltViewModel
class ProjectViewModel @Inject constructor(
    private val projectRepository: ProjectRepository
) : ViewModel() {

    // 用於管理篩選器狀態 (進行中/已完成)
    private val _statusFilter = MutableStateFlow(ProjectStatus.ACTIVE)
    val statusFilter: StateFlow<ProjectStatus> = _statusFilter.asStateFlow()

    // 根據篩選器狀態,動態地從資料庫取得對應的專案列表
    @OptIn(ExperimentalCoroutinesApi::class)
    val projects: StateFlow<List<Project>> = _statusFilter.flatMapLatest { status ->
        projectRepository.getProjectsByStatus(status)
    }.stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5000),
        initialValue = emptyList()
    )

    // 用於編輯模式,持有正在編輯的專案資料
    private val _selectedProject = MutableStateFlow<Project?>(null)
    val selectedProject: StateFlow<Project?> = _selectedProject.asStateFlow()

    fun setFilter(status: ProjectStatus) {
        _statusFilter.value = status
    }

    fun loadProject(id: Long) {
        if (id == 0L) {
            _selectedProject.value = null
            return
        }
        viewModelScope.launch {
            projectRepository.getProjectById(id).collect {
                _selectedProject.value = it
            }
        }
    }

    fun addProject(name: String, budget: Double, description: String?) {
        viewModelScope.launch {
            val newProject = Project(
                name = name,
                budget = budget,
                description = description,
                status = ProjectStatus.ACTIVE // 新專案預設為進行中
            )
            projectRepository.insert(newProject)
        }
    }

    fun updateProject(name: String, budget: Double, description: String?) {
        viewModelScope.launch {
            val projectToUpdate = _selectedProject.value ?: return@launch
            projectRepository.update(
                projectToUpdate.copy(
                    name = name,
                    budget = budget,
                    description = description
                )
            )
        }
    }
    
    fun updateProjectStatus(project: Project, status: ProjectStatus) {
        viewModelScope.launch {
            projectRepository.update(project.copy(status = status))
        }
    }
}

第二步:建立專案列表畫面 (ProjectListScreen)

這個畫面讓使用者能看到所有專案,並提供篩選功能。

路徑: app/src/main/java/com/yourcompany/cashflow/ui/screens/ProjectListScreen.kt (新檔案)

Kotlin
package com.yourcompany.cashflow.ui.screens

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.Add
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.*
import androidx.compose.runtime.*
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.data.local.entity.ProjectStatus
import com.yourcompany.cashflow.ui.viewmodel.ProjectViewModel

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ProjectListScreen(
    viewModel: ProjectViewModel = hiltViewModel(),
    onNavigateBack: () -> Unit,
    onNavigateToProjectDetail: (Long) -> Unit
) {
    val projects by viewModel.projects.collectAsStateWithLifecycle()
    val selectedStatus by viewModel.statusFilter.collectAsStateWithLifecycle()

    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text("管理專案") },
                navigationIcon = { IconButton(onClick = onNavigateBack) { Icon(Icons.Default.ArrowBack, "返回") } }
            )
        },
        floatingActionButton = {
            FloatingActionButton(onClick = { onNavigateToProjectDetail(0L) }) { // 傳入 0 代表新增
                Icon(Icons.Default.Add, "新增專案")
            }
        }
    ) { innerPadding ->
        Column(modifier = Modifier.padding(innerPadding)) {
            // 狀態篩選器
            TabRow(selectedTabIndex = selectedStatus.ordinal) {
                ProjectStatus.values().forEach { status ->
                    Tab(
                        selected = status == selectedStatus,
                        onClick = { viewModel.setFilter(status) },
                        text = { Text(if (status == ProjectStatus.ACTIVE) "進行中" else "已完成") }
                    )
                }
            }

            // 專案列表
            LazyColumn(modifier = Modifier.fillMaxSize()) {
                items(projects) { project ->
                    ListItem(
                        headlineText = { Text(project.name) },
                        supportingText = { Text("預算: NT$ ${"%,.0f".format(project.budget)}") },
                        modifier = Modifier.clickable { onNavigateToProjectDetail(project.id) }
                    )
                    Divider()
                }
            }
        }
    }
}

第三步:建立新增/編輯專案畫面 (AddEditProjectScreen)

這個畫面將重複用於新增和編輯兩種情境。

路徑: app/src/main/java/com/yourcompany/cashflow/ui/screens/AddEditProjectScreen.kt (新檔案)

Kotlin
package com.yourcompany.cashflow.ui.screens

import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Done
import androidx.compose.material3.*
import androidx.compose.runtime.*
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.viewmodel.ProjectViewModel

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AddEditProjectScreen(
    projectId: Long,
    viewModel: ProjectViewModel = hiltViewModel(),
    onNavigateBack: () -> Unit
) {
    val isEditMode = projectId != 0L
    val project by viewModel.selectedProject.collectAsStateWithLifecycle()

    var name by remember { mutableStateOf("") }
    var budget by remember { mutableStateOf("") }
    var description by remember { mutableStateOf("") }

    // 當進入編輯模式時,觸發 ViewModel 載入專案資料
    LaunchedEffect(key1 = Unit) {
        if (isEditMode) {
            viewModel.loadProject(projectId)
        }
    }

    // 當 ViewModel 的資料載入後,更新 UI 上的欄位
    LaunchedEffect(key1 = project) {
        if (isEditMode && project != null) {
            name = project!!.name
            budget = project!!.budget.toLong().toString() // 轉為整數顯示
            description = project!!.description ?: ""
        }
    }

    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text(if (isEditMode) "編輯專案" else "新增專案") },
                navigationIcon = { IconButton(onClick = onNavigateBack) { Icon(Icons.Default.ArrowBack, "返回") } }
            )
        },
        floatingActionButton = {
            FloatingActionButton(onClick = {
                val budgetDouble = budget.toDoubleOrNull()
                if (name.isNotBlank() && budgetDouble != null) {
                    if (isEditMode) {
                        viewModel.updateProject(name, budgetDouble, description)
                    } else {
                        viewModel.addProject(name, budgetDouble, description)
                    }
                    onNavigateBack()
                }
            }) {
                Icon(Icons.Default.Done, "儲存")
            }
        }
    ) { innerPadding ->
        Column(
            modifier = Modifier
                .padding(innerPadding)
                .padding(16.dp),
            verticalArrangement = Arrangement.spacedBy(16.dp)
        ) {
            OutlinedTextField(value = name, onValueChange = { name = it }, label = { Text("專案名稱") }, modifier = Modifier.fillMaxWidth())
            OutlinedTextField(value = budget, onValueChange = { budget = it }, label = { Text("預算金額") }, modifier = Modifier.fillMaxWidth())
            OutlinedTextField(value = description, onValueChange = { description = it }, label = { Text("描述 (可選)") }, modifier = Modifier.fillMaxWidth(), minLines = 3)
        }
    }
}

第四步:整合導航

最後,我們將這些新畫面加入到我們的 AppNavigation 中。

路徑: app/src/main/java/com/yourcompany/cashflow/ui/AppNavigation.kt (或 MainScreen.kt 中的 AppNavHost)

Kotlin
// ... (imports)
import androidx.navigation.NavType
import androidx.navigation.navArgument
import com.yourcompany.cashflow.ui.screens.AddEditProjectScreen
import com.yourcompany.cashflow.ui.screens.ProjectListScreen

object Routes {
    // ... (現有路線)
    const val PROJECT_LIST = "project_list"
    const val ADD_EDIT_PROJECT = "add_edit_project"
}

// 在你的 NavHost 中加入以下 composable
@Composable
fun AppNavHost(navController: NavHostController, modifier: Modifier = Modifier) {
    NavHost(...) {
        // ... (現有的 composable 路由)
        
        composable(Routes.SETTINGS) {
            SettingsScreen(
                ...
                onNavigateToProjectManagement = { navController.navigate(Routes.PROJECT_LIST) }
            )
        }

        composable(Routes.PROJECT_LIST) {
            ProjectListScreen(
                onNavigateBack = { navController.popBackStack() },
                onNavigateToProjectDetail = { projectId ->
                    navController.navigate("${Routes.ADD_EDIT_PROJECT}?projectId=$projectId")
                }
            )
        }
        
        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() }
            )
        }
    }
}

別忘了:要去 SettingsScreen.kt 啟用「管理專案」的按鈕,讓 onClick 指向 onNavigateToProjectManagement 回呼。


基本管理功能完成!

恭喜!我們已經成功建立了一個完整的專案管理模組的基礎。使用者現在可以自由地建立、查看(依狀態篩選)並編輯他們的財務目標了。

根據我們的藍圖,下一步是第十八步:建立專案詳情與進度追蹤頁面。在這個頁面,使用者將能清楚地看到單一專案的預算使用狀況,並檢視所有歸屬於此的交易明細。

準備好打造這個專案的儀表板了嗎?