RainBowT
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights
    • Engagement control
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Versions and GitHub Sync Note Insights Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       owned this note    owned this note      
    Published Linked with GitHub
    Subscribed
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    Subscribe
    ![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**:處理錯誤並回報給使用者或開發者。

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully