【Android開發(fā)】2022Android官方架構(gòu)指南系列文章

文章詳細(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 主要也是由 DataSourceRepository 組成凡橱。

想要將以上述兩件事情做好,我們就是需要搞清楚兩者的定義以及其如何協(xié)作舅柜,也就是要回答下述的幾個問題:

  • 如何定義及封裝 DataSource梭纹?

  • 如何定義及封裝 Repository

  • DataSourceRepository 之間如何交互致份?

這篇文章主要要解決的也是上面的幾個問題变抽。好的,下面我們就是進(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)是可以變化的产雹。例如:NewsRemoteDataSourceNewsLocalDataSource。在來源非常重要的情況下樱调,為了更加具體约素,可以使用來源的類型。例如:NewsNetworkDataSourceNewsDiskDataSource笆凌。

請勿根據(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拌倍、MoviesRepositoryPaymentsRepository

創(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 中包含零個或多個 DataSourceDataSource 根據(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、MaybeCompletable 類型

    • All:使用回調(diào)

  • 數(shù)據(jù)流操作

    • Java:RxJava ObservableFlowable類型哥倔。

    • All:使用回調(diào)

總結(jié)

為了更好的完成上述事情秸架,需要解答下面三個問題:

  • 如何定義及封裝 DataSource

  • 如何定義及封裝 Repository咆蒿?

  • DataSourceRepository 之間如何交互东抹?

回答這三個問題也是這篇文章的核心目的。

原作者madroid
原文鏈接Android 官方現(xiàn)代 App 架構(gòu)指南 - 總結(jié)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末沃测,一起剝皮案震驚了整個濱河市缭黔,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蒂破,老刑警劉巖馏谨,帶你破解...
    沈念sama閱讀 221,273評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異附迷,居然都是意外死亡惧互,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,349評論 3 398
  • 文/潘曉璐 我一進(jìn)店門喇伯,熙熙樓的掌柜王于貴愁眉苦臉地迎上來壹哺,“玉大人,你說我怎么就攤上這事艘刚。” “怎么了截珍?”我有些...
    開封第一講書人閱讀 167,709評論 0 360
  • 文/不壞的土叔 我叫張陵攀甚,是天一觀的道長。 經(jīng)常有香客問我岗喉,道長秋度,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,520評論 1 296
  • 正文 為了忘掉前任钱床,我火速辦了婚禮荚斯,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘查牌。我一直安慰自己事期,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 68,515評論 6 397
  • 文/花漫 我一把揭開白布纸颜。 她就那樣靜靜地躺著兽泣,像睡著了一般。 火紅的嫁衣襯著肌膚如雪胁孙。 梳的紋絲不亂的頭發(fā)上唠倦,一...
    開封第一講書人閱讀 52,158評論 1 308
  • 那天称鳞,我揣著相機與錄音,去河邊找鬼稠鼻。 笑死冈止,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的候齿。 我是一名探鬼主播熙暴,決...
    沈念sama閱讀 40,755評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼毛肋!你這毒婦竟也來了怨咪?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,660評論 0 276
  • 序言:老撾萬榮一對情侶失蹤润匙,失蹤者是張志新(化名)和其女友劉穎诗眨,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體孕讳,經(jīng)...
    沈念sama閱讀 46,203評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡匠楚,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,287評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了厂财。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片芋簿。...
    茶點故事閱讀 40,427評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖璃饱,靈堂內(nèi)的尸體忽然破棺而出与斤,到底是詐尸還是另有隱情,我是刑警寧澤荚恶,帶...
    沈念sama閱讀 36,122評論 5 349
  • 正文 年R本政府宣布撩穿,位于F島的核電站,受9級特大地震影響谒撼,放射性物質(zhì)發(fā)生泄漏食寡。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,801評論 3 333
  • 文/蒙蒙 一廓潜、第九天 我趴在偏房一處隱蔽的房頂上張望抵皱。 院中可真熱鬧,春花似錦辩蛋、人聲如沸呻畸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,272評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽擂错。三九已至,卻和暖如春樱蛤,著一層夾襖步出監(jiān)牢的瞬間钮呀,已是汗流浹背剑鞍。 一陣腳步聲響...
    開封第一講書人閱讀 33,393評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留爽醋,地道東北人蚁署。 一個月前我還...
    沈念sama閱讀 48,808評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像蚂四,于是被迫代替她去往敵國和親光戈。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,440評論 2 359

推薦閱讀更多精彩內(nèi)容