# 介紹 ![CleanShot 2024-08-24 at 14.39.39](https://hackmd.io/_uploads/BktNg-wi0.png) - 本次講座將介紹反應式編程概念如何應用於 Android 開發。 - 將學習如何使用 Kotlin 的 Flow 來建模資料流。 - 討論如何優化 Flow 以應對旋轉及應用程序進入後台等情況。 - 將會展示如何測試 Flow 確保其正常運行。 # Reactive ![CleanShot 2024-08-24 at 14.50.47](https://hackmd.io/_uploads/BJVAfWwsA.png) # 反應式編程概念 ![CleanShot 2024-08-24 at 14.40.16](https://hackmd.io/_uploads/HkR8eZPiR.png) ![CleanShot 2024-08-24 at 14.40.57](https://hackmd.io/_uploads/HJbqebvi0.png) ![CleanShot 2024-08-24 at 14.41.52](https://hackmd.io/_uploads/rk2nlZvsA.png) ![CleanShot 2024-08-24 at 14.42.18](https://hackmd.io/_uploads/B1Y0lWDsC.png) ![CleanShot 2024-08-24 at 14.42.26](https://hackmd.io/_uploads/r100xWvjR.png) - 反應式編程強調觀察者會自動對被觀察對象的變化做出反應。 - 資料流應該保持單向流動,以降低錯誤並更易於管理。 - Flow 是 Kotlin 協程庫的一部分,用於處理數據流。 # Flow 的基本結構 ![CleanShot 2024-08-24 at 14.53.12](https://hackmd.io/_uploads/r1NPQZvjR.png) ![CleanShot 2024-08-24 at 14.43.53](https://hackmd.io/_uploads/BJUN-WDiA.png) - Flow 是一種 Kotlin 型別,用來表示資料流,類似於水流的概念。 - 資料源或資料庫通常是資料流的生產者,而 UI 是消費者。 - 在 Android 中,資料來源如 Room 可以通過 Flow 自動推送資料更新到 UI。 # 創建 Flow 的方式 ![CleanShot 2024-08-24 at 14.43.58](https://hackmd.io/_uploads/S13NbWPiC.png) ![CleanShot 2024-08-24 at 14.44.31](https://hackmd.io/_uploads/H1pIbWDiA.png) ![CleanShot 2024-08-24 at 14.44.39](https://hackmd.io/_uploads/HJ7wWWDoC.png) - 大多數情況下,不需要手動創建 Flow,因為許多庫已與協程和 Flow 集成。 - Flow 建構器可以用來創建自定義的 Flow,尤其在需要定期查詢資料時。 - Flow 的執行是順序的,每次資料的更新都會通過 emit 函數推送到 Flow 中。 ![CleanShot 2024-08-24 at 14.45.34](https://hackmd.io/_uploads/Sks9ZWDi0.png) ![CleanShot 2024-08-24 at 14.46.13](https://hackmd.io/_uploads/SyQT-WDs0.png) # Flow 的轉換與操作 ![CleanShot 2024-08-24 at 14.46.20](https://hackmd.io/_uploads/S1k0WZwo0.png) - 使用中間操作符來轉換資料流,例如 map 操作符可將原始資料轉換為 UI 模型。 ![CleanShot 2024-08-24 at 14.54.41](https://hackmd.io/_uploads/S1D6XZDo0.png) ![CleanShot 2024-08-24 at 14.47.12](https://hackmd.io/_uploads/Bk1-MZPsC.png) - 可以使用 filter 操作符來過濾資料,只保留特定條件的項目。 ![CleanShot 2024-08-24 at 14.47.21](https://hackmd.io/_uploads/BJIbf-DiC.png) - 使用 catch 操作符來處理在資料流中出現的異常,例如捕捉並重新拋出異常或發出新值。 ![CleanShot 2024-08-24 at 14.47.27](https://hackmd.io/_uploads/HyjWGbvoC.png) ![CleanShot 2024-08-24 at 14.47.33](https://hackmd.io/_uploads/rkWzMbDi0.png) ![CleanShot 2024-08-24 at 14.47.40](https://hackmd.io/_uploads/rJdGzbPi0.png) ![CleanShot 2024-08-24 at 14.47.55](https://hackmd.io/_uploads/BJdmzZwj0.png) # 觀察 Flow ![CleanShot 2024-08-24 at 14.48.07](https://hackmd.io/_uploads/BJwNfZwiC.png) - 收集 Flow 通常發生在 UI 層,目的是將資料顯示在螢幕上。 ![CleanShot 2024-08-24 at 14.55.36](https://hackmd.io/_uploads/B1Hx4WDoC.png) - 使用終端操作符來開始監聽 Flow 的數據更新,例如 collect 操作符。 ![CleanShot 2024-08-24 at 14.55.45](https://hackmd.io/_uploads/Sym74ZDi0.png) - Cold Flow 是指僅在被觀察時才開始發送數據的 Flow,每次調用 collect 都會創建新的 Flow。 ![CleanShot 2024-08-24 at 14.56.40](https://hackmd.io/_uploads/ryV4EWwsR.png) # 在 Android 中收集 Flow 的最佳做法 ![CleanShot 2024-08-24 at 14.56.56](https://hackmd.io/_uploads/ByISVbvoA.png) ![CleanShot 2024-08-24 at 14.58.43](https://hackmd.io/_uploads/HkbnEbPo0.png) ![CleanShot 2024-08-24 at 14.58.57](https://hackmd.io/_uploads/SJYpN-voR.png) - 必須考慮在應用進入後台時停止收集 Flow 以節省資源。 - 需要處理配置變更,如螢幕旋轉,確保 Flow 繼續正常運行。 - 可以使用 LiveData 或與生命周期相關的協程 API 來優化 Flow 的收集,避免在 UI 不顯示時浪費資源。 # Flow 轉換為 LiveData ![CleanShot 2024-08-24 at 14.59.27](https://hackmd.io/_uploads/B130N-PsC.png) - `asLiveData` 操作符可以將 Flow 轉換為 LiveData,這樣資料只會在 UI 可見時被觀察。 - 在 ViewModel 中進行這樣的轉換,然後在 UI 中以平常的方式消費 LiveData。 - 雖然這樣做很方便,但引入了額外的技術(LiveData),不一定是必要的。 # 使用 repeatOnLifecycle 收集 Flow ![CleanShot 2024-08-24 at 15.02.14](https://hackmd.io/_uploads/rJmKHbwo0.png) ![CleanShot 2024-08-24 at 15.02.19](https://hackmd.io/_uploads/BktKr-DoA.png) ![CleanShot 2024-08-24 at 15.02.27](https://hackmd.io/_uploads/H1RFrZPsC.png) ![CleanShot 2024-08-24 at 15.03.06](https://hackmd.io/_uploads/rJOnHZvjA.png) - `repeatOnLifecycle` 是收集 UI 層 Flow 的推薦方式,這是一個 suspend 函數,接受生命週期狀態作為參數。 - 當生命週期達到指定狀態時,自動啟動協程,當生命週期低於該狀態時,協程自動取消。 - 由於 `repeatOnLifecycle` 是 suspend 函數,它需要在協程中調用。 - 最佳實踐是在生命週期初始化時(例如 `onCreate`)調用此函數。 - `repeatOnLifecycle` 自動處理 UI 生命週期中的狀態變化,而不需要額外的代碼。 # Flow with Lifecycle API ![CleanShot 2024-08-24 at 15.03.37](https://hackmd.io/_uploads/rkvABbvs0.png) - 如果只需要收集單個 Flow,可以使用 `flowWithLifecycle` 操作符來代替 `repeatOnLifecycle`。 - 這個 API 在生命週期進入和退出目標狀態時,會自動發送項目並取消底層的生產者。 # 不推薦的 Flow 收集方式 ![CleanShot 2024-08-24 at 15.03.16](https://hackmd.io/_uploads/BJfprbDjA.png) ![CleanShot 2024-08-24 at 15.03.55](https://hackmd.io/_uploads/rkv1LWvi0.png) ![CleanShot 2024-08-24 at 15.04.00](https://hackmd.io/_uploads/SJnxLZPjA.png) ![CleanShot 2024-08-24 at 15.04.52](https://hackmd.io/_uploads/ByS78bwoC.png) - 直接從 `lifecycleScope.launch` 收集 Flow 不是最安全的方法,因為即使應用在後台,UI 也會繼續更新。 - 這樣做可能導致資源浪費或危險情況,例如在應用後台顯示對話框可能導致應用崩潰。 # 旋轉與配置變更下的 Flow 收集 ![CleanShot 2024-08-24 at 15.05.43](https://hackmd.io/_uploads/HJNLIZPo0.png) ![CleanShot 2024-08-24 at 15.05.58](https://hackmd.io/_uploads/BkMwUZPs0.png) - 旋轉裝置或發生配置變更時,活動可能會重新啟動,但 ViewModel 仍然存在,因此不能簡單地暴露任何 Flow。 - `StateFlow` 是專門為此設計的,類似於水箱的概念,即使沒有收集者,它也會保留數據。 ![CleanShot 2024-08-24 at 15.06.09](https://hackmd.io/_uploads/S1pP8WDjR.png) ![CleanShot 2024-08-24 at 15.06.14](https://hackmd.io/_uploads/S1M_LbvjR.png) ![CleanShot 2024-08-24 at 15.06.20](https://hackmd.io/_uploads/H1PdUZDjA.png) # 使用 StateFlow ![CleanShot 2024-08-24 at 15.06.25](https://hackmd.io/_uploads/Bkad8Wvo0.png) ![CleanShot 2024-08-24 at 15.06.31](https://hackmd.io/_uploads/S1Gt8ZvoA.png) ![CleanShot 2024-08-24 at 15.06.48](https://hackmd.io/_uploads/HJVcIZDjC.png) - 可以將任何 Flow 轉換為 `StateFlow`,以便將最新值存儲並安全地用於 ViewModel 中。 - 使用 `stateIn` 操作符將 Flow 轉換為 `StateFlow`,其中包括初始值、協程範圍以及開始策略。 - 在旋轉場景中,`StateFlow` 保持活躍,確保體驗流暢,而在導航到主畫面時,則會暫停。 ![CleanShot 2024-08-24 at 15.09.18](https://hackmd.io/_uploads/SyimwWvsC.png) ![CleanShot 2024-08-24 at 15.09.23](https://hackmd.io/_uploads/SyWEw-woA.png) ![CleanShot 2024-08-24 at 15.09.34](https://hackmd.io/_uploads/Hyc4vbPjC.png) # 測試 Flow 的技巧 ![CleanShot 2024-08-24 at 15.09.41](https://hackmd.io/_uploads/r1-BDWPi0.png) ![CleanShot 2024-08-24 at 15.09.47](https://hackmd.io/_uploads/B1wHD-DsR.png) ![CleanShot 2024-08-24 at 15.09.55](https://hackmd.io/_uploads/rJAHP-Ds0.png) ![CleanShot 2024-08-24 at 15.09.59](https://hackmd.io/_uploads/H1zIwbDjC.png) ![CleanShot 2024-08-24 at 15.10.03](https://hackmd.io/_uploads/By8LvZvsC.png) ![CleanShot 2024-08-24 at 15.10.06](https://hackmd.io/_uploads/ByK8P-vs0.png) ![CleanShot 2024-08-24 at 15.10.10](https://hackmd.io/_uploads/S1aUDWviC.png) - 測試 Flow 可能會很複雜,但可以使用一些技巧。 - 在測試中,可以替換依賴為假設生產者,並設置所需的測試情景。 - 可以使用 Flow 的 `first` 方法或其他操作符來收集並驗證資料流。 # Resource ![CleanShot 2024-08-24 at 15.10.25](https://hackmd.io/_uploads/Bk-ODZDjR.png) # Terminology - **Reactive Programming**:一種編程範式,透過資料流與變更傳播來建立非同步系統,觀察者會自動對所觀察對象的變化做出反應。 - **Flow**:Kotlin 協程庫中的一種資料流類型,用於建模數據流,可以是任何類型的資料,如使用者數據或 UI 狀態。 - **Producer**:在 Flow 中,負責將資料發送到流中的單元,通常是應用程式的數據來源或資料庫。 - **Consumer**:接收來自 Flow 的數據並使用這些數據的單元,通常是 UI 層,用來顯示資料。 - **Flow Builder**:一個用來創建 Flow 的建構器,可以在其中調用暫停函數,並使用 `emit` 函數將資料加入 Flow。 - **Suspend Function**:一種 Kotlin 中的特殊函數,允許協程在不阻塞主線程的情況下暫停執行。 - **Intermediate Operators**:用於轉換 Flow 中數據的運算符,例如 `map` 和 `filter`,它們創建新的 Flow 並將轉換後的數據發送到下一層。 - **Map Operator**:一種中介運算符,用於將 Flow 中的數據轉換為不同類型或格式的數據。 - **Filter Operator**:一種中介運算符,用於篩選 Flow 中的數據,僅保留滿足條件的數據。 - **Catch Operator**:用於處理 Flow 中異常的運算符,可以捕捉上游的異常,並根據需要重新拋出或發送新值。 - **Terminal Operator**:終端運算符,用於收集 Flow 中的數據,例如 `collect`,是啟動 Flow 的關鍵操作。 - **Cold Flow**:一種在被觀察時才會產生數據的 Flow,只有當終端運算符(如 `collect`)被調用時才會啟動。 - **Repeat on Lifecycle**:一種與 UI 生命週期相關的協程 API,用於在指定的生命週期階段重複執行特定的任務,避免在 UI 不可見時浪費資源。 - **Flow with Lifecycle**:一種協程 API,與 UI 生命週期掛鉤,使 Flow 的收集只在 UI 可見時進行,避免不必要的資源消耗。 - **LiveData**:Android 的一種數據持有者類型,能夠觀察並回應 UI 生命週期的變化。`asLiveData` 是 Flow 的一種操作符,用於將 Flow 轉換為 LiveData,這樣可以只在 UI 可見時觀察資料。 - **Repeat on Lifecycle**:一種協程 API,允許在指定的生命週期狀態下重複執行某些操作,並在生命週期低於該狀態時取消協程。這是 Android 中收集 Flow 的推薦方式。 - **LifecycleScope**:一種與 Android UI 生命週期相關的協程範疇,用於啟動協程並確保它們在 UI 元件的生命週期內適當地啟動和取消。 - **Flow with Lifecycle**:一種協程操作符,與 UI 生命週期掛鉤,當生命週期進入或離開目標狀態時,該操作符會自動開始或取消 Flow 的收集。 - **StateFlow**:Kotlin 協程庫中的一種流類型,用來持有狀態數據。它可以在沒有收集器的情況下保留最後的數據值,並允許多個收集器安全地收集數據,適合用於 ViewModel 中。 - **ViewModelScope**:一種協程範疇,與 ViewModel 的生命週期相關聯,確保協程在 ViewModel 被清除時取消。 - **WhileSubscribed**:一種在 StateFlow 中使用的策略,用於延遲上游 Flow 的取消,以應對短暫的 UI 暫停(如旋轉屏幕)而不影響用戶體驗。 - **SharedFlow**:Kotlin 協程庫中一種熱流類型,允許多個收集器接收相同的流數據,且不需要每個收集器都從頭開始接收數據。