![image](https://hackmd.io/_uploads/HJsdWffWgx.png) ### 建立乾淨架構專案概要 * 專案展示結合分頁與快取的啤酒資訊 App * 使用 Punk API 取得啤酒資料,無需 API 金鑰 * 使用 Room 快取資料,支援離線瀏覽 * 整合 Paging 3 Library 處理分頁邏輯 * 使用 Kotlin、Jetpack Compose、Dagger Hilt、Retrofit、Room、Coil --- ### 初始設定與依賴套件 * 建議從影片描述中的 GitHub 倉庫 clone 初始專案 * 使用 Dagger Hilt 管理依賴注入 * 使用 Paging 3 管理分頁與快取流程 * 使用 Retrofit 與 Room 對接 API 與本地快取 * 使用 Coil Compose 載入圖片 * Compose 版本為 1.4.0,Kotlin 為 1.8.10 --- ### Remote 模組設計 * 建立 `BeerApi` 接口使用 Retrofit 呼叫 API * API 呼叫需傳入 `page` 與 `per_page` 參數 * 回傳值為 `List<BeerDto>` * Base URL 設定為 `https://api.punkapi.com/v2/` --- ### Domain 模組與資料模型 * 定義 `Beer` 作為 Domain 層資料模型 * 包含欄位有 id、name、tagline、description、firstBrewed、imageUrl * 作為整體專案中共用的標準資料結構 --- ### Local 模組設計 * 建立 `BeerEntity` 作為 Room Entity 類別 * 使用 `@Entity` 標註,id 為主鍵 * 建立 `BeerDao` 定義資料操作方法 * 提供 `upsertAll` 插入更新資料 * 提供 `getPagingSource` 提供分頁來源 * 提供 `clearAll` 清空資料表 --- ### Mapper 資料轉換 * 建立轉換函式將 `BeerDto` 轉為 `BeerEntity` * 建立轉換函式將 `BeerEntity` 轉為 `Beer` * 確保資料在不同層之間格式一致與解耦 --- ### Room Database 建立 * 建立 `BeerDatabase` 繼承 RoomDatabase * 使用 `@Database` 標註並設定版本與 Entity * 定義抽象方法提供 `BeerDao` 實例 --- ### RemoteMediator 分頁邏輯實作 * 建立 `BeerRemoteMediator` 繼承自 Paging 的 RemoteMediator * 接收 `BeerApi` 與 `BeerDatabase` 作為建構參數 * 根據 LoadType 決定要載入哪一頁資料 * Refresh 載入第一頁,Append 載入下一頁,Prepend 直接結束 * 使用 Retrofit 從 API 載入資料 * 使用 `withTransaction` 保證快取資料寫入的一致性 * Refresh 時先清除本地快取再寫入新資料 * Append 時追加新資料不清除 * 判斷是否為最後一頁以決定是否繼續載入 --- ### 分頁結果與錯誤處理 * 若 API 回傳空清單,視為已達分頁終點 * 成功回傳 `MediatorResult.Success` 並告知是否還有資料 * 捕捉 IOException 與 HttpException 並回傳 `MediatorResult.Error` --- ### 建立 ViewModel 與 Paging Flow * 建立 `BeerViewModel` 並繼承 `ViewModel` * 使用 `@HiltViewModel` 註解並注入 `Pager<Int, BeerEntity>` * 使用 `pager.flow.map` 將 `BeerEntity` 映射為 `Beer` * 使用 `cachedIn(viewModelScope)` 快取 flow * 提供一個 `Flow<PagingData<Beer>>` 給 UI 使用 --- ### 建立 BeerItem Composable * 顯示每筆啤酒資料的卡片 * 使用 `Card` 包含圖片與資訊 * 使用 `Row` 分左右區塊:左為圖片,右為啤酒資訊 * 圖片使用 `AsyncImage` 載入 URL * 使用 `weight` 控制圖片與文字寬度比例 (1:3) * 顯示啤酒名稱、標語、描述、釀造日期 * 使用 `TextAlign.End` 右對齊釀造日期 * 使用 `IntrinsicSize.Max` 統一高度 --- ### 建立 BeerScreen Composable * 使用 `LazyColumn` 顯示清單資料 * 使用 `LazyPagingItems<Beer>` 作為輸入 * 檢查 `loadState.refresh`,顯示中央載入圈 * 若發生錯誤,使用 `Toast` 顯示錯誤訊息 * 顯示每個 `BeerItem`,處理 nullable * 顯示底部附加時的 `CircularProgressIndicator` --- ### MainActivity 整合畫面與 ViewModel * 使用 `@AndroidEntryPoint` 以啟用 Hilt 注入 * 使用 `hiltViewModel<BeerViewModel>()` 取得 ViewModel * 使用 `beerPagingFlow.collectAsLazyPagingItems()` * 將結果傳給 `BeerScreen` --- ### 建立 DI 模組提供依賴 * 在 `di` 套件建立 `AppModule` 使用 `@Module` 與 `@InstallIn(SingletonComponent::class)` * 提供 `BeerDatabase` 使用 `Room.databaseBuilder()` * 提供 `BeerApi` 使用 `Retrofit.Builder()` 建立 * 提供 `Pager<Int, BeerEntity>` * Pager 使用 `PagingConfig` 設定 `pageSize = 20` * 注入 `BeerRemoteMediator` 作為 RemoteMediator * 使用 `beerDb.beerDao().pagingSource()` 作為 PagingSourceFactory --- ### 建立 Application 類別與權限設定 * 建立 `BeerApp` 類別繼承 `Application` * 使用 `@HiltAndroidApp` 啟用 Hilt * 在 `AndroidManifest.xml` 設定 `android:name=".BeerApp"` * 新增 `INTERNET` 權限供 Retrofit 使用 --- ### 功能驗證與錯誤處理測試 * 加入 `delay(2000)` 模擬網路延遲測試載入指示器 * 成功顯示初始載入動畫與啤酒列表 * 滑動至底部成功觸發附加載入動畫 * 開啟飛航模式後重啟 App,可正確讀取快取資料 * 在 App Inspection 工具中確認 Room 資料已寫入 --- ### Terminology * **乾淨架構(Clean Architecture)**:一種將應用分層的架構模式,強調分離關注點與依賴反轉。 * **分頁(Pagination)**:將資料分批載入,以提升效能與用戶體驗。 * **快取(Caching)**:將資料儲存在本地,以便離線存取與減少網路請求。 * **遠端資料來源(Remote Data Source)**:資料來自網路 API 的來源。 * **本地資料來源(Local Data Source)**:資料儲存在本地資料庫中,如 Room。 * **Paging 3**:Google 提供的 Jetpack 分頁庫,用於實作分頁載入。 * **RemoteMediator**:Paging 3 中的元件,協調遠端與本地資料同步。 * **Room**:Google 推出的 SQLite 封裝庫,用於資料持久化。 * **Entity**:資料庫中的資料模型,對應資料表。 * **DTO(Data Transfer Object)**:用於資料傳輸的資料模型,對應 API 結構。 * **Domain Model**:應用的核心資料模型,與外部框架解耦。 * **Mapper**:負責不同資料模型之間的轉換邏輯。 * **Retrofit**:流行的 HTTP 客戶端庫,用於網路請求。 * **Suspend Function**:Kotlin 中的協程函式,用於非同步操作。 * **Query Annotation**:Room 中用於標註 SQL 查詢的方法。 * **Primary Key**:資料表的主鍵,用於唯一識別一筆資料。 * **Abstract Class**:無法實體化的類別,用作基底類別。 * **Database Annotation**:Room 用來標註資料庫設定的註解。 * **Upsert**:資料存在時更新,不存在時插入的操作。 * **PagingSource**:Paging 3 的元件,用於從資料庫中讀取分頁資料。 * **MediatorResult**:RemoteMediator 回傳的結果類型,代表載入成功或失敗。 * **LoadType**:Paging 中的載入類型,如 REFRESH、APPEND、PREPEND。 * **PagingState**:代表當前分頁狀態的物件,如載入位置與資料。 * **withTransaction**:Room 提供的方法,用於執行資料庫交易。 * **Exception Handling**:處理執行期間可能發生的錯誤。 * **IOException**:輸入輸出相關的例外,常見於網路錯誤。 * **HttpException**:HTTP 錯誤碼相關的例外。 * **Coroutine**:Kotlin 的非同步程式設計模型。 * **ViewModel**:MVVM 架構中的元件,負責邏輯與資料管理。 * **Flow**:Kotlin 中的資料流,用於非同步資料串流。 * **Jetpack Compose**:Android 的現代化 UI 框架。 * **Coil**:用於圖片載入的輕量化函式庫。 * **Dependency Injection**:依賴注入,用來降低耦合並提升測試性。 * **Hilt**:Google 官方的依賴注入框架,基於 Dagger。 * **Repository**:負責資料來源調度的層級。 * **Base URL**:API 的基本網址。 * **GET 請求**:從伺服器獲取資料的 HTTP 方法。 * **@Entity Annotation**:標註資料模型為資料表的註解。 * **@Dao Annotation**:標註資料存取介面。 * **@Query Annotation**:標註 SQL 查詢語句。 * **@Insert Annotation**:Room 中用於插入資料的方法註解。 * **@Database Annotation**:定義 Room 資料庫的註解。 * **Nullable**:代表值可以為 null。 * **Companion Object**:Kotlin 提供的靜態物件容器。 * **Annotation Processing**:編譯時自動生成程式碼的處理機制。 * **Page Size**:每頁載入的資料筆數。 * **Key-Based Paging**:根據鍵值來實現分頁。 * **Config Object**:Paging 中的設定物件,定義頁數與大小。 * **End of Pagination**:資料載入至末端的狀態。 * **Append**:在資料列表末端新增資料。 * **Prepend**:在資料列表開頭新增資料(此案例未實作)。 * **State Restoration**:保存與還原分頁狀態的能力。 * **Data Consistency**:確保資料在不同來源間的一致性。 * **ViewModel**:管理 UI 狀態與邏輯,讓 UI 元件保持簡潔。 * **HiltViewModel**:Hilt 提供的註解,讓 ViewModel 支援依賴注入。 * **Pager**:Paging 3 提供的類別,用於建立分頁流程。 * **Flow**:Kotlin 的非同步資料串流,用於觀察分頁資料。 * **PagingData**:代表一組分頁資料的容器,可在 UI 中使用。 * **map 擴展函數**:轉換 Flow 中的資料類型,例如從 Entity 到 Domain Model。 * **cachedIn**:在 ViewModelScope 中快取分頁資料,避免重複載入。 * **LazyPagingItems**:Jetpack Compose 中的分頁資料類型,可與 LazyColumn 結合。 * **Composable**:Compose 的 UI 函式,用於建立可組合的 UI 元件。 * **BeerItem**:顯示單一啤酒資料的 UI 元件。 * **Preview**:Compose 提供的註解,用於預覽 UI 畫面。 * **Modifier**:Compose 中用來設定 UI 元件樣式與佈局的工具。 * **Card**:Compose 提供的元件,用於建立具有陰影與圓角的容器。 * **Row**:Compose 的水平佈局元件。 * **Column**:Compose 的垂直佈局元件。 * **AsyncImage**:Coil 提供的 Compose 圖片載入元件。 * **Weight Modifier**:設定 Row 或 Column 中子項的空間分配比例。 * **Spacer**:Compose 用來加入空間的元件。 * **Text**:Compose 中的文字顯示元件。 * **TextAlign**:設定文字對齊方式的屬性。 * **MaterialTheme**:Compose 的主題系統,提供字型與樣式。 * **Arrangement**:設定子元件間的間距或排列方式。 * **IntrinsicSize.Max**:讓元件高度根據子項最高值決定。 * **CircularProgressIndicator**:Compose 中的圓形載入指示器。 * **Box**:Compose 的堆疊佈局元件,可重疊顯示子項。 * **loadState**:LazyPagingItems 提供的屬性,用於檢查加載狀態。 * **LaunchEffect**:Compose 中用於執行一次性副作用的函式。 * **Toast**:Android 中的提示訊息,用於顯示錯誤或通知。 * **MainActivity**:App 的進入點 Activity。 * **AndroidEntryPoint**:Hilt 的註解,讓 Activity 支援依賴注入。 * **collectAsLazyPagingItems**:將 Flow\<PagingData<T>> 轉為 LazyPagingItems<T>。 * **DI(Dependency Injection)**:依賴注入,用於提供物件實例。 * **AppModule**:集中註冊提供依賴的模組類別。 * **@Module**:Hilt 提供的註解,用於標記提供依賴的類別。 * **@InstallIn**:指定模組要註冊在哪個元件生命週期中。 * **@Singleton**:提供單一實例的註解。 * **@Provides**:標記提供某個依賴的函式。 * **ApplicationContext**:應用程式層級的 Context,用於初始化資源。 * **Room.databaseBuilder**:建立 Room 資料庫的建構器。 * **Retrofit.Builder**:建立 Retrofit 實例的建構器。 * **MoshiConverterFactory**:提供 JSON 解析能力的 Retrofit 工廠。 * **create()**:Retrofit 建立 API 介面實例的方法。 * **provideBeerPager**:DI 中提供 Pager 實例的方法。 * **PagingConfig**:定義分頁大小與行為的設定類別。 * **pagingSourceFactory**:提供本地資料來源的工廠方法。 * **ExperimentalPagingApi**:標記 Paging 3 的實驗性功能。 * **Application 類別**:自訂 App 入口點,用於初始化 Hilt。 * **@HiltAndroidApp**:啟用 Hilt 的註解。 * **Manifest 設定**:指定 App 使用的 Application 類別與權限。 * **Internet 權限**:允許應用程式存取網路。 * **App Inspection**:Android Studio 工具,可檢查 Room 資料內容。 * **Airplane Mode**:測試無網路情況下的快取行為。 * **Error Handling**:處理錯誤並回報給使用者或開發者。