做自(zì)由與創造的先行者

Android界面層

Android開(kāi)發手冊

界面的作(zuò)用(yòng)是在屏幕上(shàng)顯示應用(yòng)數據,并充當主要的用(yòng)戶互動點。每當數據發生變化時(shí),無論是因爲用(yòng)戶互動(例如按了(le)某個按鈕),還是因爲外(wài)部輸入(例如網絡響應),界面都應随之更新,以反映這(zhè)些(xiē)變化。實際上(shàng),界面是從(cóng)數據層獲取的應用(yòng)狀态的直觀呈現(xiàn)。

不過,從(cóng)數據層獲取的應用(yòng)數據的格式通常不同于您需要顯示的信息的格式。例如,您可能(néng)隻需要在界面中顯示部分數據,或者可能(néng)需要合并兩個不同的數據源,以便提供切合用(yòng)戶需求的信息。無論您應用(yòng)的是什(shén)麽邏輯,都需要向界面傳遞完全呈現(xiàn)界面所需的所有信息。界面層是一個流水(shuǐ)線,負責将應用(yòng)數據變化轉換爲界面可以呈現(xiàn)的形式,然後将其顯示出來(lái)。

基本案例研究

讓我們以一個可獲取新聞報(bào)道(dào)供用(yòng)戶閱讀的應用(yòng)爲例。該應用(yòng)有一個報(bào)道(dào)屏幕,用(yòng)于顯示可供閱讀的報(bào)道(dào);另外(wài),該應用(yòng)允許已登錄的用(yòng)戶爲真正出衆的報(bào)道(dào)添加書簽。考慮到(dào)随時(shí)都可能(néng)有大(dà)量的報(bào)道(dào),讀者應能(néng)夠按類别浏覽報(bào)道(dào)。總的來(lái)說,該應用(yòng)可讓用(yòng)戶執行以下(xià)操作(zuò):

查看(kàn)可供閱讀的報(bào)道(dào)。

按類别浏覽報(bào)道(dào)。

登錄帳号并爲特定報(bào)道(dào)添加書簽。

使用(yòng)部分收費功能(néng)(如果符合相應條件)。

以下(xià)幾個部分使用(yòng)此示例作(zuò)爲案例研究,以便介紹單向數據流的原則,并展示在界面層的應用(yòng)架構上(shàng)下(xià)文(wén)中,這(zhè)些(xiē)原則有助于解決的問題。

界面層架構

“界面”這(zhè)一術語是指用(yòng)于顯示數據的 activity 和(hé) fragment 等界面元素,無論它們使用(yòng)哪個 API(Views 還是 Jetpack Compose)來(lái)顯示數據。由于數據層的作(zuò)用(yòng)是存儲和(hé)管理(lǐ)應用(yòng)數據,以及提供對(duì)應用(yòng)數據的訪問權限,因此界面層必須執行以下(xià)步驟:

使用(yòng)應用(yòng)數據,并将其轉換爲界面可以輕松呈現(xiàn)的數據。

使用(yòng)界面可呈現(xiàn)的數據,并将其轉換爲用(yòng)于向用(yòng)戶呈現(xiàn)的界面元素。

使用(yòng)來(lái)自(zì)這(zhè)些(xiē)組合在一起的界面元素的用(yòng)戶輸入事(shì)件,并根據需要反映它們對(duì)界面數據的影響。

根據需要重複第 1-3 步。

本指南的其餘部分展示了(le)如何實現(xiàn)用(yòng)于執行這(zhè)些(xiē)步驟的界面層。具體來(lái)說,本指南涵蓋以下(xià)任務和(hé)概念:

如何定義界面狀态。

單向數據流 (UDF),作(zuò)爲提供和(hé)管理(lǐ)界面狀态的方式。

如何根據 UDF 原則使用(yòng)可觀察數據類型公開(kāi)界面狀态。

如何實現(xiàn)使用(yòng)可觀察界面狀态的界面。

其中最基本的便是定義界面狀态。

定義界面狀态

請(qǐng)參閱上(shàng)文(wén)所述的案例研究。簡言之,界面會(huì)顯示一個報(bào)道(dào)列表,以及每篇報(bào)道(dào)的部分元數據。該應用(yòng)向用(yòng)戶顯示的這(zhè)些(xiē)信息便是界面狀态。

換言之,如果界面是相對(duì)用(yòng)戶而言的,那麽界面狀态就是相對(duì)應用(yòng)而言的。這(zhè)就像同一枚硬币的兩面,界面是界面狀态的直觀呈現(xiàn)。對(duì)界面狀态所做的任何更改都會(huì)立即反映在界面中。

不可變性

以上(shàng)示例中的界面狀态定義是不可變的。這(zhè)樣的主要好(hǎo)處是,不可變對(duì)象可保證即時(shí)提供應用(yòng)的狀态。這(zhè)樣一來(lái),界面便可專注于發揮單一作(zuò)用(yòng):讀取狀态并相應地更新其界面元素。因此,切勿直接在界面中修改界面狀态,除非界面本身是其數據的唯一來(lái)源。違反這(zhè)個原則會(huì)導緻同一條信息有多個可信來(lái)源,從(cóng)而導緻數據不一緻和(hé)輕微的 bug。

例如,如果案例研究中來(lái)自(zì)界面狀态的 NewsItemUiState 對(duì)象中的 bookmarked 标記在 Activity 類中已更新,那麽該标記會(huì)與數據層展開(kāi)競争,以争取成爲報(bào)道(dào)的“已添加書簽”狀态的來(lái)源。不可變數據類對(duì)于防止此類反模式非常有用(yòng)。

要點:隻有數據源或數據所有者才應負責更新其公開(kāi)的數據。

本指南中的命名慣例

在本指南中,界面狀态類是根據其描述的屏幕或部分屏幕的功能(néng)命名的。具體命名慣例如下(xià):

功能(néng) + UiState。

例如,用(yòng)于顯示新聞的屏幕的狀态可以稱爲 NewsUiState,新聞報(bào)道(dào)列表中的新聞報(bào)道(dào)的狀态可以爲 NewsItemUiState。

使用(yòng)單向數據流管理(lǐ)狀态

上(shàng)一部分中指出,界面狀态是呈現(xiàn)界面所需的詳細信息的不可變快(kuài)照。不過,應用(yòng)中數據的動态特性意味着狀态可能(néng)會(huì)随時(shí)間而變化。這(zhè)可能(néng)是因爲用(yòng)戶互動,也(yě)可能(néng)是因爲其他(tā)事(shì)件修改了(le)用(yòng)于填充應用(yòng)的底層數據。

這(zhè)些(xiē)互動可以受益于處理(lǐ)它們的 mediator,從(cóng)而定義要爲每個事(shì)件應用(yòng)的邏輯,并對(duì)後備數據源執行必要的轉換,以便創建界面狀态。這(zhè)些(xiē)互動及其邏輯可以位于界面本身中,但(dàn)随着界面開(kāi)始擔任其名稱所表明(míng)的角色以外(wài)的角色(數據所有者、提供方、轉換器等),這(zhè)可能(néng)很(hěn)快(kuài)就會(huì)變得難以掌控。此外(wài),這(zhè)可能(néng)會(huì)影響可測試性,因爲生成的代碼是緊密耦合的代碼,沒有可辨别的邊界。歸根結底,界面能(néng)夠受益于減輕的負擔。除非界面狀态非常簡單,否則界面的唯一職責應該是使用(yòng)和(hé)顯示界面狀态。

本部分介紹了(le)單向數據流 (UDF),這(zhè)是一種架構模式,有助于強制實施這(zhè)種健康的職責分離。

狀态容器

符合以下(xià)條件的類稱爲狀态容器:負責提供界面狀态,并且包含執行相應任務所必需的邏輯。狀态容器有多種大(dà)小(xiǎo),具體取決于所管理(lǐ)的界面元素的作(zuò)用(yòng)域(從(cóng)底部應用(yòng)欄等單個微件,到(dào)整個屏幕或導航目的地,不一而足)。

在後一種情況下(xià),典型的實現(xiàn)是 ViewModel 的實例,不過根據應用(yòng)的要求,使用(yòng)簡單的類可能(néng)就足夠了(le)。例如,案例研究中的“新聞”應用(yòng)使用(yòng) NewsViewModel 類作(zuò)爲狀态容器,以便爲該部分顯示的屏幕畫(huà)面提供界面狀态。

要點:ViewModel 類型是推薦的實現(xiàn),用(yòng)于管理(lǐ)屏幕級界面狀态,具有數據層訪問權限。此外(wài),它會(huì)在配置發生變化後自(zì)動繼續存在。ViewModel 類用(yòng)于定義要爲應用(yòng)中的事(shì)件應用(yòng)的邏輯,并提供更新後的狀态作(zuò)爲結果。

您可以通過多種方式爲界面與其狀态提供方之間的互相依賴關系建模。不過,由于界面與其 ViewModel 類之間的互動在很(hěn)大(dà)程度上(shàng)可以理(lǐ)解爲事(shì)件輸入及其随後的狀态輸出

狀态向下(xià)流動、事(shì)件向上(shàng)流動的這(zhè)種模式稱爲單向數據流 (UDF)。這(zhè)種模式對(duì)應用(yòng)架構的影響如下(xià):

ViewModel 會(huì)存儲并公開(kāi)界面要使用(yòng)的狀态。界面狀态是經過 ViewModel 轉換的應用(yòng)數據。

界面會(huì)向 ViewModel 發送用(yòng)戶事(shì)件通知(zhī)。

ViewModel 會(huì)處理(lǐ)用(yòng)戶操作(zuò)并更新狀态。

更新後的狀态将反饋給界面以進行呈現(xiàn)。

系統會(huì)對(duì)導緻狀态更改的所有事(shì)件重複上(shàng)述操作(zuò)。

對(duì)于導航目的地或屏幕,ViewModel 會(huì)使用(yòng)存儲庫或用(yòng)例類來(lái)獲取數據并将其轉換爲界面狀态,同時(shí)納入可能(néng)會(huì)導緻狀态更改的事(shì)件的影響。前面提到(dào)的案例研究包含一個報(bào)道(dào)列表,其中每篇報(bào)道(dào)都有标題、說明(míng)、來(lái)源、作(zuò)者名稱、發布日期,以及是否添加了(le)書簽

用(yòng)戶請(qǐng)求爲報(bào)道(dào)添加書簽就是一個可能(néng)會(huì)導緻狀态更改的事(shì)件示例。作(zuò)爲狀态提供方,ViewModel 的職責是定義所有必需的邏輯,以便填充界面狀态中的所有字段,并處理(lǐ)界面完全呈現(xiàn)所需的事(shì)件。

以下(xià)幾個部分更詳細地介紹了(le)導緻狀态變化的事(shì)件,以及如何使用(yòng) UDF 處理(lǐ)這(zhè)些(xiē)事(shì)件。

邏輯類型

爲報(bào)道(dào)添加書簽就是一個業務邏輯示例,因爲這(zhè)能(néng)夠爲應用(yòng)帶來(lái)價值。如需了(le)解詳情,請(qǐng)參閱數據層頁面。不過除此之外(wài),還有其他(tā)類型的重要邏輯需要定義:

業務邏輯決定着應用(yòng)數據的産品要求的實現(xiàn)。如前面所述,一個例子是在案例研究應用(yòng)中爲報(bào)道(dào)添加書簽。業務邏輯通常位于網域層或數據層中,但(dàn)絕不能(néng)位于界面層中。

界面行爲邏輯(即界面邏輯)決定着如何在屏幕上(shàng)顯示狀态變化。示例包括:使用(yòng) Android Resources 獲取要在屏幕上(shàng)顯示的正确文(wén)本、在用(yòng)戶點擊某個按鈕時(shí)前往特定屏幕,或使用(yòng)消息框或信息提示控件在屏幕上(shàng)向用(yòng)戶顯示消息。

界面邏輯(尤其是在涉及 Context 等界面類型時(shí))應位于界面中,而非 ViewModel 中。如果界面變得越來(lái)越複雜(zá),并且您希望将界面邏輯委托給另一個類,以便有利于進行測試和(hé)關注點分離,您可以創建一個簡單的類作(zuò)爲狀态容器。在界面中創建的簡單類可以采用(yòng) Android SDK 依賴項,因爲它們遵循界面的生命周期;ViewModel 對(duì)象具有更長的生命周期。

如需詳細了(le)解狀态容器以及如何利用(yòng)它們更好(hǎo)地構建界面,請(qǐng)參閱 Jetpack Compose 狀态指南。

爲何使用(yòng) UDF?

UDF 可爲狀态提供周期建模(如圖 4 所示)。它還可以将以下(xià)位置分離開(kāi)來(lái):狀态變化來(lái)源位置、轉換位置以及最終使用(yòng)位置。這(zhè)種分離可讓界面隻發揮其名稱所表明(míng)的作(zuò)用(yòng):通過觀察狀态變化來(lái)顯示信息,并通過将這(zhè)些(xiē)變化傳遞給 ViewModel 來(lái)傳遞用(yòng)戶 intent。

換句話(huà)說,UDF 有助于實現(xiàn)以下(xià)幾點:

數據一緻性。界面隻有一個可信來(lái)源。

可測試性。狀态來(lái)源是獨立的,因此可獨立于界面進行測試。

可維護性。狀态的更改遵循明(míng)确定義的模式,即狀态更改是用(yòng)戶事(shì)件及其數據拉取來(lái)源共同作(zuò)用(yòng)的結果。

公開(kāi)界面狀态

定義界面狀态并确定如何管理(lǐ)相應狀态的提供後,下(xià)一步是将提供的狀态發送給界面。由于您使用(yòng) UDF 管理(lǐ)狀态的提供,因此您可以将提供的狀态視(shì)爲數據流,換句話(huà)說,随着時(shí)間的推移,将提供狀态的多個版本。因此,您應在 LiveData 或 StateFlow 等可觀察數據容器中公開(kāi)界面狀态。這(zhè)樣做是爲了(le)使界面可以對(duì)狀态的任何變化做出反應,而無需直接從(cóng) ViewModel 手動拉取數據。這(zhè)些(xiē)類型還有一個好(hǎo)處是,始終緩存界面狀态的最新版本,這(zhè)對(duì)于在配置發生變化後快(kuài)速恢複狀态非常有用(yòng)。

如需關于将 LiveData 用(yòng)作(zuò)可觀察數據容器的介紹,請(qǐng)參閱此 Codelab。如需關于 Kotlin 數據流的類似介紹,請(qǐng)參閱 Android 上(shàng)的 Kotlin 數據流。

如果向界面公開(kāi)的數據相當簡單,通常值得将數據封裝在界面狀态類型中,因爲它能(néng)傳達狀态容器的發出與其關聯的屏幕或界面元素之間的關系。此外(wài),随着界面元素變得越來(lái)越複雜(zá),添加界面狀态的定義來(lái)容納呈現(xiàn)界面元素所需的額外(wài)信息始終會(huì)更加容易。

創建 UiState 流的一種常用(yòng)方法是,将後備可變數據流作(zuò)爲來(lái)自(zì) ViewModel 的不可變數據流進行公開(kāi),例如将 MutableStateFlow 作(zuò)爲 StateFlow 進行公開(kāi)。

這(zhè)樣一來(lái),ViewModel 便可以公開(kāi)在内部更改狀态的方法,以便發布供界面使用(yòng)的更新。以需要執行異步操作(zuò)的情況爲例,可以使用(yòng) viewModelScope 啓動協程,并且可以在操作(zuò)完成時(shí)更新可變狀态。

在上(shàng)面的示例中,NewsViewModel 類會(huì)嘗試獲取特定類别的報(bào)道(dào),然後在界面狀态中反映嘗試結果(成功或失敗),其中界面可以對(duì)其做出适當反應。如需詳細了(le)解錯誤處理(lǐ),請(qǐng)參閱在屏幕上(shàng)顯示錯誤部分。

注意:單向數據流有多種常用(yòng)的實現(xiàn),上(shàng)面示例中顯示的模式(通過 ViewModel 上(shàng)的函數更改狀态)便是其中的一種。

其他(tā)注意事(shì)項

除了(le)前面的指南之外(wài),公開(kāi)界面狀态時(shí)還要考慮以下(xià)事(shì)項:

界面狀态對(duì)象應處理(lǐ)彼此相關的狀态。 這(zhè)樣可以減少不一緻的情況,并讓代碼更易于理(lǐ)解。如果您在兩個不同的數據流中分别公開(kāi)新聞報(bào)道(dào)列表和(hé)書簽數量,可能(néng)會(huì)發現(xiàn)其中一個已更新,但(dàn)另一個沒有更新。當您使用(yòng)單個數據流時(shí),這(zhè)兩個元素都會(huì)保持最新狀态。此外(wài),某些(xiē)業務邏輯可能(néng)需要組合使用(yòng)數據源。例如,可能(néng)隻有在用(yòng)戶已登錄并且是付費新聞服務訂閱者時(shí),您才需要顯示書簽按鈕。

在此聲明(míng)中,書簽按鈕的可見性是兩個其他(tā)屬性的派生屬性。随着業務邏輯變得越來(lái)越複雜(zá),擁有單個 UiState 類,并且其中的所有屬性都是立即可用(yòng)的,變得越來(lái)越重要。

界面狀态:單個數據流還是多個數據流?是選擇在單個數據流中還是在多個數據流中公開(kāi)界面狀态,關鍵指導原則是前面提到(dào)的要點:發出的内容之間的關系。在單個數據流中進行公開(kāi)的最大(dà)優勢是便捷性和(hé)數據一緻性:狀态的使用(yòng)方随時(shí)都能(néng)立即獲取最新信息。不過,在有些(xiē)情況下(xià),可能(néng)适合使用(yòng)來(lái)自(zì) ViewModel 的單獨的狀态流:

不相關的數據類型:呈現(xiàn)界面所需的某些(xiē)狀态可能(néng)是完全相互獨立的。在此類情況下(xià),将這(zhè)些(xiē)不同的狀态捆綁在一起的代價可能(néng)會(huì)超過其優勢,尤其是當其中某個狀态的更新頻率高(gāo)于其他(tā)狀态的更新頻率時(shí)。

UiState diffing:UiState 對(duì)象中的字段越多,數據流就越有可能(néng)因爲其中一個字段被更新而發出。由于視(shì)圖沒有 diffing 機制來(lái)了(le)解連續發出的數據流是否相同,因此每次發出都會(huì)導緻視(shì)圖更新。這(zhè)意味着,可能(néng)必須要對(duì) LiveData 使用(yòng) Flow API 或 distinctUntilChanged() 等方法來(lái)緩解這(zhè)個問題。

使用(yòng)界面狀态

如需在界面中使用(yòng) UiState 對(duì)象流,您可以對(duì)所使用(yòng)的可觀察數據類型使用(yòng)終端運算(suàn)符。例如,對(duì)于 LiveData,您可以使用(yòng) observe() 方法;對(duì)于 Kotlin 數據流,您可以使用(yòng) collect() 方法或其變體。

在界面中使用(yòng)可觀察數據容器時(shí),請(qǐng)務必考慮界面的生命周期。這(zhè)非常重要,因爲當未向用(yòng)戶顯示視(shì)圖時(shí),界面不應觀察界面狀态。如需詳細了(le)解此主題,請(qǐng)參閱這(zhè)篇博文(wén)。使用(yòng) LiveData 時(shí),LifecycleOwner 會(huì)隐式處理(lǐ)生命周期問題。使用(yòng)數據流時(shí),最好(hǎo)通過适當的協程作(zuò)用(yòng)域和(hé) repeatOnLifecycle API 來(lái)處理(lǐ)這(zhè)一任務

線程處理(lǐ)和(hé)并發

在 ViewModel 中執行的所有工(gōng)作(zuò)都應具有主線程安全性(即從(cóng)主線程調用(yòng)是安全的)。這(zhè)是因爲數據層和(hé)網域層負責将工(gōng)作(zuò)移至其他(tā)線程。

如果 ViewModel 執行長時(shí)間運行的操作(zuò),則還要負責将相應邏輯移至後台線程。Kotlin 協程是管理(lǐ)并發操作(zuò)的絕佳方式,Jetpack 架構組件則爲其提供内置支持。如需詳細了(le)解如何在 Android 應用(yòng)中使用(yòng)協程,請(qǐng)參閱 Android 上(shàng)的 Kotlin 協程。

導航

應用(yòng)導航的變化通常是由類似于事(shì)件的發出操作(zuò)驅動的。例如,在 SignInViewModel 類執行登錄後,UiState 可能(néng)會(huì)有一個 isSignedIn 字段被設爲 true。此類觸發器的使用(yòng)方式應與上(shàng)面使用(yòng)界面狀态部分介紹的方式相同,不過使用(yòng)實現(xiàn)應遵從(cóng)導航組件。

Paging

Paging 庫通過一個稱爲 PagingData 的類型在界面中使用(yòng)。由于 PagingData 表示并包含可以随時(shí)間變化的内容(換句話(huà)說,它不是不可變類型),因此它不應以不可變界面狀态表示。相反,您應在單獨的流中獨立地從(cóng) ViewModel 中公開(kāi)它。如需具體示例,請(qǐng)參閱 Android Paging Codelab。

動畫(huà)

爲了(le)提供流暢的頂級導航過渡,您可能(néng)需要等待第二個屏幕加載數據,然後再啓動動畫(huà)。Android 視(shì)圖框架提供了(le)一些(xiē)鈎子,以便通過 postponeEnterTransition() 和(hé) startPostponedEnterTransition() API 延遲 fragment 目的地之間的過渡。這(zhè)些(xiē) API 提供了(le)一種方法來(lái)确保做到(dào)以下(xià)一點:在界面通過動畫(huà)過渡到(dào)第二個屏幕之前,第二個屏幕上(shàng)的界面元素(通常是從(cóng)網絡獲取的圖片)已做好(hǎo)顯示準備。如需了(le)解更多詳情和(hé)實現(xiàn)細節,請(qǐng)參閱 Android Motion 示例

網站(zhàn)建設開(kāi)發|APP設計(jì)開(kāi)發|小(xiǎo)程序建設開(kāi)發
下(xià)一篇:Android界面事(shì)件
上(shàng)一篇:Android應用(yòng)架構指南