好的,我們立刻開始執行階段 A 的下一步。
現在我們的資料庫已經準備就緒,是時候建立使用者可以真正看到並互動的介面了。我們將建立 ProjectViewModel
作為畫面的大腦,並打造 ProjectListScreen
(專案列表)和 AddEditProjectScreen
(新增/編輯專案)兩個畫面。
第十七步:建立 ProjectViewModel 與管理介面
第一步:建立 ProjectViewModel
這個 ViewModel 將集中處理所有與專案管理相關的邏輯,例如過濾專案狀態、新增、更新等。
路徑: app/src/main/java/com/yourcompany/cashflow/ui/viewmodel/ProjectViewModel.kt
(新檔案)
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
(新檔案)
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
(新檔案)
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
)
// ... (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
回呼。
基本管理功能完成!
恭喜!我們已經成功建立了一個完整的專案管理模組的基礎。使用者現在可以自由地建立、查看(依狀態篩選)並編輯他們的財務目標了。
根據我們的藍圖,下一步是第十八步:建立專案詳情與進度追蹤頁面。在這個頁面,使用者將能清楚地看到單一專案的預算使用狀況,並檢視所有歸屬於此的交易明細。
準備好打造這個專案的儀表板了嗎?