文章詳細(xì)的介紹了2022 Android 官方架構(gòu)指南,對于中高級的開發(fā)者而言都具有很高的價值嫂伞。文章主要分以下幾個部分:
概述
?? 名詞解釋
DataSource
:數(shù)據(jù)源類孔厉,是 App 中業(yè)務(wù)邏輯與系統(tǒng) API 和三方 SDK 的橋接類。Repository
:數(shù)據(jù)倉庫類帖努,其使用DataSource
處理業(yè)務(wù)邏輯撰豺,并將數(shù)據(jù)提供給上層調(diào)用者的類。
Data Layer 主要做了下面兩件事情:
通過
DataSource
封裝系統(tǒng)及三方 API拼余;通過
Repository
使用DataSource
封裝業(yè)務(wù)邏輯污桦,并暴露給使用者;
因此匙监,Data Layer 主要也是由 DataSource
與 Repository
組成凡橱。
想要將以上述兩件事情做好,我們就是需要搞清楚兩者的定義以及其如何協(xié)作舅柜,也就是要回答下述的幾個問題:
如何定義及封裝
DataSource
梭纹?如何定義及封裝
Repository
?DataSource
與Repository
之間如何交互致份?
這篇文章主要要解決的也是上面的幾個問題变抽。好的,下面我們就是進(jìn)入主題。首先看一下兩者分別是如何定義的的绍载。
如何定義及封裝 DataSource诡宗?
DataSource
主要做了以下幾件事情:
封裝系統(tǒng) API,如文件讀寫击儡、位置信息塔沃;
封裝系統(tǒng)庫及三方庫 API,如 Okhttp阳谍、Retrofit蛀柴、Room 等;
每個 DataSource
僅處理一種數(shù)據(jù)類型的數(shù)據(jù)矫夯,該數(shù)據(jù)源可以是文件鸽疾、網(wǎng)絡(luò)來源或本地數(shù)據(jù)庫。根據(jù)其來源训貌,又可以將其分為:LocalDataSource
尝盼、RemoteDataSource
等類型鲤桥。
其命名規(guī)范如下:
DataSource 命名規(guī)范
?? DataSource
命名規(guī)范:
DataSource
以其負(fù)責(zé)的數(shù)據(jù)以及使用的來源命名。具體命名慣例如下:
數(shù)據(jù)類型 + 來源類型 + DataSource。
對于數(shù)據(jù)的類型一忱,可以使用 Remote 或 Local许饿,以使其更加通用胖烛,因為實現(xiàn)是可以變化的产雹。例如:NewsRemoteDataSource
或 NewsLocalDataSource
。在來源非常重要的情況下樱调,為了更加具體约素,可以使用來源的類型。例如:NewsNetworkDataSource
或 NewsDiskDataSource
笆凌。
請勿根據(jù)實現(xiàn)細(xì)節(jié)來為數(shù)據(jù)源命名(例如 UserSharedPreferencesDataSource
)圣猎,因為使用相應(yīng)數(shù)據(jù)源的存儲庫應(yīng)該不知道數(shù)據(jù)是如何保存的。如果您遵循此規(guī)則乞而,便可以更改數(shù)據(jù)源的實現(xiàn)(例如送悔,從 SharedPreferences 遷移到 DataStore),而不會影響調(diào)用相應(yīng)數(shù)據(jù)源的層爪模。
創(chuàng)建 DataSource
以請求網(wǎng)絡(luò)接口為例欠啤,實例如下:
class NewsRemoteDataSource(
private val newsApi: NewsApi,
private val ioDispatcher: CoroutineDispatcher
){
/**
* 在 IO 線程中,獲取網(wǎng)絡(luò)數(shù)據(jù)屋灌,在主線程調(diào)用是安全的
*/
suspend fun fetchLatestNews(): List<ArticleHeadline> = withContext(ioDispatcher) {
// 將耗時操作移動到 IO 線程中
newsApi.fetchLatestNews()
}
}
interface NewsApi {
fun fetchLatestNews(): List<ArticleHeadline>
}
暴露接口
系統(tǒng) API 及大部分三方 SDK 大都是以 Callback 的形式返回數(shù)據(jù)洁段,為了統(tǒng)一與簡化使用,我們可以使用 Kotlin 協(xié)程對其進(jìn)行封裝簡化共郭。以獲取系統(tǒng)位置信息為例祠丝,可以使用 suspendCoroutine
或者suspendCancellableCoroutine
將其轉(zhuǎn)換為 suspend 函數(shù)了疾呻,如下:
class LocationDataSource {
// 將系統(tǒng)的 Callback 類型函數(shù),使用 suspendCoroutine 封裝為 suspend 函數(shù)
suspend fun awaitLastLocation(): Location =
suspendCoroutine<Location> { continuation ->
lastLocation.addOnSuccessListener { location ->
continuation.resume(location)
}.addOnFailureListener { e ->
continuation.resumeWithException(e)
}
}
這種方式只能處理單次異步的數(shù)據(jù)請求写半,對于流失數(shù)據(jù)請求岸蜗,可以使用 callbackFlow
來處理,如下:
class LocationDataSource {
// 將系統(tǒng)的 Callback 類似數(shù)據(jù)流叠蝇,使用 callbackFlow 封裝為 Flow 函數(shù)
fun locationFlow(): Flow<Location> = callbackFlow {
val callback = object : LocationCallback() {
override fun onLocationResult(result: LocationResult?) {
result ?: return
for (location in result.locations) {
offer(location) // emit location into the Flow using ProducerScope.offer
}
}
}
requestLocationUpdates(createLocationRequest(), callback, Looper.getMainLooper()).addOnFailureListener { e ->
close(e) // in case of exception, close the Flow
}
awaitClose {
removeLocationUpdates(callback) // clean up when Flow collection ends
}
}
}
更多內(nèi)容可參考官方 Codelabs 璃岳。
實體定義
DataSource 中一般都會將數(shù)據(jù)通過實體類的方式對外暴露,此部分的定義需要根據(jù)其業(yè)務(wù)類型做不同的處理:
網(wǎng)絡(luò)接口請求部分悔捶,直接將實體定義為對應(yīng)的 JSON 實例铃慷,不建議缺少字段;
對于系統(tǒng)原生 API 的返回數(shù)據(jù)蜕该,建議滿足當(dāng)前業(yè)務(wù)需求即可枚冗,無需全部保留;
對于數(shù)據(jù)庫(Room)的 Entity蛇损,可以根據(jù)業(yè)務(wù)需要自由定義;
DataSource 的幾種分類
-
LocalDataSource
DataStore坛怪、MMKV淤齐、SharedPreferenced
Room、GreenDao
File
-
RemoteDataSource
OkHttp
Retrofit
-
BusinessDataSource
Ble袜匿、Bluetooth
GPS
...
下面我們看一下如何定義及封裝 Repository更啄。
如何定義及封裝 Repository ?
Repository
主要做了一下幾件事情:
統(tǒng)一抽象 App 中的數(shù)據(jù)居灯,集中處理祭务,并對上層暴露;
并處理不同 DataSource 之間的沖突怪嫌;
處理業(yè)務(wù)邏輯义锥;
其命名規(guī)范如下:
命名規(guī)范
?? Repository
命名規(guī)范:
Repository
以其負(fù)責(zé)的數(shù)據(jù)命名。具體命名慣例如下:
數(shù)據(jù)類型 + Repository岩灭。
例如:NewsRepository
拌倍、MoviesRepository
或 PaymentsRepository
。
創(chuàng)建 Repository
以請求網(wǎng)絡(luò)接口為例噪径,實例如下:
// DataSource 層的引用通過構(gòu)造函數(shù)傳入
class NewsRepositoryImpl(
private val newsRemoteDataSource: NewsRemoteDataSource
): NewsRepository {
override suspend fun fetchLatestNews(): List<ArticleHeadline> =
newsRemoteDataSource.fetchLatestNews()
}
// 應(yīng)定義為 interface
interface NewsRepository {
suspend fun fetchLatestNews(): List<ArticleHeadline>
}
對外暴露數(shù)據(jù)
對外暴露的數(shù)據(jù)分為一次性數(shù)據(jù)以及隨時間變化的數(shù)據(jù)流柱恤,如下:
class ExampleRepository(
private val exampleRemoteDataSource: ExampleRemoteDataSource, // network
private val exampleLocalDataSource: ExampleLocalDataSource // database
) {
// 數(shù)據(jù)流
val data: Flow<Example> = ...
// 一次性操作
suspend fun modifyData(example: Example) { ... }
}
合并不同的數(shù)據(jù)源
Repository 中可以整合多個 DataSource,并將其統(tǒng)一處理作為單一信源對外暴露找爱。比如優(yōu)先從 LocalDataSource 獲取新聞信息梗顺,然后在從 RemoteDataSource 中獲取數(shù)據(jù):
class NewsRepositoryImpl(
private val remoteDataSource: NewsRemoteDataSource,
private val localDataSource: NewsLocalDataSource,
) : NewsRepository {
// 添加同步鎖,確保其在多線程中是安全的
private val latestNewsMutex = Mutex()
// 將最新的數(shù)據(jù)緩存在內(nèi)存中
private var latestNews: List<ArticleApiModel> = emptyList()
suspend fun getLatestNews(): List<ArticleApiModel> {
// 先讀取本地數(shù)據(jù)车摄,如果沒有的話寺谤,則請求云端數(shù)據(jù)
val local = localDataSource.getLatestNews()
if (local.isEmpty()) {
val networkResult = remoteDataSource.fetchLatestNews()
// 將網(wǎng)絡(luò)中的最新數(shù)據(jù)保存到本地的數(shù)據(jù)庫中
latestNewsMutex.withLock {
localDataSource.saveLatestNews(networkResult)
}
}
return latestNewsMutex.withLock { local }
}
}
為了進(jìn)一步提高提高用戶體驗仑鸥,將 UI 盡可能快的展示給用戶,也可以在增加一層內(nèi)存緩存矗漾,如下:
class NewsRepositoryImpl(
private val remoteDataSource: NewsRemoteDataSource,
private val localDataSource: NewsLocalDataSource,
) : NewsRepository {
// 添加同步鎖锈候,確保其在多線程中是安全的
private val latestNewsMutex = Mutex()
// 將最新的數(shù)據(jù)緩存在內(nèi)存中
private var latestNews: List<ArticleApiModel> = emptyList()
suspend fun getLatestNews(): List<ArticleApiModel> {
// 先判斷內(nèi)存中是否有數(shù)據(jù),沒有的話敞贡,則讀取 Local 中的數(shù)據(jù)
if (latestNews.isEmpty()) {
// 讀取本地數(shù)據(jù)泵琳,如果沒有的話,則請求云端數(shù)據(jù)
val local = localDataSource.getLatestNews()
if (local.isEmpty()) {
val networkResult = remoteDataSource.fetchLatestNews()
// 將網(wǎng)絡(luò)中的最新數(shù)據(jù)保存到本地緩存及內(nèi)存中
latestNewsMutex.withLock {
localDataSource.saveLatestNews(networkResult)
}
latestNews = networkResult
}
latestNews = local
}
return latestNewsMutex.withLock { this.latestNews }
}
}
DataSource 與 Repository 之間如何交互誊役?
一個業(yè)務(wù)模塊中可以包含一個或者多個 Repository
获列,每個 Repository
中包含零個或多個 DataSource
,DataSource
根據(jù)其來源可以分為 LocalDataSource
蛔垢、RemoteDataSource
等击孩。
依賴管理的方式建議使用 Hilt 或者是 Kion 。
統(tǒng)通規(guī)則
線程處理
線程處理的統(tǒng)一原則就是對 UI 線程是安全的鹏漆,如果是耗時操作請將其切換到非 UI 線程中執(zhí)行巩梢。除了這部分規(guī)則之外,還會根據(jù)其業(yè)務(wù)類型的不同有不同的處理方式:
面向 UI 的操作
這部分的作用域通常是 viewModelScope 或者是 lifecycleScope艺玲,其作用域會根據(jù)生命周期的變化而變化括蝠,僅僅保證其在主線程調(diào)用是安全的即可。
class NewsRemoteDataSource(
private val newsApi: NewsApi,
private val ioDispatcher: CoroutineDispatcher
) {
/**
* 在 IO 線程中饭聚,獲取網(wǎng)絡(luò)數(shù)據(jù)忌警,在主線程調(diào)用是安全的
*/
suspend fun fetchLatestNews(): List<ArticleHeadline> = withContext(ioDispatcher) {
// 將耗時操作移動到 IO 線程中
newsApi.fetchLatestNews()
}
}
面向 App 的操作
面向 App 操作的話,其任務(wù)的執(zhí)行周期要比頁面的周期更長秒梳,所以需要在頁面生命周期銷毀之后仍然可以在后臺執(zhí)行法绵,所以需要傳入一個全局的 scope,如下:
class NewsRepositoryImpl(
private val remoteDataSource: NewsRemoteDataSource,
// 傳入一個外部的 scope酪碘,通常其是全局的單例或者是在 Application 中實例化的
private val externalScope: CoroutineScope,
private val ioDispatcher: CoroutineDispatcher
) : NewsRepository {
override suspend fun fetchLatestNews(refresh: Boolean): List<ArticleApiModel> {
return withContext(ioDispatcher) {
// 切換至整個 App 的作用域內(nèi)朋譬,當(dāng)其調(diào)用者被銷毀時,其仍會執(zhí)行
withContext(externalScope.coroutineContext) {
remoteDataSource.fetchLatestNews()
}
}
}
}
更多詳細(xì)內(nèi)容可查看 Coroutines & Patterns for work that shouldn’t be cancelled 婆跑。
面向業(yè)務(wù)的操作
面向業(yè)務(wù)的操作無法取消此熬。它們應(yīng)該會在進(jìn)程終止后繼續(xù)執(zhí)行。例如滑进,完成上傳用戶想要發(fā)布到其個人資料的照片犀忱。對于這部分的業(yè)務(wù),需要使用 WorkManager 扶关,其定義如下:
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()
}
}
Worker 應(yīng)作為一個數(shù)據(jù)源進(jìn)行使用阴汇,封裝如下:
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<RefreshLatestNewsWorker>(
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)
}
}
注意: WorkManager 也可以執(zhí)行單次任務(wù),不再此處展開节槐。
提供 API
DataSource 及 Repository 中提到 Kotlin 中的一些提供方式搀庶,對應(yīng) Java 代碼或者是 RxJava 的代碼而言拐纱,有以下方式:
-
一次性操作
Java:使用RxJava
Single、Maybe
或Completable
類型All:使用回調(diào)
-
數(shù)據(jù)流操作
Java:RxJava
Observable
或Flowable
類型哥倔。All:使用回調(diào)
總結(jié)
為了更好的完成上述事情秸架,需要解答下面三個問題:
如何定義及封裝
DataSource
?如何定義及封裝
Repository
咆蒿?DataSource
與Repository
之間如何交互东抹?
回答這三個問題也是這篇文章的核心目的。