界面層包含與界面相關的狀态和(hé)界面邏輯,數(shù)據層則包含應用數(shù)據和(hé)業務邏輯。業務邏輯決定應用的價值,它由現實世界的業務規則組成,這(zhè)些規則決定着應用數(shù)據的創建、存儲和(hé)更改方式。
這(zhè)種關注點分離使得數(shù)據層可(kě)用于多個屏幕、在應用的不同部分之間(jiān)共享信息,以及在界面以外(wài)複制業務邏輯以進行(xíng)單元測試。如(rú)需詳細了(le)解數(shù)據層的優勢,請(qǐng)參閱“架構概覽”頁面。
注意:本頁中提供的建議(yì)和(hé)最佳實踐可(kě)應用于各種應用。遵循這(zhè)些建議(yì)和(hé)最佳實踐可(kě)以提升應用的可(kě)擴展性、質量和(hé)穩健性,并可(kě)使應用更易于測試。不過,您應該将這(zhè)些提示視(shì)為(wèi)指南,并視(shì)需要(yào)進行(xíng)調整來滿足您的要(yào)求。
數(shù)據層架構
數(shù)據層由多個倉庫組成,其中每個倉庫都(dōu)可(kě)以包含零到多個數(shù)據源。您應該為(wèi)應用中處理的每種不同類型的數(shù)據分别創建一(yī)個存儲庫類。例如(rú),您可(kě)以為(wèi)與電影相關的數(shù)據創建一(yī)個 MoviesRepository 類,或者為(wèi)與付款相關的數(shù)據創建一(yī)個 PaymentsRepository 類。
存儲庫類負責以下(xià)任務:
向應用的其餘部分公開(kāi)數(shù)據。
集中處理數(shù)據變化。
解決多個數(shù)據源之間(jiān)的沖突。
對應用其餘部分的數(shù)據源進行(xíng)抽象化處理。
包含業務邏輯。
每個數(shù)據源類應僅負責處理一(yī)個數(shù)據源,數(shù)據源可(kě)以是文件、網絡來源或本地(dì)數(shù)據庫。數(shù)據源類是應用與數(shù)據操作(zuò)系統之間(jiān)的橋梁。
層次結構中的其他(tā)層絕不能(néng)直接訪問(wèn)數(shù)據源;數(shù)據層的入口點始終是存儲庫類。狀态容器類(請(qǐng)參閱界面層指南)或用例類(請(qǐng)參閱網域層指南)絕不能(néng)将數(shù)據源作(zuò)為(wèi)直接依賴項。如(rú)果使用倉庫類作(zuò)為(wèi)入口點,架構的不同層便可(kě)以獨立擴縮。
該層公開(kāi)的數(shù)據應該是不可(kě)變的,這(zhè)樣就可(kě)以避免數(shù)據被其他(tā)類篡改,從(cóng)而避免數(shù)值不一(yī)緻的風(fēng)險。不可(kě)變數(shù)據也可(kě)以由多個線程安全地(dì)處理。如(rú)需了(le)解詳情,請(qǐng)參閱線程處理部分。
按照依賴項注入方面的最佳實踐,存儲庫應在其構造函數(shù)中将數(shù)據源作(zuò)為(wèi)依賴項:
class ExampleRepository(
private val exampleRemoteDataSource: ExampleRemoteDataSource, // network
private val exampleLocalDataSource: ExampleLocalDataSource // database
) { /* ... */ }
注意:通(tōng)常,如(rú)果存儲庫隻包含單個數(shù)據源并且不依賴于其他(tā)存儲庫,開(kāi)發者會将存儲庫和(hé)數(shù)據源的職責合并到存儲庫類中。這(zhè)種情況下(xià),在應用的更高版本中,如(rú)果倉庫需要(yào)處理來自(zì)其他(tā)來源的數(shù)據,請(qǐng)不要(yào)忘記拆分這(zhè)些功能(néng)。
公開(kāi) API
數(shù)據層中的類通(tōng)常會公開(kāi)函數(shù),以執行(xíng)一(yī)次性的創建、讀取、更新和(hé)删除 (CRUD) 調用,或接收關于數(shù)據随時(shí)間(jiān)變化的通(tōng)知。對于每種情況,數(shù)據層都(dōu)應公開(kāi)以下(xià)內(nèi)容:
一(yī)次性操作(zuò):在 Kotlin 中,數(shù)據層應公開(kāi)挂起函數(shù);對于 Java 編程語言,數(shù)據層應公開(kāi)用于提供回調來通(tōng)知操作(zuò)結果的函數(shù),或公開(kāi) RxJava Single、Maybe 或 Completable 類型。
接收關于數(shù)據随時(shí)間(jiān)變化的通(tōng)知:在 Kotlin 中,數(shù)據層應公開(kāi)數(shù)據流;對于 Java 編程語言,數(shù)據層應公開(kāi)用于發出新數(shù)據的回調,或公開(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) { ... }
}
本指南中的命名慣例
在本指南中,存儲庫類以其負責的數(shù)據命名。具體命名慣例如(rú)下(xià):
數(shù)據類型 + Repository。
例如(rú):NewsRepository、MoviesRepository 或 PaymentsRepository。
數(shù)據源類以其負責的數(shù)據以及使用的來源命名。具體命名慣例如(rú)下(xià):
數(shù)據類型 + 來源類型 + DataSource。
對于數(shù)據的類型,可(kě)以使用 Remote 或 Local,以使其更加通(tōng)用,因為(wèi)實現是可(kě)以變化的。例如(rú):NewsRemoteDataSource 或 NewsLocalDataSource。在來源非常重要(yào)的情況下(xià),為(wèi)了(le)更加具體,可(kě)以使用來源的類型。例如(rú):NewsNetworkDataSource 或 NewsDiskDataSource。
請(qǐng)勿根據實現細節來為(wèi)數(shù)據源命名(例如(rú) UserSharedPreferencesDataSource),因為(wèi)使用相應數(shù)據源的存儲庫應該不知道(dào)數(shù)據是如(rú)何保存的。如(rú)果您遵循此規則,便可(kě)以更改數(shù)據源的實現(例如(rú),從(cóng) SharedPreferences 遷移到 DataStore),而不會影響調用相應數(shù)據源的層。
注意:遷移到數(shù)據源的新實現時(shí),您可(kě)以為(wèi)數(shù)據源創建接口,并使用兩種數(shù)據源實現:一(yī)種用于舊(jiù)的後備技術(shù),另一(yī)種用于新的技術(shù)。在這(zhè)種情況下(xià),您可(kě)以将技術(shù)名稱用作(zuò)數(shù)據源類名稱(盡管它是一(yī)個實現細節),因為(wèi)存儲庫隻能(néng)看(kàn)到接口,而看(kàn)不到數(shù)據源類本身。完成遷移後,您可(kě)以重命名新類,使其名稱中不包含實現細節。
多層存儲庫
在某些涉及更複雜(zá)業務要(yào)求的情況下(xià),存儲庫可(kě)能(néng)需要(yào)依賴于其他(tā)存儲庫。這(zhè)可(kě)能(néng)是因為(wèi)所涉及的數(shù)據是來自(zì)多個數(shù)據源的數(shù)據聚合,或者是因為(wèi)相應職責需要(yào)封裝在其他(tā)存儲庫類中。
例如(rú),負責處理用戶身份驗證數(shù)據的存儲庫 UserRepository 可(kě)以依賴于其他(tā)存儲庫(例如(rú) LoginRepository 和(hé) RegistrationRepository),以滿足其要(yào)求。
注意:傳統上(shàng),一(yī)些開(kāi)發者将依賴于其他(tā)存儲庫類的存儲庫類稱為(wèi) manager,例如(rú)稱為(wèi) UserManager 而非 UserRepository。如(rú)果您願意,可(kě)以使用此命名慣例。
可(kě)信來源
每個存儲庫都(dōu)隻定義單個可(kě)信來源,這(zhè)一(yī)點非常重要(yào)。可(kě)信來源始終包含一(yī)緻、正确且最新的數(shù)據。實際上(shàng),從(cóng)存儲庫公開(kāi)的數(shù)據應始終是直接來自(zì)可(kě)信來源的數(shù)據。
可(kě)信來源可(kě)以是數(shù)據源(例如(rú)數(shù)據庫),甚至可(kě)以是存儲庫可(kě)能(néng)包含的內(nèi)存中緩存。存儲庫可(kě)合并不同的數(shù)據源,并解決數(shù)據源之間(jiān)的所有潛在沖突,以便定期更新或因應用戶輸入事件更新單個可(kě)信來源。
應用中的不同存儲庫可(kě)以具有不同的可(kě)信來源。例如(rú),LoginRepository 類可(kě)以将其緩存用作(zuò)可(kě)信來源,PaymentsRepository 類則可(kě)以使用網絡數(shù)據源。
為(wèi)了(le)提供離線優先支持,建議(yì)使用本地(dì)數(shù)據源(例如(rú)數(shù)據庫)作(zuò)為(wèi)可(kě)信來源。
線程處理
調用數(shù)據源和(hé)存儲庫應該具有主線程安全性(即從(cóng)主線程調用是安全的)。在執行(xíng)長(cháng)時(shí)間(jiān)運行(xíng)的阻塞操作(zuò)時(shí),這(zhè)些類負責将其邏輯的執行(xíng)移至适當的線程。例如(rú),對于數(shù)據源,從(cóng)文件讀取數(shù)據應該具有主線程安全性;對于倉庫,對大列表執行(xíng)非常耗費(fèi)資源的過濾應該具有主線程安全性。
請(qǐng)注意,大部分數(shù)據源都(dōu)已提供具有主線程安全性的 API,例如(rú) Room、Retrofit 或 Ktor 提供的挂起方法調用。在這(zhè)些 API 可(kě)用時(shí),您的倉庫可(kě)以充分利用它們。
如(rú)需詳細了(le)解線程處理,請(qǐng)參閱後台處理指南。對于 Kotlin 用戶,建議(yì)使用協程。如(rú)需了(le)解針對 Java 編程語言的推薦選項,請(qǐng)參閱在後台線程中運行(xíng) Android 任務。
生命周期
數(shù)據層中的類的實例會保留在內(nèi)存中,前提是它們可(kě)以從(cóng)垃圾回收根訪問(wèn) - 通(tōng)常是從(cóng)應用中的其他(tā)對象引用。
如(rú)果某個類包含內(nèi)存中的數(shù)據(例如(rú)緩存),您可(kě)能(néng)希望在特定時(shí)間(jiān)段內(nèi)重複使用該類的同一(yī)實例。這(zhè)也稱為(wèi)類實例的生命周期。
如(rú)果該類的職責對于整個應用至關重要(yào),您可(kě)以将該類的實例的作(zuò)用域限定為(wèi) Application 類。這(zhè)可(kě)讓該實例遵循應用的生命周期。或者,如(rú)果您隻需要(yào)在應用內(nèi)的特定流程(例如(rú)注冊流程或登錄流程)中重複使用同一(yī)實例,則應将該實例的作(zuò)用域限定為(wèi)負責相應流程的生命周期的類。例如(rú),您可(kě)以将包含內(nèi)存中數(shù)據的 RegistrationRepository 的作(zuò)用域限定為(wèi) RegistrationActivity,或限定為(wèi)注冊流程的導航圖。
每個實例的生命周期都(dōu)是決定如(rú)何在應用內(nèi)提供依賴項的關鍵因素。建議(yì)您遵循依賴項注入方面的最佳實踐來管理依賴項,并可(kě)以将依賴項的作(zuò)用域限定為(wèi)依賴項容器。如(rú)需詳細了(le)解 Android 中的作(zuò)用域限定,請(qǐng)參閱 Android 和(hé) Hilt 中的作(zuò)用域限定博文。
表示業務模式
您想要(yào)從(cóng)數(shù)據層公開(kāi)的數(shù)據模型可(kě)能(néng)是您從(cóng)不同數(shù)據源獲取的信息的子集。理想情況下(xià),不同數(shù)據源(網絡數(shù)據源和(hé)本地(dì)數(shù)據源)應該隻返回應用需要(yào)的信息;但(dàn)通(tōng)常并非如(rú)此。
例如(rú),假設有一(yī)個 News API 服務器,它不僅返回報道(dào)信息,還會返回修改記錄、用戶評論和(hé)部分元數(shù)據:
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ào)這(zhè)麽多關于報道(dào)的信息,因為(wèi)它在屏幕上(shàng)隻顯示報道(dào)內(nèi)容,以及關于作(zuò)者的基本信息。一(yī)種很(hěn)好的做(zuò)法是,分離模型類,并讓存儲庫僅公開(kāi)層次結構的其他(tā)層所需的數(shù)據。例如(rú),以下(xià)代碼段展示了(le)您可(kě)以如(rú)何從(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
)
分離模型類可(kě)以帶來以下(xià)好處:
将數(shù)據減少(shǎo)到隻包含需要(yào)的內(nèi)容,從(cóng)而節省應用內(nèi)存。
根據應用所使用的數(shù)據類型來調整外(wài)部數(shù)據類型 - 例如(rú),應用可(kě)以使用不同的數(shù)據類型來表示日期。
更好地(dì)分離關注點 - 例如(rú),如(rú)果預先定義了(le)模型類,大型團隊的成員(yuán)便可(kě)以在功能(néng)的網絡層和(hé)界面層單獨開(kāi)展工(gōng)作(zuò)。
您可(kě)以擴展這(zhè)種做(zuò)法,并可(kě)以在應用架構的其他(tā)部分(例如(rú),在數(shù)據源類和(hé) ViewModel 中)定義單獨的模型類。不過,這(zhè)需要(yào)您定義額外(wài)的類和(hé)邏輯,并且您應正确記錄和(hé)測試這(zhè)些類和(hé)邏輯。至少(shǎo),我們建議(yì)您在數(shù)據源接收的數(shù)據與應用其餘部分所需的數(shù)據不符時(shí),創建新模型。
數(shù)據操作(zuò)類型
數(shù)據層可(kě)以處理的操作(zuò)類型會因操作(zuò)的重要(yào)程度而異:面向界面的操作(zuò)、面向應用的操作(zuò)和(hé)面向業務的操作(zuò)。
面向界面的操作(zuò)
面向界面的操作(zuò)僅在用戶位于特定屏幕上(shàng)時(shí)才相關,當用戶離開(kāi)相應屏幕時(shí)便會被取消。例如(rú),顯示從(cóng)數(shù)據庫獲取的部分數(shù)據。
面向界面的操作(zuò)通(tōng)常由界面層觸發,并且遵循調用方的生命周期,例如(rú) ViewModel 的生命周期。如(rú)需查看(kàn)面向界面的操作(zuò)的示例,請(qǐng)參閱發出網絡請(qǐng)求部分。
面向應用的操作(zuò)
隻要(yào)應用處于打開(kāi)狀态,面向應用的操作(zuò)就一(yī)直相關。如(rú)果應用關閉或進程終止,這(zhè)些操作(zuò)将會被取消。例如(rú),緩存網絡請(qǐng)求結果,以便在以後需要(yào)時(shí)使用。如(rú)需了(le)解詳情,請(qǐng)參閱實現內(nèi)存中數(shù)據緩存部分。
這(zhè)些操作(zuò)通(tōng)常遵循 Application 類或數(shù)據層的生命周期。如(rú)需查看(kàn)示例,請(qǐng)參閱讓操作(zuò)擁有比屏幕更長(cháng)的生命周期部分。
面向業務的操作(zuò)
面向業務的操作(zuò)無法取消。它們應該會在進程終止後繼續執行(xíng)。例如(rú),完成上(shàng)傳用戶想要(yào)發布到其個人(rén)資料的照片。
對于面向業務的操作(zuò),建議(yì)使用 WorkManager。如(rú)需了(le)解詳情,請(qǐng)參閱使用 WorkManager 調度任務部分。
公開(kāi)錯誤
與存儲庫和(hé)數(shù)據源的互動可(kě)能(néng)會成功,也可(kě)能(néng)會在出現故障時(shí)抛出異常。對于協程和(hé)數(shù)據流,您應使用 Kotlin 的內(nèi)置錯誤處理機制。對于可(kě)以由挂起函數(shù)觸發的錯誤,可(kě)以在适當時(shí)使用 try/catch 塊;在數(shù)據流中,可(kě)以使用 catch 運算符。如(rú)果使用這(zhè)種方式,界面層應負責處理在調用數(shù)據層時(shí)出現的異常。
數(shù)據層可(kě)以理解和(hé)處理不同類型的錯誤,并可(kě)以使用自(zì)定義異常(例如(rú) UserNotAuthenticatedException)公開(kāi)這(zhè)些錯誤。
注意:若要(yào)為(wèi)與數(shù)據層的互動結果建模,另一(yī)種方法是使用 Result 類。此模式會為(wèi)在處理結果時(shí)可(kě)能(néng)出現的錯誤和(hé)其他(tā)信号進行(xíng)建模。在此模式中,數(shù)據層會返回 Result
如(rú)需詳細了(le)解協程中的錯誤,請(qǐng)參閱協程中的異常博文。
常見(jiàn)任務
以下(xià)幾個部分舉例說明(míng)了(le)如(rú)何使用和(hé)構建數(shù)據層來執行(xíng) Android 應用中常見(jiàn)的特定任務。這(zhè)些示例基于本指南前面提到的典型“新聞”應用。
發出網絡請(qǐng)求
發出網絡請(qǐng)求是 Android 應用可(kě)能(néng)執行(xíng)的最常見(jiàn)任務之一(yī)。“新聞”應用需要(yào)向用戶提供從(cóng)網絡獲取的最新新聞。因此,該應用需要(yào)一(yī)個數(shù)據源類來管理網絡操作(zuò):NewsRemoteDataSource。為(wèi)了(le)向該應用的其餘部分公開(kāi)信息,我們創建了(le)一(yī)個用于處理新聞數(shù)據操作(zuò)的新存儲庫:NewsRepository。
該應用需要(yào)滿足的要(yào)求是,當用戶打開(kāi)屏幕時(shí),該應用一(yī)律需要(yào)更新最新新聞。因此,這(zhè)是一(yī)項面向界面的操作(zuò)。
創建數(shù)據源
數(shù)據源需要(yào)公開(kāi)一(yī)個用于返回最新新聞(ArticleHeadline 實例的列表)的函數(shù)。數(shù)據源需要(yào)提供一(yī)種具有主線程安全性的方式,以便從(cóng)網絡獲取最新新聞。為(wèi)此,它需要(yào)依賴于 CoroutineDispatcher 或 Executor 來運行(xíng)任務。
發出網絡請(qǐng)求是由新的 fetchLatestNews() 方法處理的一(yī)次性調用:
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 接口會隐藏網絡 API 客戶端的實現;接口是由 Retrofit 還是由 HttpURLConnection 提供支持,并沒有區(qū)别。依賴于接口能(néng)夠使 API 實現在應用中可(kě)交換。
要(yào)點:依賴于接口能(néng)夠使 API 實現在應用中可(kě)交換。除了(le)提供可(kě)擴縮性并可(kě)讓您更輕松地(dì)替換依賴項之外(wài),這(zhè)還有利于進行(xíng)測試,因為(wèi)您可(kě)以在測試時(shí)注入虛構的數(shù)據源實現。
創建存儲庫
存儲庫類中不需要(yào)任何額外(wài)的邏輯,即可(kě)執行(xíng)此任務,因此 NewsRepository 可(kě)充當網絡數(shù)據源的代理。內(nèi)存中緩存部分介紹了(le)添加這(zhè)一(yī)額外(wài)抽象層的好處。
// NewsRepository is consumed from other layers of the hierarchy.
class NewsRepository(
private val newsRemoteDataSource: NewsRemoteDataSource
) {
suspend fun fetchLatestNews(): List
newsRemoteDataSource.fetchLatestNews()
}
如(rú)需了(le)解如(rú)何直接從(cóng)界面層使用存儲庫類,請(qǐng)參閱界面層指南。
實現內(nèi)存中數(shù)據緩存
假設為(wèi)“新聞”應用引入了(le)一(yī)項新的要(yào)求:當用戶打開(kāi)屏幕時(shí),如(rú)果用戶之前已發出請(qǐng)求,那(nà)麽該應用必須向用戶顯示緩存的新聞。否則,該應用應發出網絡請(qǐng)求以獲取最新新聞。
鑒于這(zhè)項新的要(yào)求,當用戶已打開(kāi)該應用時(shí),該應用必須在內(nèi)存中保留最新新聞。因此,這(zhè)是一(yī)項面向應用的操作(zuò)。
緩存
通(tōng)過添加內(nèi)存中數(shù)據緩存,您可(kě)以在用戶位于您的應用中時(shí)保留數(shù)據。緩存旨在使一(yī)些信息在內(nèi)存中保存特定的時(shí)間(jiān)長(cháng)度,在此示例中,隻要(yào)用戶位于該應用中,就一(yī)直保存相應信息。緩存實現可(kě)以采用不同的形式。從(cóng)簡單的可(kě)變變量,到更為(wèi)複雜(zá)、可(kě)以防止在多個線程上(shàng)進行(xíng)讀/寫操作(zuò)的類,不一(yī)而足。可(kě)以在存儲庫中實現緩存,也可(kě)以在數(shù)據源類中實現緩存,具體取決于用例。
緩存網絡請(qǐng)求結果
為(wèi)了(le)簡單起見(jiàn),NewsRepository 使用可(kě)變變量來緩存最新新聞。為(wèi)了(le)保護來自(zì)不同線程的讀取和(hé)寫入操作(zuò),我們使用了(le) Mutex。如(rú)需詳細了(le)解共享的可(kě)變狀态和(hé)并發,請(qǐng)參閱 Kotlin 文檔。
以下(xià)實現會将最新新聞信息緩存到存儲庫中的一(yī)個變量,該變量由 Mutex 提供寫保護。如(rú)果網絡請(qǐng)求結果是成功,數(shù)據将分配給 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
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ò)擁有比屏幕更長(cháng)的生命周期
如(rú)果用戶在網絡請(qǐng)求正在進行(xíng)時(shí)離開(kāi)屏幕,系統将取消該請(qǐng)求,并且不會緩存結果。NewsRepository 不應使用調用方的 CoroutineScope 來執行(xíng)此邏輯。NewsRepository 應使用附加到其生命周期的 CoroutineScope。獲取最新新聞必須是面向應用的操作(zuò)。
為(wèi)了(le)遵循依賴項注入方面的最佳實踐,NewsRepository 應在其構造函數(shù)中接收一(yī)個作(zuò)用域作(zuò)為(wèi)參數(shù),而不是創建自(zì)己的 CoroutineScope。由于存儲庫應在後台線程中執行(xíng)大部分工(gōng)作(zuò),因此您應使用 Dispatchers.Default 或您自(zì)己的線程池來配置 CoroutineScope。
class NewsRepository(
...,
// This could be CoroutineScope(SupervisorJob() + Dispatchers.Default).
private val externalScope: CoroutineScope
) { ... }
由于 NewsRepository 已準備好使用外(wài)部 CoroutineScope 來執行(xíng)面向應用的操作(zuò),因此它必須調用數(shù)據源,并使用由相應作(zuò)用域啓動的新協程來保存其結果:
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 用于在外(wài)部作(zuò)用域內(nèi)啓動協程。await 在新的協程上(shàng)調用,以便在網絡請(qǐng)求返回結果并且結果保存到緩存中之前,一(yī)直保持挂起狀态。如(rú)果屆時(shí)用戶仍位于屏幕上(shàng),就會看(kàn)到最新新聞;如(rú)果用戶已離開(kāi)屏幕,await 将被取消,但(dàn) async 內(nèi)部的邏輯将繼續執行(xíng)。
如(rú)需詳細了(le)解 CoroutineScope 的模式,請(qǐng)參閱這(zhè)篇博文。
将數(shù)據保存到磁盤以及從(cóng)磁盤檢索數(shù)據
假設您要(yào)保存一(yī)些數(shù)據,例如(rú)添加了(le)書(shū)簽的新聞和(hé)用戶偏好設置。這(zhè)種類型的數(shù)據需要(yào)在進程終止後繼續保留,并且即使用戶未連接到網絡,也必須可(kě)供用戶訪問(wèn)。
如(rú)果您處理的數(shù)據需要(yào)在進程終止後繼續保留,則您需要(yào)通(tōng)過以下(xià)方式之一(yī)将其存儲在磁盤上(shàng):
對于需要(yào)查詢、需要(yào)實現引用完整性或需要(yào)部分更新的大型數(shù)據集,請(qǐng)将數(shù)據保存在 Room 數(shù)據庫中。在“新聞”應用示例中,新聞報道(dào)或作(zuò)者信息可(kě)以保存在該數(shù)據庫中。
對于隻需要(yào)檢索和(hé)設置(不需要(yào)查詢,也不需要(yào)部分更新)的小型數(shù)據集,請(qǐng)使用 DataStore。在“新聞”應用示例中,用戶的首選日期格式或其他(tā)顯示偏好設置可(kě)以保存在 DataStore 中。
對于數(shù)據塊(例如(rú) JSON 對象),可(kě)以使用文件。
如(rú)可(kě)信來源部分所述,每個數(shù)據源都(dōu)隻能(néng)處理一(yī)個來源,并且與特定的數(shù)據類型(例如(rú) News、Authors、NewsAndAuthors 或 UserPreferences)相對應。使用數(shù)據源的類應該不知道(dào)數(shù)據是如(rú)何保存的,例如(rú)是保存在數(shù)據庫中,還是保存在文件中。
使用 Room 作(zuò)為(wèi)數(shù)據源
由于每個數(shù)據源都(dōu)應隻負責處理一(yī)種特定類型的數(shù)據的一(yī)個數(shù)據源,因此 Room 數(shù)據源會接收數(shù)據訪問(wèn)對象 (DAO) 或數(shù)據庫本身作(zuò)為(wèi)參數(shù)。例如(rú),NewsLocalDataSource 可(kě)以接收 NewsDao 的實例作(zuò)為(wèi)參數(shù),AuthorsLocalDataSource 則可(kě)以接收 AuthorsDao 的實例。
在某些情況下(xià),如(rú)果不需要(yào)額外(wài)的邏輯,您可(kě)以直接将 DAO 注入存儲庫,因為(wèi) DAO 是一(yī)種可(kě)以在測試中輕松替換的接口。
如(rú)需詳細了(le)解如(rú)何使用 Room API,請(qǐng)參閱 Room 指南。
使用 DataStore 作(zuò)為(wèi)數(shù)據源
DataStore 非常适合存儲鍵值對,例如(rú)用戶設置,具體示例可(kě)能(néng)包括時(shí)間(jiān)格式、通(tōng)知偏好設置,以及是顯示還是隐藏用戶已閱讀的新聞報道(dào)。DataStore 還可(kě)以使用協議(yì)緩沖區(qū)來存儲類型化對象。
與任何其他(tā)對象一(yī)樣,由 DataStore 提供支持的數(shù)據源應包含與特定類型相對應或與應用的特定部分相對應的數(shù)據。對于 DataStore 來說更是如(rú)此,因為(wèi) DataStore 讀取操作(zuò)會作(zuò)為(wèi)一(yī)個每次值更新後都(dōu)會發出的數(shù)據流進行(xíng)公開(kāi)。因此,您應将相關偏好設置存儲在同一(yī)個 DataStore 中。
例如(rú),您可(kě)以創建一(yī)個僅處理通(tōng)知相關偏好設置的 NotificationsDataStore,并創建一(yī)個僅處理新聞屏幕相關偏好設置的 NewsPreferencesDataStore。這(zhè)樣,您就可(kě)以更好地(dì)限定更新作(zuò)用域,因為(wèi)隻有當與相應屏幕相關的偏好設置發生變化時(shí),newsScreenPreferencesDataStore.data 流才會發出。這(zhè)也意味着,該對象的生命周期可(kě)以更短,因為(wèi)它隻能(néng)在新聞屏幕顯示時(shí)存在。
如(rú)需詳細了(le)解如(rú)何使用 DataStore API,請(qǐng)參閱 DataStore 指南。
使用文件作(zuò)為(wèi)數(shù)據源
處理大型對象(例如(rú) JSON 對象或位圖)時(shí),您需要(yào)使用 File 對象并處理線程切換。
如(rú)需詳細了(le)解如(rú)何使用文件存儲空間(jiān),請(qǐng)參閱存儲空間(jiān)概覽頁面。
使用 WorkManager 調度任務
假設為(wèi)“新聞”應用引入了(le)一(yī)項新的要(yào)求:隻要(yào)設備正在充電并且已連接到不按流量計費(fèi)的網絡,該應用就必須為(wèi)用戶提供用于選擇定期自(zì)動獲取最新新聞的選項。這(zhè)會使此操作(zuò)成為(wèi)一(yī)項面向業務的操作(zuò)。如(rú)果該應用實現了(le)這(zhè)一(yī)要(yào)求,那(nà)麽在用戶打開(kāi)該應用時(shí),即使設備沒有連接到網絡,用戶仍然可(kě)以看(kàn)到最近的新聞。
借助 WorkManager,可(kě)以輕松調度異步的可(kě)靠工(gōng)作(zuò),并可(kě)以負責管理約束條件。我們建議(yì)使用該庫執行(xíng)持久性工(gōng)作(zuò)。為(wèi)了(le)執行(xíng)上(shàng)面定義的任務,我們創建了(le)一(yī)個 Worker 類:RefreshLatestNewsWorker。此類以 NewsRepository 作(zuò)為(wèi)依賴項,以便獲取最新新聞并将其緩存到磁盤中。
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ì)為(wèi)單獨的數(shù)據源。這(zhè)樣一(yī)來,WorkManager 将僅負責确保工(gōng)作(zuò)會在所有約束條件都(dōu)得到滿足時(shí)在後台線程中執行(xíng)。通(tōng)過遵循此模式,您可(kě)以根據需要(yào)在不同環境中快(kuài)速交換實現。
在此示例中,必須從(cóng) NewsRepository 調用這(zhè)個與新聞相關的任務,前者會将一(yī)個新的數(shù)據源作(zuò)為(wèi)依賴項:NewsTasksDataSource。實現方式如(rú)下(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è)些類型的類以其負責的數(shù)據命名,例如(rú) NewsTasksDataSource 或 PaymentsTasksDataSource。與特定類型的數(shù)據相關的所有任務都(dōu)應封裝在同一(yī)個類中。
如(rú)果任務需要(yào)在應用啓動時(shí)觸發,建議(yì)使用從(cóng) Initializer 調用存儲庫的 App Startup 庫觸發 WorkManager 請(qǐng)求。
如(rú)需詳細了(le)解如(rú)何使用 WorkManager API,請(qǐng)參閱 WorkManager 指南。
測試
遵循依賴項注入方面的最佳實踐有助于您測試自(zì)己的應用。對于與外(wài)部資源進行(xíng)通(tōng)信的類,依賴于接口也很(hěn)有幫助。測試某個單元時(shí),您可(kě)以注入其依賴項的虛構版本,以使測試具有确定性和(hé)可(kě)靠性。
單元測試
測試數(shù)據層時(shí),請(qǐng)遵循常規測試指南。對于單元測試,可(kě)以在需要(yào)時(shí)使用真實對象,并虛構所有會聯系外(wài)部來源(例如(rú)從(cóng)文件讀取內(nèi)容或從(cóng)網絡讀取內(nèi)容)的依賴項。
集成測試
需要(yào)訪問(wèn)外(wài)部來源的集成測試往往不太具有确定性,因為(wèi)它們需要(yào)在實際設備上(shàng)運行(xíng)。建議(yì)您在受控環境中執行(xíng)這(zhè)些測試,以便使集成測試更加可(kě)靠。
對于數(shù)據庫,Room 允許創建一(yī)個您可(kě)以在測試時(shí)完全控制的內(nèi)存中數(shù)據庫。如(rú)需了(le)解詳情,請(qǐng)參閱測試和(hé)調試數(shù)據庫頁面。
對于網絡,有一(yī)些常用的庫(例如(rú) WireMock 或 MockWebServer)可(kě)用于虛構 HTTP 和(hé) HTTPS 調用并驗證請(qǐng)求是否已按預期發出。
網站建設開(kāi)發|APP設計開(kāi)發|小程序建設開(kāi)發