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

Android數據層

Android開(kāi)發手冊

界面層包含與界面相關的狀态和(hé)界面邏輯,數據層則包含應用(yòng)數據和(hé)業務邏輯。業務邏輯決定應用(yòng)的價值,它由現(xiàn)實世界的業務規則組成,這(zhè)些(xiē)規則決定着應用(yòng)數據的創建、存儲和(hé)更改方式。

這(zhè)種關注點分離使得數據層可用(yòng)于多個屏幕、在應用(yòng)的不同部分之間共享信息,以及在界面以外(wài)複制業務邏輯以進行單元測試。如需詳細了(le)解數據層的優勢,請(qǐng)參閱“架構概覽”頁面。

注意:本頁中提供的建議(yì)和(hé)最佳實踐可應用(yòng)于各種應用(yòng)。遵循這(zhè)些(xiē)建議(yì)和(hé)最佳實踐可以提升應用(yòng)的可擴展性、質量和(hé)穩健性,并可使應用(yòng)更易于測試。不過,您應該将這(zhè)些(xiē)提示視(shì)爲指南,并視(shì)需要進行調整來(lái)滿足您的要求。

數據層架構

數據層由多個倉庫組成,其中每個倉庫都可以包含零到(dào)多個數據源。您應該爲應用(yòng)中處理(lǐ)的每種不同類型的數據分别創建一個存儲庫類。例如,您可以爲與電影相關的數據創建一個 MoviesRepository 類,或者爲與付款相關的數據創建一個 PaymentsRepository 類。

存儲庫類負責以下(xià)任務:

向應用(yòng)的其餘部分公開(kāi)數據。

集中處理(lǐ)數據變化。

解決多個數據源之間的沖突。

對(duì)應用(yòng)其餘部分的數據源進行抽象化處理(lǐ)。

包含業務邏輯。

每個數據源類應僅負責處理(lǐ)一個數據源,數據源可以是文(wén)件、網絡來(lái)源或本地數據庫。數據源類是應用(yòng)與數據操作(zuò)系統之間的橋梁。

層次結構中的其他(tā)層絕不能(néng)直接訪問數據源;數據層的入口點始終是存儲庫類。狀态容器類(請(qǐng)參閱界面層指南)或用(yòng)例類(請(qǐng)參閱網域層指南)絕不能(néng)将數據源作(zuò)爲直接依賴項。如果使用(yòng)倉庫類作(zuò)爲入口點,架構的不同層便可以獨立擴縮。

該層公開(kāi)的數據應該是不可變的,這(zhè)樣就可以避免數據被其他(tā)類篡改,從(cóng)而避免數值不一緻的風(fēng)險。不可變數據也(yě)可以由多個線程安全地處理(lǐ)。如需了(le)解詳情,請(qǐng)參閱線程處理(lǐ)部分。

按照依賴項注入方面的最佳實踐,存儲庫應在其構造函數中将數據源作(zuò)爲依賴項:

class ExampleRepository(

private val exampleRemoteDataSource: ExampleRemoteDataSource, // network

private val exampleLocalDataSource: ExampleLocalDataSource // database

) { /* ... */ }

注意:通常,如果存儲庫隻包含單個數據源并且不依賴于其他(tā)存儲庫,開(kāi)發者會(huì)将存儲庫和(hé)數據源的職責合并到(dào)存儲庫類中。這(zhè)種情況下(xià),在應用(yòng)的更高(gāo)版本中,如果倉庫需要處理(lǐ)來(lái)自(zì)其他(tā)來(lái)源的數據,請(qǐng)不要忘記拆分這(zhè)些(xiē)功能(néng)。

公開(kāi) API

數據層中的類通常會(huì)公開(kāi)函數,以執行一次性的創建、讀取、更新和(hé)删除 (CRUD) 調用(yòng),或接收關于數據随時(shí)間變化的通知(zhī)。對(duì)于每種情況,數據層都應公開(kāi)以下(xià)内容:

一次性操作(zuò):在 Kotlin 中,數據層應公開(kāi)挂起函數;對(duì)于 Java 編程語言,數據層應公開(kāi)用(yòng)于提供回調來(lái)通知(zhī)操作(zuò)結果的函數,或公開(kāi) RxJava Single、Maybe 或 Completable 類型。

接收關于數據随時(shí)間變化的通知(zhī):在 Kotlin 中,數據層應公開(kāi)數據流;對(duì)于 Java 編程語言,數據層應公開(kāi)用(yòng)于發出新數據的回調,或公開(kāi) RxJava Observable 或 Flowable 類型。

class ExampleRepository(

private val exampleRemoteDataSource: ExampleRemoteDataSource, // network

private val exampleLocalDataSource: ExampleLocalDataSource // database

) {

val data: Flow = ...

suspend fun modifyData(example: Example) { ... }

}

本指南中的命名慣例

在本指南中,存儲庫類以其負責的數據命名。具體命名慣例如下(xià):

數據類型 + Repository。

例如:NewsRepository、MoviesRepository 或 PaymentsRepository。

數據源類以其負責的數據以及使用(yòng)的來(lái)源命名。具體命名慣例如下(xià):

數據類型 + 來(lái)源類型 + DataSource。

對(duì)于數據的類型,可以使用(yòng) Remote 或 Local,以使其更加通用(yòng),因爲實現(xiàn)是可以變化的。例如:NewsRemoteDataSource 或 NewsLocalDataSource。在來(lái)源非常重要的情況下(xià),爲了(le)更加具體,可以使用(yòng)來(lái)源的類型。例如:NewsNetworkDataSource 或 NewsDiskDataSource。

請(qǐng)勿根據實現(xiàn)細節來(lái)爲數據源命名(例如 UserSharedPreferencesDataSource),因爲使用(yòng)相應數據源的存儲庫應該不知(zhī)道(dào)數據是如何保存的。如果您遵循此規則,便可以更改數據源的實現(xiàn)(例如,從(cóng) SharedPreferences 遷移到(dào) DataStore),而不會(huì)影響調用(yòng)相應數據源的層。

注意:遷移到(dào)數據源的新實現(xiàn)時(shí),您可以爲數據源創建接口,并使用(yòng)兩種數據源實現(xiàn):一種用(yòng)于舊的後備技術,另一種用(yòng)于新的技術。在這(zhè)種情況下(xià),您可以将技術名稱用(yòng)作(zuò)數據源類名稱(盡管它是一個實現(xiàn)細節),因爲存儲庫隻能(néng)看(kàn)到(dào)接口,而看(kàn)不到(dào)數據源類本身。完成遷移後,您可以重命名新類,使其名稱中不包含實現(xiàn)細節。

多層存儲庫

在某些(xiē)涉及更複雜(zá)業務要求的情況下(xià),存儲庫可能(néng)需要依賴于其他(tā)存儲庫。這(zhè)可能(néng)是因爲所涉及的數據是來(lái)自(zì)多個數據源的數據聚合,或者是因爲相應職責需要封裝在其他(tā)存儲庫類中。

例如,負責處理(lǐ)用(yòng)戶身份驗證數據的存儲庫 UserRepository 可以依賴于其他(tā)存儲庫(例如 LoginRepository 和(hé) RegistrationRepository),以滿足其要求。

注意:傳統上(shàng),一些(xiē)開(kāi)發者将依賴于其他(tā)存儲庫類的存儲庫類稱爲 manager,例如稱爲 UserManager 而非 UserRepository。如果您願意,可以使用(yòng)此命名慣例。

可信來(lái)源

每個存儲庫都隻定義單個可信來(lái)源,這(zhè)一點非常重要。可信來(lái)源始終包含一緻、正确且最新的數據。實際上(shàng),從(cóng)存儲庫公開(kāi)的數據應始終是直接來(lái)自(zì)可信來(lái)源的數據。

可信來(lái)源可以是數據源(例如數據庫),甚至可以是存儲庫可能(néng)包含的内存中緩存。存儲庫可合并不同的數據源,并解決數據源之間的所有潛在沖突,以便定期更新或因應用(yòng)戶輸入事(shì)件更新單個可信來(lái)源。

應用(yòng)中的不同存儲庫可以具有不同的可信來(lái)源。例如,LoginRepository 類可以将其緩存用(yòng)作(zuò)可信來(lái)源,PaymentsRepository 類則可以使用(yòng)網絡數據源。

爲了(le)提供離線優先支持,建議(yì)使用(yòng)本地數據源(例如數據庫)作(zuò)爲可信來(lái)源。

線程處理(lǐ)

調用(yòng)數據源和(hé)存儲庫應該具有主線程安全性(即從(cóng)主線程調用(yòng)是安全的)。在執行長時(shí)間運行的阻塞操作(zuò)時(shí),這(zhè)些(xiē)類負責将其邏輯的執行移至适當的線程。例如,對(duì)于數據源,從(cóng)文(wén)件讀取數據應該具有主線程安全性;對(duì)于倉庫,對(duì)大(dà)列表執行非常耗費資源的過濾應該具有主線程安全性。

請(qǐng)注意,大(dà)部分數據源都已提供具有主線程安全性的 API,例如 Room、Retrofit 或 Ktor 提供的挂起方法調用(yòng)。在這(zhè)些(xiē) API 可用(yòng)時(shí),您的倉庫可以充分利用(yòng)它們。

如需詳細了(le)解線程處理(lǐ),請(qǐng)參閱後台處理(lǐ)指南。對(duì)于 Kotlin 用(yòng)戶,建議(yì)使用(yòng)協程。如需了(le)解針對(duì) Java 編程語言的推薦選項,請(qǐng)參閱在後台線程中運行 Android 任務。

生命周期

數據層中的類的實例會(huì)保留在内存中,前提是它們可以從(cóng)垃圾回收根訪問 - 通常是從(cóng)應用(yòng)中的其他(tā)對(duì)象引用(yòng)。

如果某個類包含内存中的數據(例如緩存),您可能(néng)希望在特定時(shí)間段内重複使用(yòng)該類的同一實例。這(zhè)也(yě)稱爲類實例的生命周期。

如果該類的職責對(duì)于整個應用(yòng)至關重要,您可以将該類的實例的作(zuò)用(yòng)域限定爲 Application 類。這(zhè)可讓該實例遵循應用(yòng)的生命周期。或者,如果您隻需要在應用(yòng)内的特定流程(例如注冊流程或登錄流程)中重複使用(yòng)同一實例,則應将該實例的作(zuò)用(yòng)域限定爲負責相應流程的生命周期的類。例如,您可以将包含内存中數據的 RegistrationRepository 的作(zuò)用(yòng)域限定爲 RegistrationActivity,或限定爲注冊流程的導航圖。

每個實例的生命周期都是決定如何在應用(yòng)内提供依賴項的關鍵因素。建議(yì)您遵循依賴項注入方面的最佳實踐來(lái)管理(lǐ)依賴項,并可以将依賴項的作(zuò)用(yòng)域限定爲依賴項容器。如需詳細了(le)解 Android 中的作(zuò)用(yòng)域限定,請(qǐng)參閱 Android 和(hé) Hilt 中的作(zuò)用(yòng)域限定博文(wén)。

表示業務模式

您想要從(cóng)數據層公開(kāi)的數據模型可能(néng)是您從(cóng)不同數據源獲取的信息的子集。理(lǐ)想情況下(xià),不同數據源(網絡數據源和(hé)本地數據源)應該隻返回應用(yòng)需要的信息;但(dàn)通常并非如此。

例如,假設有一個 News API 服務器,它不僅返回報(bào)道(dào)信息,還會(huì)返回修改記錄、用(yòng)戶評論和(hé)部分元數據:

data class ArticleApiModel(

val id: Long,

val title: String,

val content: String,

val publicationDate: Date,

val modifications: Array,

val comments: Array,

val lastModificationDate: Date,

val authorId: Long,

val authorName: String,

val authorDateOfBirth: Date,

val readTimeMin: Int

)

該應用(yòng)不需要這(zhè)麽多關于報(bào)道(dào)的信息,因爲它在屏幕上(shàng)隻顯示報(bào)道(dào)内容,以及關于作(zuò)者的基本信息。一種很(hěn)好(hǎo)的做法是,分離模型類,并讓存儲庫僅公開(kāi)層次結構的其他(tā)層所需的數據。例如,以下(xià)代碼段展示了(le)您可以如何從(cóng)網絡中删減 ArticleApiModel,以便将 Article 模型類公開(kāi)給網域層和(hé)界面層:

data class Article(

val id: Long,

val title: String,

val content: String,

val publicationDate: Date,

val authorName: String,

val readTimeMin: Int

)

分離模型類可以帶來(lái)以下(xià)好(hǎo)處:

将數據減少到(dào)隻包含需要的内容,從(cóng)而節省應用(yòng)内存。

根據應用(yòng)所使用(yòng)的數據類型來(lái)調整外(wài)部數據類型 - 例如,應用(yòng)可以使用(yòng)不同的數據類型來(lái)表示日期。

更好(hǎo)地分離關注點 - 例如,如果預先定義了(le)模型類,大(dà)型團隊的成員便可以在功能(néng)的網絡層和(hé)界面層單獨開(kāi)展工(gōng)作(zuò)。

您可以擴展這(zhè)種做法,并可以在應用(yòng)架構的其他(tā)部分(例如,在數據源類和(hé) ViewModel 中)定義單獨的模型類。不過,這(zhè)需要您定義額外(wài)的類和(hé)邏輯,并且您應正确記錄和(hé)測試這(zhè)些(xiē)類和(hé)邏輯。至少,我們建議(yì)您在數據源接收的數據與應用(yòng)其餘部分所需的數據不符時(shí),創建新模型。

數據操作(zuò)類型

數據層可以處理(lǐ)的操作(zuò)類型會(huì)因操作(zuò)的重要程度而異:面向界面的操作(zuò)、面向應用(yòng)的操作(zuò)和(hé)面向業務的操作(zuò)。

面向界面的操作(zuò)

面向界面的操作(zuò)僅在用(yòng)戶位于特定屏幕上(shàng)時(shí)才相關,當用(yòng)戶離開(kāi)相應屏幕時(shí)便會(huì)被取消。例如,顯示從(cóng)數據庫獲取的部分數據。

面向界面的操作(zuò)通常由界面層觸發,并且遵循調用(yòng)方的生命周期,例如 ViewModel 的生命周期。如需查看(kàn)面向界面的操作(zuò)的示例,請(qǐng)參閱發出網絡請(qǐng)求部分。

面向應用(yòng)的操作(zuò)

隻要應用(yòng)處于打開(kāi)狀态,面向應用(yòng)的操作(zuò)就一直相關。如果應用(yòng)關閉或進程終止,這(zhè)些(xiē)操作(zuò)将會(huì)被取消。例如,緩存網絡請(qǐng)求結果,以便在以後需要時(shí)使用(yòng)。如需了(le)解詳情,請(qǐng)參閱實現(xiàn)内存中數據緩存部分。

這(zhè)些(xiē)操作(zuò)通常遵循 Application 類或數據層的生命周期。如需查看(kàn)示例,請(qǐng)參閱讓操作(zuò)擁有比屏幕更長的生命周期部分。

面向業務的操作(zuò)

面向業務的操作(zuò)無法取消。它們應該會(huì)在進程終止後繼續執行。例如,完成上(shàng)傳用(yòng)戶想要發布到(dào)其個人資料的照片。

對(duì)于面向業務的操作(zuò),建議(yì)使用(yòng) WorkManager。如需了(le)解詳情,請(qǐng)參閱使用(yòng) WorkManager 調度任務部分。

公開(kāi)錯誤

與存儲庫和(hé)數據源的互動可能(néng)會(huì)成功,也(yě)可能(néng)會(huì)在出現(xiàn)故障時(shí)抛出異常。對(duì)于協程和(hé)數據流,您應使用(yòng) Kotlin 的内置錯誤處理(lǐ)機制。對(duì)于可以由挂起函數觸發的錯誤,可以在适當時(shí)使用(yòng) try/catch 塊;在數據流中,可以使用(yòng) catch 運算(suàn)符。如果使用(yòng)這(zhè)種方式,界面層應負責處理(lǐ)在調用(yòng)數據層時(shí)出現(xiàn)的異常。

數據層可以理(lǐ)解和(hé)處理(lǐ)不同類型的錯誤,并可以使用(yòng)自(zì)定義異常(例如 UserNotAuthenticatedException)公開(kāi)這(zhè)些(xiē)錯誤。

注意:若要爲與數據層的互動結果建模,另一種方法是使用(yòng) Result 類。此模式會(huì)爲在處理(lǐ)結果時(shí)可能(néng)出現(xiàn)的錯誤和(hé)其他(tā)信号進行建模。在此模式中,數據層會(huì)返回 Result 類型,而非 T,以便讓界面知(zhī)道(dào)在特定情況下(xià)可能(néng)發生的已知(zhī)錯誤。對(duì)于沒有适當異常處理(lǐ)機制的反應式編程 API(例如 LiveData)來(lái)說,必須要使用(yòng)這(zhè)種方法。

如需詳細了(le)解協程中的錯誤,請(qǐng)參閱協程中的異常博文(wén)。

常見任務

以下(xià)幾個部分舉例說明(míng)了(le)如何使用(yòng)和(hé)構建數據層來(lái)執行 Android 應用(yòng)中常見的特定任務。這(zhè)些(xiē)示例基于本指南前面提到(dào)的典型“新聞”應用(yòng)。

發出網絡請(qǐng)求

發出網絡請(qǐng)求是 Android 應用(yòng)可能(néng)執行的最常見任務之一。“新聞”應用(yòng)需要向用(yòng)戶提供從(cóng)網絡獲取的最新新聞。因此,該應用(yòng)需要一個數據源類來(lái)管理(lǐ)網絡操作(zuò):NewsRemoteDataSource。爲了(le)向該應用(yòng)的其餘部分公開(kāi)信息,我們創建了(le)一個用(yòng)于處理(lǐ)新聞數據操作(zuò)的新存儲庫:NewsRepository。

該應用(yòng)需要滿足的要求是,當用(yòng)戶打開(kāi)屏幕時(shí),該應用(yòng)一律需要更新最新新聞。因此,這(zhè)是一項面向界面的操作(zuò)。

創建數據源

數據源需要公開(kāi)一個用(yòng)于返回最新新聞(ArticleHeadline 實例的列表)的函數。數據源需要提供一種具有主線程安全性的方式,以便從(cóng)網絡獲取最新新聞。爲此,它需要依賴于 CoroutineDispatcher 或 Executor 來(lái)運行任務。

發出網絡請(qǐng)求是由新的 fetchLatestNews() 方法處理(lǐ)的一次性調用(yòng):

class NewsRemoteDataSource(

private val newsApi: NewsApi,

private val ioDispatcher: CoroutineDispatcher

) {

/**

* Fetches the latest news from the network and returns the result.

* This executes on an IO-optimized thread pool, the function is main-safe.

*/

suspend fun fetchLatestNews(): List =

// Move the execution to an IO-optimized thread since the ApiService

// doesn't support coroutines and makes synchronous requests.

withContext(ioDispatcher) {

newsApi.fetchLatestNews()

}

}

// Makes news-related network synchronous requests.

interface NewsApi {

fun fetchLatestNews(): List

}

NewsApi 接口會(huì)隐藏網絡 API 客戶端的實現(xiàn);接口是由 Retrofit 還是由 HttpURLConnection 提供支持,并沒有區(qū)别。依賴于接口能(néng)夠使 API 實現(xiàn)在應用(yòng)中可交換。

要點:依賴于接口能(néng)夠使 API 實現(xiàn)在應用(yòng)中可交換。除了(le)提供可擴縮性并可讓您更輕松地替換依賴項之外(wài),這(zhè)還有利于進行測試,因爲您可以在測試時(shí)注入虛構的數據源實現(xiàn)。

創建存儲庫

存儲庫類中不需要任何額外(wài)的邏輯,即可執行此任務,因此 NewsRepository 可充當網絡數據源的代理(lǐ)。内存中緩存部分介紹了(le)添加這(zhè)一額外(wài)抽象層的好(hǎo)處。

// NewsRepository is consumed from other layers of the hierarchy.

class NewsRepository(

private val newsRemoteDataSource: NewsRemoteDataSource

) {

suspend fun fetchLatestNews(): List =

newsRemoteDataSource.fetchLatestNews()

}

如需了(le)解如何直接從(cóng)界面層使用(yòng)存儲庫類,請(qǐng)參閱界面層指南。

實現(xiàn)内存中數據緩存

假設爲“新聞”應用(yòng)引入了(le)一項新的要求:當用(yòng)戶打開(kāi)屏幕時(shí),如果用(yòng)戶之前已發出請(qǐng)求,那麽該應用(yòng)必須向用(yòng)戶顯示緩存的新聞。否則,該應用(yòng)應發出網絡請(qǐng)求以獲取最新新聞。

鑒于這(zhè)項新的要求,當用(yòng)戶已打開(kāi)該應用(yòng)時(shí),該應用(yòng)必須在内存中保留最新新聞。因此,這(zhè)是一項面向應用(yòng)的操作(zuò)。

緩存

通過添加内存中數據緩存,您可以在用(yòng)戶位于您的應用(yòng)中時(shí)保留數據。緩存旨在使一些(xiē)信息在内存中保存特定的時(shí)間長度,在此示例中,隻要用(yòng)戶位于該應用(yòng)中,就一直保存相應信息。緩存實現(xiàn)可以采用(yòng)不同的形式。從(cóng)簡單的可變變量,到(dào)更爲複雜(zá)、可以防止在多個線程上(shàng)進行讀/寫操作(zuò)的類,不一而足。可以在存儲庫中實現(xiàn)緩存,也(yě)可以在數據源類中實現(xiàn)緩存,具體取決于用(yòng)例。

緩存網絡請(qǐng)求結果

爲了(le)簡單起見,NewsRepository 使用(yòng)可變變量來(lái)緩存最新新聞。爲了(le)保護來(lái)自(zì)不同線程的讀取和(hé)寫入操作(zuò),我們使用(yòng)了(le) Mutex。如需詳細了(le)解共享的可變狀态和(hé)并發,請(qǐng)參閱 Kotlin 文(wén)檔。

以下(xià)實現(xiàn)會(huì)将最新新聞信息緩存到(dào)存儲庫中的一個變量,該變量由 Mutex 提供寫保護。如果網絡請(qǐng)求結果是成功,數據将分配給 latestNews 變量。

class NewsRepository(

private val newsRemoteDataSource: NewsRemoteDataSource

) {

// Mutex to make writes to cached values thread-safe.

private val latestNewsMutex = Mutex()

// Cache of the latest news got from the network.

private var latestNews: List = emptyList()

suspend fun getLatestNews(refresh: Boolean = false): List {

if (refresh || latestNews.isEmpty()) {

val networkResult = newsRemoteDataSource.fetchLatestNews()

// Thread-safe write to latestNews

latestNewsMutex.withLock {

this.latestNews = networkResult

}

}

return latestNewsMutex.withLock { this.latestNews }

}

}

讓操作(zuò)擁有比屏幕更長的生命周期

如果用(yòng)戶在網絡請(qǐng)求正在進行時(shí)離開(kāi)屏幕,系統将取消該請(qǐng)求,并且不會(huì)緩存結果。NewsRepository 不應使用(yòng)調用(yòng)方的 CoroutineScope 來(lái)執行此邏輯。NewsRepository 應使用(yòng)附加到(dào)其生命周期的 CoroutineScope。獲取最新新聞必須是面向應用(yòng)的操作(zuò)。

爲了(le)遵循依賴項注入方面的最佳實踐,NewsRepository 應在其構造函數中接收一個作(zuò)用(yòng)域作(zuò)爲參數,而不是創建自(zì)己的 CoroutineScope。由于存儲庫應在後台線程中執行大(dà)部分工(gōng)作(zuò),因此您應使用(yòng) Dispatchers.Default 或您自(zì)己的線程池來(lái)配置 CoroutineScope。

class NewsRepository(

...,

// This could be CoroutineScope(SupervisorJob() + Dispatchers.Default).

private val externalScope: CoroutineScope

) { ... }

由于 NewsRepository 已準備好(hǎo)使用(yòng)外(wài)部 CoroutineScope 來(lái)執行面向應用(yòng)的操作(zuò),因此它必須調用(yòng)數據源,并使用(yòng)由相應作(zuò)用(yòng)域啓動的新協程來(lái)保存其結果:

class NewsRepository(

private val newsRemoteDataSource: NewsRemoteDataSource,

private val externalScope: CoroutineScope

) {

/* ... */

suspend fun getLatestNews(refresh: Boolean = false): List {

return if (refresh) {

externalScope.async {

newsRemoteDataSource.fetchLatestNews().also { networkResult ->

// Thread-safe write to latestNews.

latestNewsMutex.withLock {

latestNews = networkResult

}

}

}.await()

} else {

return latestNewsMutex.withLock { this.latestNews }

}

}

}

async 用(yòng)于在外(wài)部作(zuò)用(yòng)域内啓動協程。await 在新的協程上(shàng)調用(yòng),以便在網絡請(qǐng)求返回結果并且結果保存到(dào)緩存中之前,一直保持挂起狀态。如果屆時(shí)用(yòng)戶仍位于屏幕上(shàng),就會(huì)看(kàn)到(dào)最新新聞;如果用(yòng)戶已離開(kāi)屏幕,await 将被取消,但(dàn) async 内部的邏輯将繼續執行。

如需詳細了(le)解 CoroutineScope 的模式,請(qǐng)參閱這(zhè)篇博文(wén)。

将數據保存到(dào)磁盤以及從(cóng)磁盤檢索數據

假設您要保存一些(xiē)數據,例如添加了(le)書簽的新聞和(hé)用(yòng)戶偏好(hǎo)設置。這(zhè)種類型的數據需要在進程終止後繼續保留,并且即使用(yòng)戶未連接到(dào)網絡,也(yě)必須可供用(yòng)戶訪問。

如果您處理(lǐ)的數據需要在進程終止後繼續保留,則您需要通過以下(xià)方式之一将其存儲在磁盤上(shàng):

對(duì)于需要查詢、需要實現(xiàn)引用(yòng)完整性或需要部分更新的大(dà)型數據集,請(qǐng)将數據保存在 Room 數據庫中。在“新聞”應用(yòng)示例中,新聞報(bào)道(dào)或作(zuò)者信息可以保存在該數據庫中。

對(duì)于隻需要檢索和(hé)設置(不需要查詢,也(yě)不需要部分更新)的小(xiǎo)型數據集,請(qǐng)使用(yòng) DataStore。在“新聞”應用(yòng)示例中,用(yòng)戶的首選日期格式或其他(tā)顯示偏好(hǎo)設置可以保存在 DataStore 中。

對(duì)于數據塊(例如 JSON 對(duì)象),可以使用(yòng)文(wén)件。

如可信來(lái)源部分所述,每個數據源都隻能(néng)處理(lǐ)一個來(lái)源,并且與特定的數據類型(例如 News、Authors、NewsAndAuthors 或 UserPreferences)相對(duì)應。使用(yòng)數據源的類應該不知(zhī)道(dào)數據是如何保存的,例如是保存在數據庫中,還是保存在文(wén)件中。

使用(yòng) Room 作(zuò)爲數據源

由于每個數據源都應隻負責處理(lǐ)一種特定類型的數據的一個數據源,因此 Room 數據源會(huì)接收數據訪問對(duì)象 (DAO) 或數據庫本身作(zuò)爲參數。例如,NewsLocalDataSource 可以接收 NewsDao 的實例作(zuò)爲參數,AuthorsLocalDataSource 則可以接收 AuthorsDao 的實例。

在某些(xiē)情況下(xià),如果不需要額外(wài)的邏輯,您可以直接将 DAO 注入存儲庫,因爲 DAO 是一種可以在測試中輕松替換的接口。

如需詳細了(le)解如何使用(yòng) Room API,請(qǐng)參閱 Room 指南。

使用(yòng) DataStore 作(zuò)爲數據源

DataStore 非常适合存儲鍵值對(duì),例如用(yòng)戶設置,具體示例可能(néng)包括時(shí)間格式、通知(zhī)偏好(hǎo)設置,以及是顯示還是隐藏用(yòng)戶已閱讀的新聞報(bào)道(dào)。DataStore 還可以使用(yòng)協議(yì)緩沖區(qū)來(lái)存儲類型化對(duì)象。

與任何其他(tā)對(duì)象一樣,由 DataStore 提供支持的數據源應包含與特定類型相對(duì)應或與應用(yòng)的特定部分相對(duì)應的數據。對(duì)于 DataStore 來(lái)說更是如此,因爲 DataStore 讀取操作(zuò)會(huì)作(zuò)爲一個每次值更新後都會(huì)發出的數據流進行公開(kāi)。因此,您應将相關偏好(hǎo)設置存儲在同一個 DataStore 中。

例如,您可以創建一個僅處理(lǐ)通知(zhī)相關偏好(hǎo)設置的 NotificationsDataStore,并創建一個僅處理(lǐ)新聞屏幕相關偏好(hǎo)設置的 NewsPreferencesDataStore。這(zhè)樣,您就可以更好(hǎo)地限定更新作(zuò)用(yòng)域,因爲隻有當與相應屏幕相關的偏好(hǎo)設置發生變化時(shí),newsScreenPreferencesDataStore.data 流才會(huì)發出。這(zhè)也(yě)意味着,該對(duì)象的生命周期可以更短,因爲它隻能(néng)在新聞屏幕顯示時(shí)存在。

如需詳細了(le)解如何使用(yòng) DataStore API,請(qǐng)參閱 DataStore 指南。

使用(yòng)文(wén)件作(zuò)爲數據源

處理(lǐ)大(dà)型對(duì)象(例如 JSON 對(duì)象或位圖)時(shí),您需要使用(yòng) File 對(duì)象并處理(lǐ)線程切換。

如需詳細了(le)解如何使用(yòng)文(wén)件存儲空(kōng)間,請(qǐng)參閱存儲空(kōng)間概覽頁面。

使用(yòng) WorkManager 調度任務

假設爲“新聞”應用(yòng)引入了(le)一項新的要求:隻要設備正在充電并且已連接到(dào)不按流量計(jì)費的網絡,該應用(yòng)就必須爲用(yòng)戶提供用(yòng)于選擇定期自(zì)動獲取最新新聞的選項。這(zhè)會(huì)使此操作(zuò)成爲一項面向業務的操作(zuò)。如果該應用(yòng)實現(xiàn)了(le)這(zhè)一要求,那麽在用(yòng)戶打開(kāi)該應用(yòng)時(shí),即使設備沒有連接到(dào)網絡,用(yòng)戶仍然可以看(kàn)到(dào)最近的新聞。

借助 WorkManager,可以輕松調度異步的可靠工(gōng)作(zuò),并可以負責管理(lǐ)約束條件。我們建議(yì)使用(yòng)該庫執行持久性工(gōng)作(zuò)。爲了(le)執行上(shàng)面定義的任務,我們創建了(le)一個 Worker 類:RefreshLatestNewsWorker。此類以 NewsRepository 作(zuò)爲依賴項,以便獲取最新新聞并将其緩存到(dào)磁盤中。

class RefreshLatestNewsWorker(

private val newsRepository: NewsRepository,

context: Context,

params: WorkerParameters

) : CoroutineWorker(context, params) {

override suspend fun doWork(): Result = try {

newsRepository.refreshLatestNews()

Result.success()

} catch (error: Throwable) {

Result.failure()

}

}

此類任務的業務邏輯應封裝在其自(zì)己的類中,并且應被視(shì)爲單獨的數據源。這(zhè)樣一來(lái),WorkManager 将僅負責确保工(gōng)作(zuò)會(huì)在所有約束條件都得到(dào)滿足時(shí)在後台線程中執行。通過遵循此模式,您可以根據需要在不同環境中快(kuài)速交換實現(xiàn)。

在此示例中,必須從(cóng) NewsRepository 調用(yòng)這(zhè)個與新聞相關的任務,前者會(huì)将一個新的數據源作(zuò)爲依賴項:NewsTasksDataSource。實現(xiàn)方式如下(xià):

private const val REFRESH_RATE_HOURS = 4L

private const val FETCH_LATEST_NEWS_TASK = "FetchLatestNewsTask"

private const val TAG_FETCH_LATEST_NEWS = "FetchLatestNewsTaskTag"

class NewsTasksDataSource(

private val workManager: WorkManager

) {

fun fetchNewsPeriodically() {

val fetchNewsRequest = PeriodicWorkRequestBuilder(

REFRESH_RATE_HOURS, TimeUnit.HOURS

).setConstraints(

Constraints.Builder()

.setRequiredNetworkType(NetworkType.TEMPORARILY_UNMETERED)

.setRequiresCharging(true)

.build()

)

.addTag(TAG_FETCH_LATEST_NEWS)

workManager.enqueueUniquePeriodicWork(

FETCH_LATEST_NEWS_TASK,

ExistingPeriodicWorkPolicy.KEEP,

fetchNewsRequest.build()

)

}

fun cancelFetchingNewsPeriodically() {

workManager.cancelAllWorkByTag(TAG_FETCH_LATEST_NEWS)

}

}

這(zhè)些(xiē)類型的類以其負責的數據命名,例如 NewsTasksDataSource 或 PaymentsTasksDataSource。與特定類型的數據相關的所有任務都應封裝在同一個類中。

如果任務需要在應用(yòng)啓動時(shí)觸發,建議(yì)使用(yòng)從(cóng) Initializer 調用(yòng)存儲庫的 App Startup 庫觸發 WorkManager 請(qǐng)求。

如需詳細了(le)解如何使用(yòng) WorkManager API,請(qǐng)參閱 WorkManager 指南。

測試

遵循依賴項注入方面的最佳實踐有助于您測試自(zì)己的應用(yòng)。對(duì)于與外(wài)部資源進行通信的類,依賴于接口也(yě)很(hěn)有幫助。測試某個單元時(shí),您可以注入其依賴項的虛構版本,以使測試具有确定性和(hé)可靠性。

單元測試

測試數據層時(shí),請(qǐng)遵循常規測試指南。對(duì)于單元測試,可以在需要時(shí)使用(yòng)真實對(duì)象,并虛構所有會(huì)聯系外(wài)部來(lái)源(例如從(cóng)文(wén)件讀取内容或從(cóng)網絡讀取内容)的依賴項。

集成測試

需要訪問外(wài)部來(lái)源的集成測試往往不太具有确定性,因爲它們需要在實際設備上(shàng)運行。建議(yì)您在受控環境中執行這(zhè)些(xiē)測試,以便使集成測試更加可靠。

對(duì)于數據庫,Room 允許創建一個您可以在測試時(shí)完全控制的内存中數據庫。如需了(le)解詳情,請(qǐng)參閱測試和(hé)調試數據庫頁面。

對(duì)于網絡,有一些(xiē)常用(yòng)的庫(例如 WireMock 或 MockWebServer)可用(yòng)于虛構 HTTP 和(hé) HTTPS 調用(yòng)并驗證請(qǐng)求是否已按預期發出。

網站(zhàn)建設開(kāi)發|APP設計(jì)開(kāi)發|小(xiǎo)程序建設開(kāi)發
下(xià)一篇:Android構建離線優先應用(yòng)
上(shàng)一篇:Android網域層