Vibe coding全紀錄-6

A:

 好的,現在讓我們一步一步慢慢進行下一步建立一個 Repository (資料倉庫層),作為 ViewModel 和資料庫之間的橋樑,謝謝 

Q:

好的,我們繼續!現在我們已經有了堅實的資料庫地基,接下來就要蓋一座橋樑,也就是 Repository (資料倉庫層)

這一步非常關鍵,它能讓我們的 App 架構變得乾淨、有彈性且易於測試。


第五步:建立 Repository (資料倉庫層)

1. 為什麼需要 Repository?它扮演什麼角色?

你可以把 Repository 想像成一個 App 的「數據總管」。

目前,我們的數據來源只有一個:Room 資料庫。但未來,App 可能會變得更複雜,例如:

  • 需要從網路 API 下載最新的匯率。

  • 需要從手機的檔案系統讀取備份資料。

  • 需要快取一些常用資料在記憶體中,以加快讀取速度。

如果讓 ViewModel (負責處理 UI 邏輯) 直接去跟 Room、網路、檔案系統等不同的數據來源溝通,那 ViewModel 就會變得非常混亂且難以維護。

Repository 的職責就是將這種複雜性隱藏起來。

它為 ViewModel 提供一個乾淨、統一的介面來存取數據。ViewModel 只需對 Repository 說:「請給我交易紀錄」,它完全不需要知道這些紀錄是從資料庫來的,還是從網路來的。所有判斷和數據整合的邏輯都封裝在 Repository 內部。

這座「橋樑」的核心價值:

  • 關注點分離 (Separation of Concerns): ViewModel 專注於 UI 相關的邏輯,Repository 專注於數據的來源與管理。

  • 單一數據來源 (Single Source of Truth): Repository 決定了數據的最終來源,確保 App 各處的數據一致。

  • 可測試性 (Testability): 在測試 ViewModel 時,我們可以輕易地用一個「假的」Repository 來替換掉真實的,從而讓我們可以專注測試 ViewModel 的邏輯,而不用啟動一個真實的資料庫。

2. 實作步驟:從介面開始

在軟體工程中,一個很好的實踐是「面向介面編程」。我們先定義一個「合約」(Interface),規定這個 Repository 能做什麼,然後再寫一個實作類別 (Implementation Class) 去實現 具體怎麼做

A. 建立 TransactionRepository.kt (介面)

這個介面定義了所有跟「交易」相關的數據操作。

Kotlin
import kotlinx.coroutines.flow.Flow

/**
 * 交易數據倉庫的介面 (合約)
 * 定義了 ViewModel 可以對交易數據執行的所有操作
 */
interface TransactionRepository {

    /**
     * 取得所有交易紀錄的詳細資訊。
     * 我們不只回傳 Transaction,而是回傳一個包含分類資訊的組合物件。
     * 使用 Flow 可以在資料庫變動時自動收到更新。
     */
    fun getAllTransactionDetails(): Flow<List<TransactionWithDetails>>

    /**
     * 插入一筆新的交易紀錄。
     * suspend 關鍵字表示這是一個協程函式,不會阻塞主執行緒。
     */
    suspend fun insertTransaction(transaction: Transaction)

    // 未來可以增加更多方法,例如:
    // suspend fun getTransactionById(id: Long): TransactionWithDetails?
    // suspend fun deleteTransaction(transaction: Transaction)
}

B. 建立一個新的資料模型 TransactionWithDetails.kt

你會發現上面的介面回傳了一個 Flow<List<TransactionWithDetails>>Transaction 物件本身只存了 subCategoryId,這在畫面上顯示很不方便。我們真正需要的是一個包含了主、子類別名稱的完整物件。

Room 提供了強大的 @Relation 註解,可以幫我們自動完成這種關聯查詢。我們先定義好這個組合資料類別:

Kotlin
import androidx.room.Embedded
import androidx.room.Relation

/**
 * 這不是一個資料表 (Entity),而是一個用於查詢結果的資料組合物件 (Data Transfer Object)。
 * 它代表了一筆交易以及其所有關聯的詳細資訊。
 */
data class TransactionWithDetails(
    // @Embedded 會將 Transaction 物件的所有欄位直接嵌入到這個類別中
    @Embedded
    val transaction: Transaction,

    // @Relation 用來定義關聯查詢
    @Relation(
        parentColumn = "subCategoryId", // Transaction 表中的欄位
        entityColumn = "id"           // SubCategory 表中的欄位
    )
    val subCategory: SubCategory, // Room 會自動根據 ID 找到對應的 SubCategory

    @Relation(
        parentColumn = "accountId", // Transaction 表中的欄位
        entityColumn = "id"         // Account 表中的欄位
    )
    val account: Account // Room 會自動根據 ID 找到對應的 Account
    
    // 思考:我們還需要主類別名稱,但 SubCategory 裡只有 mainCategoryId。
    // 我們可以在 DAO 查詢後,手動在 Repository 中組合主類別資訊,
    // 或是在 DAO 中使用更複雜的 SQL 查詢。我們先從簡單的開始。
)

C. 更新 TransactionDao.kt

現在,我們需要在 DAO 中加入一個方法來查詢這個 TransactionWithDetails。Room 會幫我們處理好背後的 JOIN 查詢。

Kotlin
// 在 TransactionDao.kt 中加入這個新方法
import androidx.room.Transaction // 注意:這裡要 import androidx.room.Transaction

@Transaction // 當一個查詢需要跨越多個資料表時,使用 @Transaction 註解可以確保操作的原子性
@Query("SELECT * FROM transactions ORDER BY transactionDate DESC")
fun getAllTransactionDetails(): Flow<List<TransactionWithDetails>>

D. 建立 TransactionRepositoryImpl.kt (實作類別)

這個類別才是真正做事的地方。它會持有 DAO 的實例,並呼叫 DAO 的方法來完成介面規定的任務。

Kotlin
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject // 我們先加入 Hilt 的 Inject 註解,為下一步做準備

/**
 * TransactionRepository 的具體實作。
 * 它的職責是與本地端資料庫 (DAO) 溝通。
 * @Inject constructor(...) 告訴 Hilt 如何建立這個類別的實例。
 */
class TransactionRepositoryImpl @Inject constructor(
    private val transactionDao: TransactionDao,
    private val categoryDao: CategoryDao // 如果需要,也可以注入其他的 DAO
) : TransactionRepository {

    override fun getAllTransactionDetails(): Flow<List<TransactionWithDetails>> {
        // 目前的邏輯很簡單,就是直接呼叫 DAO 的方法並回傳結果。
        // 未來如果需要從網路獲取數據,相關的判斷邏輯就會寫在這裡。
        return transactionDao.getAllTransactionDetails()
    }

    override suspend fun insertTransaction(transaction: Transaction) {
        // 同樣,直接呼叫 DAO 的方法。
        transactionDao.insert(transaction)
    }
}

3. 視覺化我們的架構

到目前為止,我們的 App 架構層次如下,數據流向由上至下:

+--------------------------------+
|      UI / Compose 畫面         |  (使用者看到的畫面)
+--------------------------------+
               |
               v (呼叫方法)
+--------------------------------+
|           ViewModel            |  (持有 UI 狀態,處理使用者操作)
+--------------------------------+
               |
               v (透過介面呼叫)
+--------------------------------+
|  Repository (Interface)        |  (定義了「能做什麼」的合約)
+--------------------------------+
               ^
               | (被實作)
+--------------------------------+
|  RepositoryImpl (Class)        |  (真正做事的地方,管理數據來源)
+--------------------------------+
               |
               v (呼叫 DAO 方法)
+--------------------------------+
|       DAO (Data Access Object) |  (定義如何存取資料庫)
+--------------------------------+
               |
               v (實際操作)
+--------------------------------+
|         Room Database          |  (底層的 SQLite 資料庫)
+--------------------------------+

我們已經成功地在 ViewModelDAO 之間建立了一個強大而靈活的 Repository 層。


下一步

現在,我們有了 DatabaseDAOsRepository,但它們都是分散的元件。問題來了:誰來建立這些類別的實例?RepositoryImpl 需要 TransactionDao,那 TransactionDao 的實例又是從哪裡來的?

手動一個一個去建立 (new TransactionRepositoryImpl(new TransactionDao(...))) 會非常麻煩且容易出錯。

這就是 依賴注入 (Dependency Injection) 框架 Hilt 登場的時機了!Hilt 會像一個聰明的管家,自動幫我們建立並「注入」這些我們需要的依"賴"元件。

你準備好學習如何使用 Hilt 來自動化管理這些元件之間的依賴關係,讓我們整個架構無縫地運作起來了嗎?