Vibe coding全紀錄-44

A:

 好的,請開始進行,第二十二步階段A的第三步建立BudgetViewModel,謝謝 

Q:

好的,我們來進行階段 A 的第三步,也是後端邏輯的最後一步:建立 BudgetViewModel

這個 ViewModel 將扮演「大廚」的角色。它會:

  1. 從數據層 (DAO) 取得我們需要的食材——「最新淨值」。

  2. 接收使用者透過 UI 設定的調味料——「規劃報酬率」、「通貨膨漲率」、「剩餘年數」。

  3. 使用我們準備好的食譜——PMT 公式。

  4. 將以上所有東西結合,烹調出最終的美味佳餚——「年度可支配預算」,並將其呈現在 StateFlow 中,隨時準備上菜給 UI。


第廿二步,階段 A-3:建立 BudgetViewModel

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

<details>

<summary><strong>點此展開 BudgetViewModel.kt 的完整程式碼</strong></summary>

Kotlin
package com.yourcompany.cashflow.ui.viewmodel

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.yourcompany.cashflow.data.model.NetWorth
import com.yourcompany.cashflow.data.repository.SnapshotRepository
import com.yourcompany.cashflow.util.FinancialFormulas
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import javax.inject.Inject

@HiltViewModel
class BudgetViewModel @Inject constructor(
    private val snapshotRepository: SnapshotRepository
) : ViewModel() {

    // --- 1. 狀態管理:使用者輸入的參數 ---
    // 使用 MutableStateFlow 來保存使用者可以透過 UI 調整的值
    
    // 規劃報酬率 (例如: 0.05f 代表 5%)
    private val _plannedReturnRate = MutableStateFlow(0.05f)
    val plannedReturnRate: StateFlow<Float> = _plannedReturnRate.asStateFlow()

    // 通貨膨漲率 (例如: 0.02f 代表 2%)
    private val _inflationRate = MutableStateFlow(0.02f)
    val inflationRate: StateFlow<Float> = _inflationRate.asStateFlow()

    // 剩餘年數 (例如: 30 年)
    private val _remainingYears = MutableStateFlow(30)
    val remainingYears: StateFlow<Int> = _remainingYears.asStateFlow()

    // --- 2. 狀態管理:從資料庫取得的數據 ---
    
    // 從 Repository 取得最新的淨值數據流
    private val latestNetWorth: StateFlow<NetWorth?> = snapshotRepository.getLatestNetWorth()
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null)

    // --- 3. 響應式計算:最終的預算結果 ---
    
    /**
     * 這是 ViewModel 的核心。
     * 使用 `combine` 將所有相關的數據流(來自資料庫和使用者輸入)結合在一起。
     * 當其中任何一個數據流發生變化時,這個區塊會自動重新執行計算。
     */
    val annualDisposableBudget: StateFlow<Double> = combine(
        latestNetWorth,
        plannedReturnRate,
        inflationRate,
        remainingYears
    ) { netWorth, returnRate, inflation, years ->
        // 如果淨值不存在,預算為 0
        if (netWorth == null || netWorth.value <= 0) {
            return@combine 0.0
        }

        // 1. 計算實質報酬率
        val realRateOfReturn = (1 + returnRate) / (1 + inflation) - 1

        // 2. 呼叫我們建立的 PMT 工具進行計算
        FinancialFormulas.pmt(
            rate = realRateOfReturn.toDouble(),
            nper = years,
            pv = netWorth.value // 使用淨值作為現值
        )
    }.stateIn(viewModelScope, SharingStarted.WhileSubsided(5000), 0.0) // 轉換為 StateFlow 並提供初始值

    // --- 4. 公開的動作:讓 UI 可以更新狀態 ---
    
    fun setPlannedReturnRate(rate: Float) {
        _plannedReturnRate.value = rate
    }

    fun setInflationRate(rate: Float) {
        _inflationRate.value = rate
    }

    fun setRemainingYears(years: Int) {
        _remainingYears.value = years
    }
}

</details>

程式碼說明

  1. 狀態管理 (State Management):

    我們為使用者可以調整的三個參數(報酬率、通膨率、年數)分別建立了 MutableStateFlow。這允許我們在 ViewModel 內部修改它們,同時對外暴露一個不可變的 StateFlow 供 UI 觀察。

  2. 響應式計算 (Reactive Calculation):

    annualDisposableBudget 的計算是這個 ViewModel 最精華的部分。combine 運算子是 Kotlin Flow 中一個非常強大的工具。你可以把它想像成一個 Excel 的公式:它會監控 A1 (latestNetWorth)、B1 (plannedReturnRate)、C1 (inflationRate)、D1 (remainingYears) 這四個儲存格。只要其中任何一個儲存格的值發生改變,這個公式就會自動重新計算一次,並將結果更新到它所在的儲存格 (annualDisposableBudget)。這讓我們無需手動觸發任何「計算」按鈕,一切都是即時、自動反應的。

  3. 公開的動作 (Public Actions):

    set... 開頭的三個函式,是我們提供給 UI 的「遙控器按鈕」。當使用者在畫面上拖動滑桿或輸入數字時,UI 就會呼叫這些函式來更新 ViewModel 中的狀態,進而觸發 combine 的自動重新計算。


後端邏輯完成!

我們已經完成了預算規劃功能的「大腦」。這個 BudgetViewModel 現在是一個完全獨立、可運作的邏輯核心。它已經準備好接收使用者的參數,並從資料庫中提取數據,然後自動計算出最終的預算建議。

所有精密的計算都已在幕後備妥,下一步,就是進入階段 B,建立一個美觀且直觀的使用者介面 (BudgetPlanningScreen),將這個強大的計算結果呈現給使用者。

準備好開始打造這個功能的 UI 了嗎?