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
(介面)
這個介面定義了所有跟「交易」相關的數據操作。
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
註解,可以幫我們自動完成這種關聯查詢。我們先定義好這個組合資料類別:
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 查詢。
// 在 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 的方法來完成介面規定的任務。
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 資料庫)
+--------------------------------+
我們已經成功地在 ViewModel
和 DAO
之間建立了一個強大而靈活的 Repository
層。
下一步
現在,我們有了 Database
、DAOs
、Repository
,但它們都是分散的元件。問題來了:誰來建立這些類別的實例?RepositoryImpl
需要 TransactionDao
,那 TransactionDao
的實例又是從哪裡來的?
手動一個一個去建立 (new TransactionRepositoryImpl(new TransactionDao(...))
) 會非常麻煩且容易出錯。
這就是 依賴注入 (Dependency Injection) 框架 Hilt 登場的時機了!Hilt 會像一個聰明的管家,自動幫我們建立並「注入」這些我們需要的依"賴"元件。
你準備好學習如何使用 Hilt 來自動化管理這些元件之間的依賴關係,讓我們整個架構無縫地運作起來了嗎?