Android App封裝 ——架構(gòu)(MVI + kotlin + Flow)

原文地址

項(xiàng)目搭建經(jīng)歷記錄

  1. Android App封裝 ——架構(gòu)(MVI + kotlin + Flow)
  2. Android App封裝 —— ViewBinding
  3. Android App封裝 —— DI框架 Hilt?Koin阅仔?
  4. Android App封裝 —— 實(shí)現(xiàn)自己的EventBus

一藕甩、背景

最近看了好多MVI的文章锥累,原理大多都是參照google發(fā)布的 應(yīng)用架構(gòu)指南土铺,但是實(shí)現(xiàn)方式有很多種,就想自己封裝一套自己喜歡用的MVI架構(gòu)帆焕,以供以后開(kāi)發(fā)App使用帖旨。

說(shuō)干就干,準(zhǔn)備對(duì)標(biāo)“玩Android”彬呻,利用提供的數(shù)據(jù)接口衣陶,搭建一個(gè)自己習(xí)慣使用的一套App項(xiàng)目,項(xiàng)目地址:Github wanandroid闸氮。

二剪况、MVI

先簡(jiǎn)單說(shuō)一下MVI,從MVC到MVP到MVVM再到現(xiàn)在的MVI蒲跨,google是為了一直解決痛點(diǎn)所以不斷推出新的框架译断,具體的發(fā)展流程就不多做贅訴了,網(wǎng)上有好多或悲,我們可以選擇性適合自己的孙咪。

應(yīng)用架構(gòu)指南中主要的就是兩個(gè)架構(gòu)圖:

2.1 總體架構(gòu)

[圖片上傳失敗...(image-ad7268-1673343374642)]

Google推薦的是每個(gè)應(yīng)用至少有兩層:

  • UI Layer 界面層: 在屏幕上顯示應(yīng)用數(shù)據(jù)
  • Data Layer 數(shù)據(jù)層: 提供所需要的應(yīng)用數(shù)據(jù)(通過(guò)網(wǎng)絡(luò)、文件等)
  • Domain Layer(optional)領(lǐng)域?qū)?網(wǎng)域?qū)?(可選):主要用于封裝數(shù)據(jù)層的邏輯巡语,方便與界面層的交互翎蹈,可以根據(jù)User Case

圖中主要的點(diǎn)在于各層之間的依賴關(guān)系是單向的,所以方便了各層之間的單元測(cè)試

2.2 UI層架構(gòu)

UI簡(jiǎn)單來(lái)說(shuō)就是拿到數(shù)據(jù)并展示捌臊,而數(shù)據(jù)是以state表示UI不同的狀態(tài)傳送給界面的杨蛋,所以UI架構(gòu)分為

  • UI elements層:UI元素,由activity、fragment以及包含的控件組成
  • State holders層: state狀態(tài)的持有者逞力,這里一般是由viewModel承擔(dān)

[圖片上傳失敗...(image-60cfc6-1673343374643)]

2.3 MVI UI層的特點(diǎn)

MVI在UI層相比與MVVM的核心區(qū)別是它的兩大特性:

  1. 唯一可信數(shù)據(jù)源
  2. 數(shù)據(jù)單向流動(dòng)曙寡。

[圖片上傳失敗...(image-8de122-1673343374643)]

從圖中可以看到,

  1. 數(shù)據(jù)從Data Layer -> ViewModel -> UI寇荧,數(shù)據(jù)是單向流動(dòng)的举庶。ViewModel將數(shù)據(jù)封裝成UI State傳輸?shù)経I elements中,而UI elements是不會(huì)傳輸數(shù)據(jù)到ViewModel的揩抡。
  2. UI elements上的一些點(diǎn)擊或者用戶事件户侥,都會(huì)封裝成events事件,發(fā)送給ViewModel

2.4 搭建MVI要注意的點(diǎn)

了解了MVI的原理和特點(diǎn)后峦嗤,我們就要開(kāi)始著手搭建了蕊唐,其中需要解決的有以下幾點(diǎn)

  1. 定義UI Stateevents
  2. 構(gòu)建UI State單向數(shù)據(jù)流UDF
  3. 構(gòu)建事件流events
  4. UI State的訂閱和發(fā)送

三烁设、搭建項(xiàng)目

3.1 定義UI State替梨、events

我們可以用interface先定義一個(gè)抽象的UI Stateevents装黑,eventintent是一個(gè)意思副瀑,都可以用來(lái)表示一次事件。

@Keep
interface IUiState

@Keep
interface IUiIntent

然后根據(jù)具體邏輯定義頁(yè)面的UIState和UiIntent恋谭。

data class MainState(val bannerUiState: BannerUiState, val detailUiState: DetailUiState) : IUiState

sealed class BannerUiState {
    object INIT : BannerUiState()
    data class SUCCESS(val models: List<BannerModel>) : BannerUiState()
}

sealed class DetailUiState {
    object INIT : DetailUiState()
    data class SUCCESS(val articles: ArticleModel) : DetailUiState()
}

通過(guò)MainState將頁(yè)面的不同狀態(tài)封裝起來(lái)糠睡,從而實(shí)現(xiàn)唯一可信數(shù)據(jù)源

3.2 構(gòu)建單向數(shù)據(jù)流UDF

在ViewModel中使用StateFlow構(gòu)建UI State流。

  • _uiStateFlow用來(lái)更新數(shù)據(jù)
  • uiStateFlow用來(lái)暴露給UI elements訂閱
abstract class BaseViewModel<UiState : IUiState, UiIntent : IUiIntent> : ViewModel() {

    private val _uiStateFlow = MutableStateFlow(initUiState())
    val uiStateFlow: StateFlow<UiState> = _uiStateFlow

    protected abstract fun initUiState(): UiState

    protected fun sendUiState(copy: UiState.() -> UiState) {
        _uiStateFlow.update { copy(_uiStateFlow.value) }
    }
}
class MainViewModel : BaseViewModel<MainState, MainIntent>() {

    override fun initUiState(): MainState {
        return MainState(BannerUiState.INIT, DetailUiState.INIT)
    }
}

3.3 構(gòu)建事件流

在ViewModel中使用 Channel構(gòu)建事件流

  1. _uiIntentFlow用來(lái)傳輸Intent
  2. 在viewModelScope中開(kāi)啟協(xié)程監(jiān)聽(tīng)uiIntentFlow疚颊,在子ViewModel中只用重寫handlerIntent方法就可以處理Intent事件了
  3. 通過(guò)sendUiIntent就可以發(fā)送Intent事件了
abstract class BaseViewModel<UiState : IUiState, UiIntent : IUiIntent> : ViewModel() {

    private val _uiIntentFlow: Channel<UiIntent> = Channel()
    val uiIntentFlow: Flow<UiIntent> = _uiIntentFlow.receiveAsFlow()
    
    fun sendUiIntent(uiIntent: UiIntent) {
        viewModelScope.launch {
            _uiIntentFlow.send(uiIntent)
        }
    }

    init {
        viewModelScope.launch {
            uiIntentFlow.collect {
                handleIntent(it)
            }
        }
    }

    protected abstract fun handleIntent(intent: IUiIntent)
class MainViewModel : BaseViewModel<MainState, MainIntent>() {

    override fun handleIntent(intent: IUiIntent) {
        when (intent) {
            MainIntent.GetBanner -> {
                requestDataWithFlow()
            }
            is MainIntent.GetDetail -> {
                requestDataWithFlow()
            }
        }
    }
}

3.4 UI State的訂閱和發(fā)送

3.4.1 訂閱UI State

在Activity中訂閱UI state的變化

  1. lifecycleScope中開(kāi)啟協(xié)程狈孔,collect uiStateFlow
  2. 使用map 來(lái)做局部變量的更新
  3. 使用distinctUntilChanged來(lái)做數(shù)據(jù)防抖
class MainActivity : BaseMVIActivity() {

    private fun registerEvent() {
        lifecycleScope.launchWhenStarted {
            mViewModel.uiStateFlow.map { it.bannerUiState }.distinctUntilChanged().collect { bannerUiState ->
                when (bannerUiState) {
                    is BannerUiState.INIT -> {}
                    is BannerUiState.SUCCESS -> {
                        bannerAdapter.setList(bannerUiState.models)
                    }
                }
            }
        }
        lifecycleScope.launchWhenStarted {
            mViewModel.uiStateFlow.map { it.detailUiState }.distinctUntilChanged().collect { detailUiState ->
                when (detailUiState) {
                    is DetailUiState.INIT -> {}
                    is DetailUiState.SUCCESS -> {
                        articleAdapter.setList(detailUiState.articles.datas)
                    }
                }

            }
        }
    }
}

3.4.2 發(fā)送Intent

直接調(diào)用sendUiIntent就可以發(fā)送Intent事件

button.setOnClickListener {
    mViewModel.sendUiIntent(MainIntent.GetBanner)
    mViewModel.sendUiIntent(MainIntent.GetDetail(0))
}

3.4.3 更新Ui State

調(diào)用sendUiState發(fā)送Ui State更新

需要注意的是: 在UiState改變時(shí)串稀,使用的是copy復(fù)制一份原來(lái)的UiState除抛,然后修改變動(dòng)的值。這是為了做到 “可信數(shù)據(jù)源”母截,在定義MainState的時(shí)候,設(shè)置的就是val橄教,是為了避免多線程并發(fā)讀寫清寇,導(dǎo)致線程安全的問(wèn)題。

class MainViewModel : BaseViewModel<MainState, MainIntent>() {
    private val mWanRepo = WanRepository()

    override fun initUiState(): MainState {
        return MainState(BannerUiState.INIT, DetailUiState.INIT)
    }

    override fun handleIntent(intent: IUiIntent) {
        when (intent) {
            MainIntent.GetBanner -> {
                requestDataWithFlow(showLoading = true,
                    request = { mWanRepo.requestWanData() },
                    successCallback = { data -> sendUiState { copy(bannerUiState = BannerUiState.SUCCESS(data)) } },
                    failCallback = {})
            }
            is MainIntent.GetDetail -> {
                requestDataWithFlow(showLoading = false,
                    request = { mWanRepo.requestRankData(intent.page) },
                    successCallback = { data -> sendUiState { copy(detailUiState = DetailUiState.SUCCESS(data)) } })
            }
        }
    }
}

其中 requestDataWithFlow 是封裝的一個(gè)網(wǎng)絡(luò)請(qǐng)求的方法

protected fun <T : Any> requestDataWithFlow(
    showLoading: Boolean = true,
    request: suspend () -> BaseData<T>,
    successCallback: (T) -> Unit,
    failCallback: suspend (String) -> Unit = { errMsg ->
        //默認(rèn)異常處理
    },
) {
    viewModelScope.launch {
        val baseData: BaseData<T>
        try {
            baseData = request()
            when (baseData.state) {
                ReqState.Success -> {
                    sendLoadUiState(LoadUiState.ShowMainView)
                    baseData.data?.let { successCallback(it) }
                }
                ReqState.Error -> baseData.msg?.let { error(it) }
            }
        } catch (e: Exception) {
            e.message?.let { failCallback(it) }
        }
    }
}

至此一個(gè)MVI的框架基本就搭建完畢了

四护蝶、 總結(jié)

不管是MVC华烟、MVP、MVVM還是MVI持灰,主要就是View和Model之間的交互關(guān)系不同

  • MVI的核心是 數(shù)據(jù)的單向流動(dòng)
  • MVI使用kotlin flow可以很方便的實(shí)現(xiàn) 響應(yīng)式編程
  • MV整個(gè)View只依賴一個(gè)State刷新盔夜,這個(gè)State就是 唯一可信數(shù)據(jù)源

目前搭建了基礎(chǔ)框架,后續(xù)還會(huì)在此項(xiàng)目的基礎(chǔ)上繼續(xù)封裝jetpack等更加完善這個(gè)項(xiàng)目。

項(xiàng)目源碼地址:Github wanandroid

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末喂链,一起剝皮案震驚了整個(gè)濱河市返十,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌椭微,老刑警劉巖洞坑,帶你破解...
    沈念sama閱讀 218,682評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異蝇率,居然都是意外死亡迟杂,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門本慕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)排拷,“玉大人,你說(shuō)我怎么就攤上這事锅尘」テ茫” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 165,083評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵鉴象,是天一觀的道長(zhǎng)忙菠。 經(jīng)常有香客問(wèn)我,道長(zhǎng)纺弊,這世上最難降的妖魔是什么牛欢? 我笑而不...
    開(kāi)封第一講書人閱讀 58,763評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮淆游,結(jié)果婚禮上傍睹,老公的妹妹穿的比我還像新娘。我一直安慰自己犹菱,他們只是感情好拾稳,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著腊脱,像睡著了一般访得。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上悍抑,一...
    開(kāi)封第一講書人閱讀 51,624評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音杜耙,去河邊找鬼。 笑死佑女,一個(gè)胖子當(dāng)著我的面吹牛记靡,可吹牛的內(nèi)容都是我干的谈竿。 我是一名探鬼主播劫恒,決...
    沈念sama閱讀 40,358評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼憔辫,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼锦亦!你這毒婦竟也來(lái)了杠园?” 一聲冷哼從身側(cè)響起惕橙,我...
    開(kāi)封第一講書人閱讀 39,261評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤吼虎,失蹤者是張志新(化名)和其女友劉穎洒疚,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體吠昭,經(jīng)...
    沈念sama閱讀 45,722評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蘑拯,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年距糖,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了悍引。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片玉凯。...
    茶點(diǎn)故事閱讀 40,030評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡泪幌,死狀恐怖署照,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情吗浩,我是刑警寧澤建芙,帶...
    沈念sama閱讀 35,737評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站懂扼,受9級(jí)特大地震影響禁荸,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜阀湿,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評(píng)論 3 330
  • 文/蒙蒙 一赶熟、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧炕倘,春花似錦钧大、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,941評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至涨醋,卻和暖如春瓜饥,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背浴骂。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,057評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工乓土, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人溯警。 一個(gè)月前我還...
    沈念sama閱讀 48,237評(píng)論 3 371
  • 正文 我出身青樓趣苏,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親梯轻。 傳聞我的和親對(duì)象是個(gè)殘疾皇子食磕,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評(píng)論 2 355

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