離線優先應用是無需訪問(wèn)互聯網就能(néng)執行(xíng)全部核心功能(néng)或一(yī)部分關鍵核心功能(néng)的應用。也就是說,它可(kě)以離線執行(xíng)部分或全部業務邏輯。
構建離線優先應用首先要(yào)考慮數(shù)據層,它提供對應用數(shù)據和(hé)業務邏輯的訪問(wèn)。應用可(kě)能(néng)需要(yào)不時(shí)刷新這(zhè)些來自(zì)設備外(wài)部來源的數(shù)據。為(wèi)此,它可(kě)能(néng)需要(yào)調用網絡資源來保持更新。
網絡的可(kě)用性并不一(yī)定總是能(néng)得到保證。設備往往免不了(le)會遇到網絡連接不穩定或速度緩慢(màn)的問(wèn)題。用戶也可(kě)能(néng)會遇到以下(xià)情況:
互聯網帶寬有限。
連接短暫中斷,例如(rú)在電梯或隧道(dào)中。
偶爾才能(néng)訪問(wèn)數(shù)據。例如(rú),使用的平闆電腦僅支持 Wi-Fi 連接。
不管原因如(rú)何,應用通(tōng)常可(kě)以在上(shàng)述情況下(xià)正常運行(xíng)。為(wèi)了(le)确保應用可(kě)在離線狀态下(xià)正常運行(xíng),它應該具備以下(xià)能(néng)力:
在沒有可(kě)靠網絡連接的情況下(xià)仍可(kě)使用。
立即向用戶提供本地(dì)數(shù)據,而不是等待第一(yī)次網絡調用完成或失敗。
提取數(shù)據的方式考慮到電池和(hé)數(shù)據狀态。例如(rú),僅在理想情況下(xià)(例如(rú)充電或有 Wi-Fi 連接時(shí))請(qǐng)求提取數(shù)據。
滿足上(shàng)述标準的應用通(tōng)常稱為(wèi)離線優先應用。
設計離線優先應用
在設計離線優先應用時(shí),首先應該設計數(shù)據層以及您可(kě)以對應用數(shù)據執行(xíng)的以下(xià)兩項主要(yào)操作(zuò):
讀取:檢索數(shù)據以供應用的其他(tā)部分使用,例如(rú)向用戶顯示信息。
寫入:持久存儲用戶輸入供日後檢索之用。
數(shù)據層中的存儲庫負責組合數(shù)據源以提供應用數(shù)據。在離線優先應用中,必須至少(shǎo)有一(yī)個數(shù)據源無需訪問(wèn)網絡即可(kě)執行(xíng)其最關鍵的任務。其中一(yī)項關鍵任務是讀取數(shù)據。
注意:離線優先應用至少(shǎo)應能(néng)在不訪問(wèn)網絡的情況下(xià)執行(xíng)讀取操作(zuò)。
離線優先應用中的模型數(shù)據
對于需要(yào)使用網絡資源的每個存儲庫,離線優先應用至少(shǎo)有 2 個數(shù)據源:
本地(dì)數(shù)據源
網絡數(shù)據源
注意:離線優先應用中需要(yào)訪問(wèn)網絡數(shù)據源的存儲庫應當始終具有本地(dì)數(shù)據源。
本地(dì)數(shù)據源
本地(dì)數(shù)據源是應用的規範可(kě)信來源。應用的較高層讀取任何數(shù)據,都(dōu)應将其作(zuò)為(wèi)專屬來源。這(zhè)樣可(kě)在處于兩次連接之間(jiān)的狀态時(shí)确保數(shù)據一(yī)緻性。本地(dì)數(shù)據源通(tōng)常由存儲空間(jiān)提供支持并持久存儲到磁盤。下(xià)面是将數(shù)據持久存儲到磁盤的一(yī)些常用方法:
結構化數(shù)據源,例如(rú) Room 等關系型數(shù)據庫。
非結構化數(shù)據源。例如(rú),Datastore 的協議(yì)緩沖區(qū)。
簡單文件
網絡數(shù)據源
網絡數(shù)據源是應用的實際狀态。最好将本地(dì)數(shù)據源與網絡數(shù)據源同步。本地(dì)數(shù)據源也有可(kě)能(néng)滞後于網絡數(shù)據源,在這(zhè)種情況下(xià),應用需要(yào)在重新聯網後進行(xíng)更新。相反,網絡數(shù)據源可(kě)以滞後于本地(dì)數(shù)據源,待連接恢複後,應用便可(kě)對其進行(xíng)更新。應用的網域層和(hé)界面層絕不應直接與網絡層通(tōng)信,而應由托管 repository 負責與其通(tōng)信并用其更新本地(dì)數(shù)據源。
公開(kāi)資源
應用對本地(dì)數(shù)據源和(hé)網絡數(shù)據源執行(xíng)讀寫操作(zuò)的方式存在根本差異。查詢本地(dì)數(shù)據源既快(kuài)速又靈活,例如(rú)在使用 SQL 查詢時(shí)。相反,查詢網絡數(shù)據源可(kě)能(néng)又慢(màn)又受到限制,例如(rú)在通(tōng)過 ID 逐步訪問(wèn) RESTful 資源時(shí)。這(zhè)導緻每種數(shù)據源對其提供的數(shù)據通(tōng)常需要(yào)采用自(zì)己的表示形式。因此,本地(dì)數(shù)據源和(hé)網絡數(shù)據源可(kě)能(néng)有自(zì)己的模型。
下(xià)面的目錄結構直觀體現了(le)這(zhè)一(yī)概念。AuthorEntity 表示從(cóng)應用的本地(dì)數(shù)據庫讀取的作(zuò)者,而 NetworkAuthor 表示通(tōng)過網絡序列化的作(zuò)者:
data/
├─ local/
│ ├─ entities/
│ │ ├─ AuthorEntity
│ ├─ dao/
│ ├─ NiADatabase
├─ network/
│ ├─ NiANetwork
│ ├─ models/
│ │ ├─ NetworkAuthor
├─ model/
│ ├─ Author
├─ repository/
接下(xià)來是 AuthorEntity 和(hé) NetworkAuthor 的詳細信息:
/**
* Network representation of [Author]
*/
@Serializable
data class NetworkAuthor(
val id: String,
val name: String,
val imageUrl: String,
val twitter: String,
val mediumPage: String,
val bio: String,
)
/**
* Defines an author for either an [EpisodeEntity] or [NewsResourceEntity].
* It has a many-to-many relationship with both entities
*/
@Entity(tableName = "authors")
data class AuthorEntity(
@PrimaryKey
val id: String,
val name: String,
@ColumnInfo(name = "image_url")
val imageUrl: String,
@ColumnInfo(defaultValue = "")
val twitter: String,
@ColumnInfo(name = "medium_page", defaultValue = "")
val mediumPage: String,
@ColumnInfo(defaultValue = "")
val bio: String,
)
最好将 AuthorEntity 和(hé) NetworkAuthor 都(dōu)留在數(shù)據層內(nèi)部,公開(kāi)第三種類型供外(wài)部層使用。這(zhè)可(kě)以保護外(wài)部層免受本地(dì)數(shù)據源和(hé)網絡數(shù)據源中不會從(cóng)根本上(shàng)改變應用行(xíng)為(wèi)的細微(wēi)更改影響。這(zhè)種做(zuò)法如(rú)以下(xià)代碼段所示:
/**
* External data layer representation of a "Now in Android" Author
*/
data class Author(
val id: String,
val name: String,
val imageUrl: String,
val twitter: String,
val mediumPage: String,
val bio: String,
)
然後,網絡模型可(kě)定義一(yī)種用于将其轉換為(wèi)本地(dì)模型的擴展方法,本地(dì)模型同樣也可(kě)定義一(yī)種用于将其轉換為(wèi)外(wài)部表示形式的擴展方法,如(rú)下(xià)所示:
/**
* Converts the network model to the local model for persisting
* by the local data source
*/
fun NetworkAuthor.asEntity() = AuthorEntity(
id = id,
name = name,
imageUrl = imageUrl,
twitter = twitter,
mediumPage = mediumPage,
bio = bio,
)
/**
* Converts the local model to the external model for use
* by layers external to the data layer
*/
fun AuthorEntity.asExternalModel() = Author(
id = id,
name = name,
imageUrl = imageUrl,
twitter = twitter,
mediumPage = mediumPage,
bio = bio,
)
注意:如(rú)上(shàng)所示的映射器通(tōng)常在不同模塊中定義的模型之間(jiān)進行(xíng)映射。因此,在使用這(zhè)些映射器的模塊中對其進行(xíng)定義以免模塊緊密耦合通(tōng)常是一(yī)種有益的做(zuò)法。如(rú)需了(le)解詳情,請(qǐng)參閱模塊化指南。
讀取
讀取是離線優先應用中應用數(shù)據的基本操作(zuò)。因此,您必須确保您的應用可(kě)以讀取數(shù)據,并确保一(yī)旦有新數(shù)據可(kě)用,應用便可(kě)以顯示這(zhè)些數(shù)據。能(néng)夠做(zuò)到這(zhè)一(yī)點的應用屬于響應式應用,因為(wèi)它們會公開(kāi)具有可(kě)觀察類型的讀取 API。
在下(xià)面的代碼段中,OfflineFirstTopicRepository 會為(wèi)其所有讀取 API 返回 Flows。這(zhè)樣,當它收到來自(zì)網絡數(shù)據源的更新時(shí),便可(kě)以更新自(zì)己的讀取器。換言之,它允許 OfflineFirstTopicRepository 在本地(dì)數(shù)據源失效時(shí)推送更改。因此,OfflineFirstTopicRepository 的每個讀取器都(dōu)必須準備好在應用恢複網絡連接時(shí)處理可(kě)能(néng)觸發的數(shù)據更改。此外(wài),OfflineFirstTopicRepository 還會直接從(cóng)本地(dì)數(shù)據源讀取數(shù)據。它隻能(néng)先更新本地(dì)數(shù)據源,通(tōng)過這(zhè)種更新将數(shù)據更改通(tōng)知讀取器。
class OfflineFirstTopicsRepository(
private val topicDao: TopicDao,
private val network: NiaNetworkDataSource,
) : TopicsRepository {
override fun getTopicsStream(): Flow<>
topicDao.getTopicEntitiesStream()
.map { it.map(TopicEntity::asExternalModel) }
}
注意:從(cóng)離線優先應用中的存儲庫讀取數(shù)據應直接從(cóng)本地(dì)數(shù)據源讀取。所有更新均應先寫入本地(dì)數(shù)據源,本地(dì)數(shù)據源會更新其使用方,因為(wèi)它可(kě)觀察。
錯誤處理策略
離線優先應用采用特有的錯誤處理方式,具體方式取決于出現錯誤的是哪一(yī)種數(shù)據源。以下(xià)幾小節将概要(yào)介紹這(zhè)些策略。
本地(dì)數(shù)據源
從(cóng)本地(dì)數(shù)據源讀取數(shù)據時(shí)遇到的錯誤應當極少(shǎo)。為(wèi)防止讀取器出錯,請(qǐng)對讀取器從(cóng)中收集數(shù)據的 Flows 使用 catch 操作(zuò)符。
在 ViewModel 中使用 catch 操作(zuò)符的代碼如(rú)下(xià)所示:
class AuthorViewModel(
authorsRepository: AuthorsRepository,
...
) : ViewModel() {
private val authorId: String = ...
// Observe author information
private val authorStream: Flow
authorsRepository.getAuthorStream(
id = authorId
)
.catch { emit(Author.empty()) }
}
注意:catch 操作(zuò)符隻能(néng)防止因異常而導緻應用崩潰,後備 Flow 仍會終止。如(rú)需在發生異常後恢複從(cóng)該數(shù)據流收集數(shù)據,請(qǐng)考慮使用 retry 方法。
網絡數(shù)據源
從(cóng)網絡數(shù)據源讀取數(shù)據時(shí)如(rú)果發生錯誤,應用需要(yào)采用啓發法來重試提取數(shù)據。常見(jiàn)的啓發法包括:
指數(shù)退避算法
在指數(shù)退避算法中,應用不斷嘗試從(cóng)網絡數(shù)據源讀取數(shù)據,兩次嘗試間(jiān)的時(shí)間(jiān)間(jiān)隔也不斷增加,直到讀取成功或其他(tā)條件決定其應停止讀取為(wèi)止。
評估應用是否應繼續退避的标準包括:
網絡數(shù)據源指出的錯誤類型。例如(rú),如(rú)果網絡調用返回的錯誤指出沒有連接,就應該重試該網絡調用。反之,如(rú)果 HTTP 請(qǐng)求未獲授權,那(nà)麽在獲得正确的憑據之前,就不應重試該 HTTP 請(qǐng)求。
允許的最大重試次數(shù)。
網絡連接監控
在此方法中,在應用确定可(kě)以連接到網絡數(shù)據源之前,系統會将讀取請(qǐng)求加入隊列。連接建立後,系統會将讀取請(qǐng)求移出隊列,讀取數(shù)據并更新本地(dì)數(shù)據源。在 Android 上(shàng),可(kě)使用 Room 數(shù)據庫維護此隊列,并使用 WorkManager 将其作(zuò)為(wèi)持久性工(gōng)作(zuò)排空。
寫入
讀取離線優先應用中數(shù)據的建議(yì)方式是使用可(kě)觀察類型,而寫入 API 的等效方式是異步 API,例如(rú)挂起函數(shù)。這(zhè)可(kě)以避免阻塞界面線程,并且有助于處理錯誤,因為(wèi)離線優先應用中的寫入操作(zuò)可(kě)能(néng)會在跨越網絡邊界時(shí)失敗。
interface UserDataRepository {
/**
* Updates the bookmarked status for a news resource
*/
suspend fun updateNewsResourceBookmark(newsResourceId: String, bookmarked: Boolean)
}
在上(shàng)面的代碼段中,所選的異步 API 是協程,因為(wèi)上(shàng)述方法挂起。
寫入策略
在離線優先應用中寫入數(shù)據時(shí),可(kě)以考慮采取三種策略。具體選擇哪種策略取決于要(yào)寫入的數(shù)據類型以及應用的要(yào)求:
僅在線寫入
嘗試跨網絡邊界寫入數(shù)據。如(rú)果成功,就更新本地(dì)數(shù)據源,否則抛出異常并留待調用方進行(xíng)适當響應。
此策略通(tōng)常用于必須近乎實時(shí)地(dì)在線執行(xíng)的寫入事務。例如(rú),銀(yín)行(xíng)轉賬。由于寫入可(kě)能(néng)會失敗,因此通(tōng)常有必要(yào)告知用戶寫入失敗,或者從(cóng)一(yī)開(kāi)始就阻止用戶嘗試寫入數(shù)據。在此類情況下(xià),您可(kě)以采取的策略可(kě)能(néng)包括:
如(rú)果應用需要(yào)訪問(wèn)互聯網才能(néng)寫入數(shù)據,可(kě)以選擇不向用戶顯示可(kě)供用戶寫入數(shù)據的界面,或至少(shǎo)也要(yào)停用該界面。
您可(kě)以使用一(yī)個用戶無法關閉的彈出式消息或一(yī)個短暫提示來通(tōng)知用戶他(tā)們處于離線狀态。
加入隊列的寫入
如(rú)果您有想要(yào)寫入的對象,請(qǐng)将其插入隊列。當應用恢複在線狀态時(shí),繼續使用指數(shù)退避算法排空隊列。在 Android 上(shàng),排空離線隊列是一(yī)項持久性工(gōng)作(zuò),通(tōng)常委托給 WorkManager。
此方法适合以下(xià)情況:
将數(shù)據寫入網絡并非必不可(kě)少(shǎo)。
事務對時(shí)效的要(yào)求不高。
如(rú)果操作(zuò)失敗,并非一(yī)定要(yào)通(tōng)知用戶。
适合此方法的用例包括分析事件和(hé)日志記錄。
延遲寫入
先寫入本地(dì)數(shù)據源,然後将寫入請(qǐng)求加入隊列,以便盡快(kuài)通(tōng)知網絡數(shù)據源。這(zhè)一(yī)點非常重要(yào),因為(wèi)當應用恢複在線狀态時(shí),網絡數(shù)據源與本地(dì)數(shù)據源之間(jiān)可(kě)能(néng)會存在沖突。下(xià)一(yī)部分将詳細介紹如(rú)何解決沖突。
當數(shù)據對應用至關重要(yào)時(shí),此方法是正确的選擇。例如(rú),在待辦事項列表離線優先應用中,用戶離線添加的任何任務都(dōu)必須存儲在本地(dì),以避免數(shù)據丢失的風(fēng)險。
注意:由于存在潛在沖突,在離線優先應用中寫入數(shù)據通(tōng)常比讀取數(shù)據需要(yào)考慮更多方面。離線優先應用想要(yào)被視(shì)為(wèi)離線優先,并不一(yī)定需要(yào)在離線狀态下(xià)能(néng)夠寫入數(shù)據。
同步和(hé)解決沖突
離線優先應用恢複連接時(shí),需要(yào)使本地(dì)數(shù)據源中的數(shù)據與網絡數(shù)據源中的數(shù)據一(yī)緻。此過程稱為(wèi)同步。應用與網絡數(shù)據源同步主要(yào)有兩種方式:
基于拉取的同步
基于推送的同步
基于拉取的同步
在基于拉取的同步中,應用在需要(yào)的時(shí)候連接到網絡數(shù)據源讀取最新應用數(shù)據。此方法的一(yī)種常用啓發法基于用戶導航,采用這(zhè)種方法時(shí),應用僅在向用戶提供數(shù)據之前提取數(shù)據。
當應用預計短時(shí)間(jiān)到中等長(cháng)度的時(shí)間(jiān)內(nèi)沒有網絡連接時(shí),最适合使用此方法。這(zhè)是因為(wèi)數(shù)據刷新需要(yào)伺機而為(wèi),長(cháng)時(shí)間(jiān)沒有網絡連接會提高用戶嘗試使用過時(shí)緩存或空緩存訪問(wèn)應用目的地(dì)的幾率。
假設在一(yī)個應用中,使用頁面令牌為(wèi)某個特定屏幕提取無限滾動列表中的項目。該應用的實現可(kě)延遲連接到網絡數(shù)據源,将數(shù)據持久存儲到本地(dì)數(shù)據源,然後從(cóng)本地(dì)數(shù)據源讀取數(shù)據以向用戶顯示信息。在沒有網絡連接的情況下(xià),存儲庫可(kě)以隻向本地(dì)數(shù)據源請(qǐng)求數(shù)據。以下(xià)是 Jetpack Paging 庫通(tōng)過其 RemoteMediator API 使用的模式。
class FeedRepository(...) {
fun feedPagingSource(): PagingSource
}
class FeedViewModel(
private val repository: FeedRepository
) : ViewModel() {
private val pager = Pager(
config = PagingConfig(
pageSize = NETWORK_PAGE_SIZE,
enablePlaceholders = false
),
remoteMediator = FeedRemoteMediator(...),
pagingSourceFactory = feedRepository::feedPagingSource
)
val feedPagingData = pager.flow
}
下(xià)表總結了(le)基于拉取的同步的優缺點:
優點 缺點
實現起來相對容易。 容易消耗大量流量。這(zhè)是因為(wèi)重複訪問(wèn)導航目的地(dì)會觸發不必要(yào)的操作(zuò),重新提取未更改的信息。您可(kě)以通(tōng)過适當的緩存來減少(shǎo)此問(wèn)題。若要(yào)使用緩存,可(kě)在界面層使用 cachedIn 操作(zuò)符或在網絡層使用 HTTP 緩存。
絕不會提取不需要(yào)的數(shù)據。 不能(néng)使用關系型數(shù)據很(hěn)好地(dì)擴展,因為(wèi)拉取的模型需要(yào)自(zì)給自(zì)足。如(rú)果待同步的模型依賴于需要(yào)提取的其他(tā)模型來填充自(zì)己,那(nà)麽上(shàng)面提到的消耗大量流量的問(wèn)題将變得更加嚴重。此外(wài),它還可(kě)能(néng)導緻父模型的存儲庫與嵌套模型的存儲庫之間(jiān)存在依賴關系。
基于推送的同步
在基于推送的同步中,本地(dì)數(shù)據源會盡力嘗試模拟網絡數(shù)據源的副本集。它會在首次啓動時(shí)主動提取适當數(shù)量的數(shù)據來設置基準,之後依靠來自(zì)服務器的通(tōng)知提醒自(zì)己數(shù)據何時(shí)過時(shí)。
收到過時(shí)通(tōng)知後,應用連接到網絡數(shù)據源,隻更新标記為(wèi)過時(shí)的數(shù)據。這(zhè)項工(gōng)作(zuò)将委托給 Repository,由其連接到網絡數(shù)據源,并将提取的數(shù)據持久存儲到本地(dì)數(shù)據源。由于存儲庫通(tōng)過可(kě)觀測類型公開(kāi)其數(shù)據,因此讀取器将收到所有更改的通(tōng)知。
class UserDataRepository(...) {
suspend fun synchronize() {
val userData = networkDataSource.fetchUserData()
localDataSource.saveUserData(userData)
}
}
在此方法中,應用對網絡數(shù)據源的依賴要(yào)低(dī)得多,而且長(cháng)時(shí)間(jiān)無法使用網絡數(shù)據源也能(néng)正常運行(xíng)。它可(kě)以在離線狀态下(xià)提供讀寫訪問(wèn),因為(wèi)系統假定本地(dì)存儲着來自(zì)網絡數(shù)據源的最新信息。
下(xià)表總結了(le)基于推送的同步的優缺點:
優點 缺點
應用可(kě)以無限期離線使用。 為(wèi)了(le)解決沖突,對數(shù)據進行(xíng)版本控制非常重要(yào)。
可(kě)将流量消耗降到最低(dī)。應用僅提取經過更改的數(shù)據。 需要(yào)考慮同步期間(jiān)的寫入問(wèn)題。
非常适合關系型數(shù)據。每個存儲庫隻負責為(wèi)其支持的模型提取數(shù)據。 網絡數(shù)據源需要(yào)支持同步。
混合同步
某些應用采用混合方法,具體基于拉取還是基于推送根據數(shù)據而定。例如(rú),某個社交媒體應用可(kě)能(néng)會使用基于拉取的同步按需提取用戶的關注 Feed,因為(wèi) Feed 更新的頻(pín)率較高。然而,同一(yī)應用可(kě)能(néng)會選擇使用基于推送的同步來提取已登錄用戶的相關數(shù)據,包括其用戶名、個人(rén)資料照片等。
最終,離線優先同步的選擇取決于産品要(yào)求和(hé)可(kě)用的技術(shù)基礎架構。
注意:應用的同步方法取決于應用的需求以及支持本地(dì)數(shù)據源和(hé)網絡數(shù)據源的基礎架構的限制。
沖突解決
如(rú)果應用處于離線狀态時(shí)在本地(dì)寫入的數(shù)據與網絡數(shù)據源的數(shù)據不一(yī)緻,說明(míng)存在沖突,必須解決沖突後才能(néng)進行(xíng)同步。
解決沖突問(wèn)題通(tōng)常需要(yào)借助版本控制。應用需要(yào)通(tōng)過一(yī)些簿記來跟蹤發生更改的時(shí)間(jiān)。這(zhè)樣,它就能(néng)将元數(shù)據傳遞給網絡數(shù)據源。然後,由網絡數(shù)據源負責提供絕對可(kě)信來源。根據應用的需求,可(kě)以考慮的沖突解決策略還有很(hěn)多。對于移動應用,常見(jiàn)的方法是“最後寫入內(nèi)容生效”。
最後寫入內(nèi)容生效
在此方法中,設備将時(shí)間(jiān)戳元數(shù)據附加到其寫入網絡數(shù)據源的數(shù)據中。網絡數(shù)據源在收到這(zhè)些數(shù)據後,會舍棄比當前狀态舊(jiù)的所有數(shù)據而接受比當前狀态新的數(shù)據。
在上(shàng)圖中,兩部設備都(dōu)處于離線狀态,并且最初都(dōu)與網絡數(shù)據源同步。離線時(shí),它們都(dōu)在本地(dì)寫入數(shù)據并跟蹤自(zì)己寫入數(shù)據的時(shí)間(jiān)。當二者恢複在線狀态并與網絡數(shù)據源同步時(shí),網絡數(shù)據源通(tōng)過持久存儲來自(zì)設備 B 的數(shù)據來解決沖突,因為(wèi)設備 B 寫入數(shù)據的時(shí)間(jiān)更晚。
離線優先應用中的 WorkManager
在前面介紹的讀取和(hé)寫入策略中,有兩個常用的實用程序:
隊列
讀取:用于将讀取操作(zuò)推遲到網絡連接可(kě)用時(shí)。
寫入:用于将寫入操作(zuò)推遲到網絡連接可(kě)用時(shí),并将寫入操作(zuò)重新加入隊列進行(xíng)重試。
網絡連接監視(shì)器
讀取:在應用連接時(shí)用作(zuò)排空讀取隊列的信号,也用于同步
寫入:在應用連接時(shí)用作(zuò)排空寫入隊列的信号,也用于同步
這(zhè)兩種情況都(dōu)是 WorkManager 擅長(cháng)的持久性工(gōng)作(zuò)的例子。例如(rú),在 Now in Android 示例應用中,同步本地(dì)數(shù)據源時(shí)将 WorkManager 用作(zuò)讀取隊列監視(shì)器和(hé)網絡監視(shì)器。在啓動時(shí),該應用會執行(xíng)以下(xià)操作(zuò):
将讀取同步工(gōng)作(zuò)加入隊列,以确保本地(dì)數(shù)據源和(hé)網絡數(shù)據源相同。
排空讀取同步隊列,并在應用處于在線狀态時(shí)開(kāi)始同步。
使用指數(shù)退避算法執行(xíng)從(cóng)網絡數(shù)據源讀取數(shù)據的操作(zuò)。
将讀取結果持久存儲到本地(dì)數(shù)據源中,解決可(kě)能(néng)發生的任何沖突。
公開(kāi)本地(dì)數(shù)據源中的數(shù)據,供應用的其他(tā)層使用。
使用 WorkManager 将同步工(gōng)作(zuò)加入隊列後,使用 KEEP ExistingWorkPolicy 将其指定為(wèi)唯一(yī)工(gōng)作(zuò):
class SyncInitializer : Initializer
override fun create(context: Context): Sync {
WorkManager.getInstance(context).apply {
// Queue sync on app startup and ensure only one
// sync worker runs at any time
enqueueUniqueWork(
SyncWorkName,
ExistingWorkPolicy.KEEP,
SyncWorker.startUpSyncWork()
)
}
return Sync
}
}
注意:“Now in Android”中的讀取隊列非常簡單,使用 enqueueUniqueWork API 來表示就足矣。為(wèi)了(le)進一(yī)步保證隊列的排空順序,需要(yào)使用 Room 或 Datastore 等數(shù)據持久化 API 實現更可(kě)靠的隊列實現。然後,您可(kě)以設置一(yī)個 Worker 來按順序排空此隊列。
其中,SyncWorker.startupSyncWork() 的定義如(rú)下(xià):
/**
Create a WorkRequest to call the SyncWorker using a DelegatingWorker.
This allows for dependency injection into the SyncWorker in a different
module than the app module without having to create a custom WorkManager
configuration.
*/
fun startUpSyncWork() = OneTimeWorkRequestBuilder
// Run sync as expedited work if the app is able to.
// If not, it runs as regular work.
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.setConstraints(SyncConstraints)
// Delegate to the SyncWorker.
.setInputData(SyncWorker::class.delegatedData())
.build()
val SyncConstraints
get() = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
具體而言,由 SyncConstraints 定義的 Constraints 要(yào)求 NetworkType 為(wèi) NetworkType.CONNECTED。也就是說,它會等到網絡可(kě)用後再運行(xíng)。
當網絡可(kě)用後,工(gōng)作(zuò)器将 SyncWorkName 指定的唯一(yī)工(gōng)作(zuò)隊列委托給适當的 Repository 實例來排空該隊列。如(rú)果同步失敗,doWork() 方法會返回 Result.retry()。WorkManager 将采用指數(shù)退避算法自(zì)動重試同步。否則,返回 Result.success() 完成同步。
class SyncWorker(...) : CoroutineWorker(appContext, workerParams), Synchronizer {
override suspend fun doWork(): Result = withContext(ioDispatcher) {
// First sync the repositories in parallel
val syncedSuccessfully = awaitAll(
async { topicRepository.sync() },
async { authorsRepository.sync() },
async { newsRepository.sync() },
).all { it }
if (syncedSuccessfully) Result.success()
else Result.retry()
}
}
網站建設開(kāi)發|APP設計開(kāi)發|小程序建設開(kāi)發