A:
好的,請開始第二十二步的階段 B:使用者介面開發中的第四步:建立 BudgetPlanningScreen,謝謝
Q:
好的,我們來進行開發的視覺化階段!這一步的目標是打造一個簡潔、互動性強的儀表板,讓使用者可以輕鬆地調整參數,並即時看到自己的未來財務規劃結果。
我們將會建立 BudgetPlanningScreen,並在其中放置幾個核心的 UI 元件。
第廿二步,階段 B-4:建立 BudgetPlanningScreen
第一步:建立可重複使用的參數滑桿 (ParameterSlider)
為了避免重複撰寫三個幾乎一樣的滑桿,我們先建立一個通用的 Composable。
路徑: app/src/main/java/com/yourcompany/cashflow/ui/components/ParameterSlider.kt (新檔案)
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.foundation.layout.width
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
import androidx.compose.ui.unit.dp
@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
)
}
}
第二步:建立 BudgetPlanningScreen 主畫面
現在,我們來組合出完整的規劃頁面。
路徑: app/src/main/java/com/yourcompany/cashflow/ui/screens/BudgetPlanningScreen.kt (新檔案)
<details>
<summary><strong>點此展開 BudgetPlanningScreen.kt 的完整程式碼</strong></summary>
package com.yourcompany.cashflow.ui.screens
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Info
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.yourcompany.cashflow.ui.components.ParameterSlider
import com.yourcompany.cashflow.ui.viewmodel.BudgetViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BudgetPlanningScreen(
viewModel: BudgetViewModel = hiltViewModel()
) {
val plannedReturnRate by viewModel.plannedReturnRate.collectAsStateWithLifecycle()
val inflationRate by viewModel.inflationRate.collectAsStateWithLifecycle()
val remainingYears by viewModel.remainingYears.collectAsStateWithLifecycle()
val annualBudget by viewModel.annualDisposableBudget.collectAsStateWithLifecycle()
// 我們需要從 ViewModel 取得最新淨值來顯示
// 假設已在 ViewModel 中加入 val latestNetWorth: StateFlow<NetWorth?>
// val netWorth by viewModel.latestNetWorth.collectAsStateWithLifecycle()
Scaffold(
topBar = {
TopAppBar(title = { Text("年度預算規劃") })
}
) { innerPadding ->
LazyColumn(
modifier = Modifier.padding(innerPadding),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// 項目一:結果顯示區
item {
ResultCard(
annualBudget = annualBudget,
// currentNetWorth = netWorth?.value ?: 0.0 // 傳入當前淨值
)
}
// 項目二:參數調整區
item {
ParametersCard(
returnRate = plannedReturnRate,
onReturnRateChange = { viewModel.setPlannedReturnRate(it) },
inflationRate = inflationRate,
onInflationRateChange = { viewModel.setInflationRate(it) },
years = remainingYears.toFloat(),
onYearsChange = { viewModel.setRemainingYears(it.toInt()) }
)
}
// 項目三:儲存按鈕
item {
Button(
onClick = { /* TODO: Step 22-C: Save budget to DataStore */ },
modifier = Modifier.fillMaxWidth()
) {
Text("儲存為本年度預算")
}
}
// 項目四:提示說明
item {
Text(
"提示:此計算基於 PMT 模型,結果僅為財務參考,實際市場狀況可能有所不同。",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
@Composable
fun ResultCard(annualBudget: Double, /* currentNetWorth: Double */) {
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(4.dp)
) {
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("年度可支配預算", style = MaterialTheme.typography.titleMedium)
Text(
text = "NT$ ${"%,.0f".format(annualBudget)}",
style = MaterialTheme.typography.displayMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "每月平均 NT$ ${"%,.0f".format(annualBudget / 12)}",
style = MaterialTheme.typography.bodyLarge
)
// Divider(modifier = Modifier.padding(vertical = 16.dp))
// Text("計算基礎:當前總淨值 NT$ ${"%,.0f".format(currentNetWorth)}")
}
}
}
@Composable
fun ParametersCard(
returnRate: Float, onReturnRateChange: (Float) -> Unit,
inflationRate: Float, onInflationRateChange: (Float) -> Unit,
years: Float, onYearsChange: (Float) -> Unit
) {
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
Text("規劃參數", style = MaterialTheme.typography.titleLarge)
// 規劃報酬率滑桿
ParameterSlider(
label = "規劃報酬率",
value = returnRate,
onValueChange = onReturnRateChange,
valueRange = 0f..0.15f, // 0% to 15%
steps = 14, // 15 steps
formatValue = { "%.1f %%".format(it * 100) }
)
// 通貨膨脹率滑桿
ParameterSlider(
label = "通貨膨脹率",
value = inflationRate,
onValueChange = onInflationRateChange,
valueRange = 0f..0.05f, // 0% to 5%
steps = 9, // 10 steps
formatValue = { "%.1f %%".format(it * 100) }
)
// 剩餘年數滑桿
ParameterSlider(
label = "剩餘年數",
value = years,
onValueChange = onYearsChange,
valueRange = 5f..50f, // 5 to 50 years
steps = 44, // 45 steps
formatValue = { "%.0f 年".format(it) }
)
}
}
}
</details>
第三步:整合導覽,加入「規劃」分頁
現在 App 有了第四個主要功能,我們在底部導覽列為它加上一個專屬的入口。
A. 更新 BottomNavItem.kt
路徑: app/src/main/java/com/yourcompany/cashflow/ui/navigation/BottomNavItem.kt (假設你已將它移到新資料夾)
// ...
import androidx.compose.material.icons.filled.QueryStats // 匯入一個新圖示
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.QueryStats, "規劃") // <-- 新增項目
object Settings : BottomNavItem(Routes.SETTINGS, Icons.Default.Settings, "設定")
}
B. 在 Routes.kt 和 AppNavHost.kt 中註冊新畫面
在
Routes物件中新增const val PLANNING = "planning"。在
MainScreen.kt(或AppNavHost.kt) 的NavHost中,加入新的composable(BottomNavItem.Planning.route) { BudgetPlanningScreen() }。在
BottomNavigationBarComposable 中,將BottomNavItem.Planning加入到items列表中。
執行與預期結果
當你完成以上步驟並執行 App:
你會在底部導覽列看到一個新的「規劃」分頁。
點擊進入後,會看到我們剛剛建立的儀表板。
最重要的是,你可以試著拖動滑桿,你會發現上方的「年度可支配預算」和「每月平均金額」會像施了魔法一樣,即時地跟著你的調整而變化!這完美地展示了我們從一開始就建立的「響應式」架構的強大威力。
UI 開發完成!
我們已經成功地為這個複雜的財務規劃功能,打造了一個直觀、互動的介面。
現在,只剩下最後的臨門一腳:實作「儲存」按鈕的邏輯,將使用者辛苦規劃出來的預算數字保存下來,並最終整合到我們的記帳主畫面上,讓它真正發揮「預算追蹤」的作用。準備好完成這最後的整合了嗎?