<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Android 最新架構(gòu)詳解 | MVI = 響應(yīng)式編程 + 單向數(shù)據(jù)流 + 唯一可信數(shù)據(jù)源 !

          共 18465字,需瀏覽 37分鐘

           ·

          2022-05-30 18:22

          ?安卓進(jìn)階漲薪訓(xùn)練營(yíng),讓一部分人先進(jìn)大廠

          大家好,我是皇叔,最近開了一個(gè)安卓進(jìn)階漲薪訓(xùn)練營(yíng),可以幫助大家突破技術(shù)&職場(chǎng)瓶頸,從而度過難關(guān),進(jìn)入心儀的公司。


          詳情見這篇文章:繼Android進(jìn)階三部曲之后,我的最強(qiáng)作來啦!


          作者:唐子玄鏈接:https://juejin.cn/post/7087717477246369805

          引子

          MVI 是Model-View-Intent的簡(jiǎn)稱,它們分別表示。。。。。

          我并不打算逐個(gè)字母介紹它們代表的意思。因?yàn)檫@樣一點(diǎn)也不能增進(jìn)對(duì) MVI 的理解,反而會(huì)對(duì)它的認(rèn)識(shí)蒙上厚厚的一層迷霧。

          奧古斯都在《懺悔錄》里面問我這樣一個(gè)問題:“時(shí)間到底是什么?你不問我的時(shí)候,我是知道的;你一問我,我就不知道了。” 把“時(shí)間”換成“MVI”,這個(gè)問題同樣困擾著我:“MVI 到底是什么?你不問我的時(shí)候,我是知道的;你一問我,我就不知道了。” 維特根斯坦會(huì)說,上面是一個(gè)非法的問題,源于錯(cuò)誤地使用了語言。正確的問題應(yīng)該是這樣問的:人們?cè)谑裁磮?chǎng)景下使用 MVI,他們是怎么使用 MVI 的?他們?yōu)槭裁磿?huì)使用 MVI?

          唯一可信數(shù)據(jù)源

          請(qǐng)?jiān)徫遥€是使用了這么拗口的一個(gè)名詞作為本節(jié)的開始。會(huì)試著從日常開發(fā)中熟悉的場(chǎng)景出發(fā),一步步演繹出什么叫“唯一”,什么叫“可信”。

          假唯一數(shù)據(jù)源

          假設(shè)下面這個(gè)場(chǎng)景:“一個(gè)可以發(fā)帖的社區(qū)界面,未加入社區(qū)時(shí)發(fā)帖按鈕是置灰不能點(diǎn)擊的,當(dāng)以版主身份進(jìn)入社區(qū)時(shí)發(fā)帖按鈕是紅色,當(dāng)被禁言后按鈕變黑。”

          最初我是這樣實(shí)現(xiàn)的:

          class?CommunityActivity?:?AppCompatActivity()?{
          ????private?val?postBtn:?Button
          ????override?fun?onCreate(savedInstanceState:?Bundle?)?{
          ????????super.onCreate(savedInstanceState)
          ????????val?userInfo?=?viewModel.getUserInfo()?//?從服務(wù)器獲取的用戶身份狀態(tài)信息
          ????????postBtn.apply?{...}?//?初始化發(fā)帖按鈕
          ????}
          ????override?fun?onEnter(identity:?Identity)?{
          ????????postBtn.apply?{...}?//?加入社區(qū)后刷新發(fā)帖按鈕(依賴?Identity)
          ????}
          ????override?fun?onExit()?{
          ????????postBtn.apply?{...}?//?退出社區(qū)后刷新發(fā)帖按鈕
          ????}
          ????override?fun?onMute(mute:?Mute)?{
          ????????postBtn.apply?{...}?//?禁言后刷新發(fā)帖按鈕?(依賴?Mute)
          ????}
          }

          這樣寫對(duì)于功能實(shí)現(xiàn)來說沒毛病,但維護(hù)起來會(huì)很頭痛,因?yàn)橐粋€(gè)控件的更新邏輯散落在 Activity 的各個(gè)地方,并且更新控件所依賴的數(shù)據(jù)是五花八門的,即未做到依賴單一數(shù)據(jù)源。界面簡(jiǎn)單還好,若復(fù)雜界面中有十幾個(gè)這樣的控件,Activity 的代碼沒法看。

          這樣實(shí)現(xiàn)還會(huì)增加 bug 數(shù)。假設(shè)發(fā)布按鈕置灰的樣式更改了,就需要改兩個(gè)地方,分別是初始化和退出社區(qū)的回調(diào)中。這是一個(gè)潛規(guī)則,容易出錯(cuò),當(dāng)代碼中隱匿著眾多這樣的潛規(guī)則時(shí),且之前還不是你維護(hù)的,那就等著和測(cè)試小姐姐相約在午夜吧~。

          迭代總是趕的,重構(gòu)總是被提上議程(且它一直在議程上),每次迭代只能無可奈何地按照原先的寫法,把坑挖的更深一點(diǎn)。若干次迭代后,這個(gè)模塊已經(jīng)不堪入目。在產(chǎn)品會(huì)上,它的迭代估時(shí)總是會(huì)更長(zhǎng)一些。。。

          稍好一點(diǎn)的寫法是將發(fā)布按鈕的更新邏輯封裝在一個(gè)方法內(nèi):

          class?CommunityActivity?:?AppCompatActivity()?{
          ????private?val?postBtn:?Button
          ????override?fun?onCreate(savedInstanceState:?Bundle?)?{
          ????????super.onCreate(savedInstanceState)
          ????????val?userInfo?=?viewModel.getUserInfo()
          ????????updatePostBtn()
          ????}
          ????override?fun?onEnter(identity:?Identity)?{?updatePostBtn(identity)?}
          ????override?fun?onExit()?{?updatePostBtn()?}
          ????override?fun?onMute(mute:?Mute)?{?updatePostBtn(mute)?}
          ????private?fun?updatePostBtn(mute:?Mute?,?identity:?Identity?,?userInfo:?UserInfo?){
          ????????postBtn.apply?{...}
          ????}
          }

          這為代碼維護(hù)提供了極大的便利,因?yàn)榭梢詫?shí)現(xiàn)改一處,多處聯(lián)動(dòng)。但美中不足的是,發(fā)布按鈕的更新需要依賴三個(gè)數(shù)據(jù)源,分別是禁言、身份信息、用戶信息。

          只要它們中的任何一個(gè)發(fā)生變動(dòng),都會(huì)影響到發(fā)布按鈕的顯示狀態(tài),這樣的寫法是耦合的。這使得界面展示和業(yè)務(wù)邏輯耦合在一起,若業(yè)務(wù)變化,比如新增了一種觸發(fā)按鈕樣式變更的情況,則 updatePostBtn() 得跟著改。

          按鈕其實(shí)不關(guān)心禁言、身份信息、用戶信息。它只關(guān)心應(yīng)該展示什么背景色、是否可以點(diǎn)擊。所以這些信息應(yīng)該抽象成一個(gè)按鈕的界面狀態(tài):

          data?class?PostBtnState(
          ????var?clickable:?Boolean,?//?是否可點(diǎn)擊
          ????var?backgroundColor:?Int,?//?背景色
          ????var?text:?String,?//?按鈕名稱
          )

          然后按鈕更新方法就得以解耦,簡(jiǎn)化:

          private?fun?updataPostBtn(state:?PostBtnState){
          ????postBtn.apply?{...}
          }

          Flutter 就是這樣做的,每一個(gè)控件都會(huì)對(duì)應(yīng)一個(gè)“數(shù)據(jù)”。并且數(shù)據(jù)在按鈕構(gòu)建時(shí)就和它綁定了

          關(guān)于 Flutter 的介紹及應(yīng)用,可以關(guān)注我的專欄:Flutter 關(guān)鍵概念解析 - 唐子玄的專欄

          現(xiàn)在代碼進(jìn)化成如下狀態(tài):

          class?CommunityActivity?:?AppCompatActivity()?{
          ????private?val?postBtn:?Button
          ????private?val?state:?PostBtnState
          ????override?fun?onCreate(savedInstanceState:?Bundle?)?{
          ????????super.onCreate(savedInstanceState)
          ????????val?userInfo?=?viewModel.getUserInfo()
          ????????updateState()
          ????????updatePostBtn(state)
          ????}
          ????override?fun?onEnter(identity:?Identity)?{?
          ????????updateState()
          ????????updatePostBtn(state)?
          ????}
          ????override?fun?onExit()?{?
          ????????updateState()
          ????????updatePostBtn(state)?
          ????}
          ????override?fun?onMute(mute:?Mute)?{?
          ????????updateState()
          ????????updatePostBtn(state)?
          ????}
          ????private?fun?updateState(){
          ????????state.apply?{...}
          ????}
          ????private?fun?updatePostBtn(state:?PostBtnState){
          ????????postBtn.apply?{...}
          ????}
          }

          這就完成了唯一數(shù)據(jù)源,即控件刷新所依賴的數(shù)據(jù)只有一個(gè)。(不過這里的唯一是假的,真的在下下節(jié))

          解耦 & 唯一刷新點(diǎn)

          現(xiàn)在 Activity 持有了一個(gè)按鈕對(duì)應(yīng)的狀態(tài)實(shí)例,更新操作只依賴該狀態(tài)。但更新按鈕的觸發(fā)還是散落在 Activity 中不同的地方,對(duì)按鈕來說依然有多個(gè)刷新點(diǎn),這容易出錯(cuò)。

          而且此時(shí)“界面狀態(tài)”和“界面元素”混在了一起,界面簡(jiǎn)單還好,對(duì)于復(fù)雜界面就會(huì)形成上帝 Activity。從另一個(gè)角度看,界面展示和業(yè)務(wù)邏輯耦合在一起,使得抽取共用邏輯成為不可能。

          比如另外一個(gè)版本的社區(qū),按鈕的交互邏輯完全一樣,只是樣式不同,當(dāng)前 CommunityActivity 的代碼就無法復(fù)用,只能復(fù)制粘貼,改界面。交互邏輯統(tǒng)一變動(dòng)時(shí),得改兩個(gè) Activity 的代碼。

          谷歌給出第一版的解決方案是 MVP,即將界面狀態(tài)抽離到Presenter中,實(shí)現(xiàn)了界面元素和界面狀態(tài)的分離。

          但 Presenter 有兩個(gè)缺點(diǎn):

          • Presenter 通過接口方式和 Activity 耦合,且通信接口膨脹。
          • Presenter 在界面翻轉(zhuǎn)時(shí)數(shù)據(jù)重新加載。

          所以就有了 MVP 的升級(jí)版 MVVM。數(shù)據(jù)驅(qū)動(dòng)是 MVVM 的關(guān)鍵詞,ViewModel 不再主動(dòng)調(diào)用方法去更新界面,而是主動(dòng)更新數(shù)據(jù),同時(shí)界面采用觀察數(shù)據(jù)的方式,等待被更新。

          關(guān)于 MVP 和 MVVM 的詳盡分析可以點(diǎn)擊我是怎么把業(yè)務(wù)代碼越寫越復(fù)雜的 | MVP - MVVM - Clean Architecture

          “多個(gè)刷新點(diǎn)”的問題就迎刃而解了:用一個(gè)集線器把多個(gè)更新源約束為一個(gè)更新源。 這個(gè)集線器就是帶數(shù)據(jù)驅(qū)動(dòng)的 ViewModel:

          class?CommunityViewModel?:?ViewModel()?{
          ????//?將按鈕?Model?組織成私有的可變?LiveData
          ????private?val?_postBtnLiveData?=?MutableLiveData()
          ????//?公開的不可變?LiveData
          ????val?postBtnLiveData:?LiveData?=?_postBtnLiveData
          ????//?唯一的更新按鈕?Model?的入口
          ????fun?updatePostBtnState(state:?PostBtnState)?{
          ????????_postBtnLiveData.value?=?state
          ????}
          }

          首先,ViewModel 是一個(gè)數(shù)據(jù)持有者,界面狀態(tài)被存儲(chǔ)在LiveData中,這樣就和界面元素分離了,生命周期更長(zhǎng)了,而且還能感知生命周期。其次,更新狀態(tài)有了唯一入口 updatePostBtnModel(),可變的 LiveData 被定義為私有的,只公開不可變的版本。這些都暫時(shí)保證了更新狀態(tài)的唯一數(shù)據(jù)源。

          然后界面只需觀察唯一數(shù)據(jù)源即可:

          class?CommunityActivity?:?AppCompatActivity()?{
          ????private?val?viewModel:?CommunityViewModel?by?activityViewModels()
          ????override?fun?onCreate(savedInstanceState:?Bundle?)?{
          ????????super.onCreate(savedInstanceState)
          ????????val?userInfo?=?viewModel.getUserInfo()
          ????????viewModel.updatePostBtnState()
          ????????//?觀察唯一數(shù)據(jù)源
          ????????viewModel.postBtnLiveData.observer(this)?{
          ????????????updatePostBtn(it)//?唯一刷新點(diǎn)
          ????????}
          ????}
          ????override?fun?onEnter(identity:?Identity)?{?
          ????????viewModel.updatePostBtnState(identity.toPostState())?
          ????}
          ????override?fun?onExit()?{?
          ????????viewModel.updatePostBtnState()
          ????}
          ????override?fun?onMute(mute:?Mute)?{?
          ????????viewModel.updatePostBtnState(mute.toPostState())
          ????}
          ????private?fun?updatePostBtn(state:?PostBtnState){
          ????????postBtn.apply?{...}
          ????}
          }

          fun?Mute.toPostState():?PostBtnState?{...}
          fun?Identity.toPostState():?PostBtnState?{...}

          通過數(shù)據(jù)驅(qū)動(dòng)的方式實(shí)現(xiàn)了界面展示和界面狀態(tài)分離,實(shí)現(xiàn)了解耦以及界面層的唯一刷新點(diǎn)。

          真唯一數(shù)據(jù)源

          故事還沒講完:點(diǎn)擊發(fā)帖會(huì)展示一個(gè)全屏置灰的 loading,發(fā)帖按鈕展示“發(fā)送中...”。若網(wǎng)絡(luò)不好,則常駐顯示“發(fā)帖失敗請(qǐng)檢查網(wǎng)絡(luò)”(類微信聊天列表頂部效果)。若帖子包含敏感詞,則會(huì)彈出警告。若發(fā)帖成功,則展示一個(gè)打鉤動(dòng)畫。

          按照之前的思路,很容易寫出如下的 ViewModel:

          class?CommunityViewModel?:?ViewModel()?{
          ????//?按鈕狀態(tài)數(shù)據(jù)源
          ????private?val?_postBtnLiveData?=?MutableLiveData()
          ????val?postBtnLiveData:?LiveData?=?_postBtnLiveData
          ????//?loading?數(shù)據(jù)源
          ????private?val?_postingLiveData?=?MutableLiveData<Boolean>()
          ????val?postingLiveData:?LiveData<Boolean>?=?_postingLiveData
          ????//?弱網(wǎng)數(shù)據(jù)源
          ????private?val?_poorNetworkLiveData?=?MutableLiveData<Boolean>()
          ????val?poorNetworkLiveData:?LiveData<Boolean>?=?_poorNetworkLiveData
          ????//?敏感詞數(shù)據(jù)源
          ????private?val?_badWordLiveData?=?MutableLiveData()
          ????val?badWordLiveData:?LiveData?=?_badWordLiveData
          ????//?發(fā)帖成功數(shù)據(jù)源
          ????private?val?_successLiveData?=?MutableLiveData<Boolean>()
          ????val?successLiveData:?LiveData<Boolean>?=?_successLiveData
          ????//?發(fā)帖
          ????fun?post(){
          ????????_postingLiveData.value?=?true
          ????????_postBtnLiveData.value?=?PostBtnState(text?=?"發(fā)送中..",?clickable?=?false)
          ????????viewModelScope.launch(Dispatchers.IO)?{
          ????????????val?response?=?api.post()
          ????????????//?你猜這樣寫會(huì)有什么問題嗎?
          ????????????if(response.isFailed){
          ????????????????_poorNetworkLiveData.postValue(true)
          ????????????}?else?{
          ????????????????when(response.code)?{
          ????????????????????CODE_BAD_WORD?->?_badWordLiveData.postValue("敏感詞")
          ????????????????????else?->?_successLiveData.postValue(true)
          ????????????????}
          ????????????}
          ????????????_postingLiveData.postValue(false)
          ????????????_postBtnLiveData.postValue(PostBtnModel(text?=?"發(fā)送",?clickable?=?true))
          ????????}
          ????}
          }

          對(duì)應(yīng)地,界面需要觀察新增的數(shù)據(jù)源:

          class?CommunityActivity?:?AppCompatActivity()?{
          ????private?val?viewModel:?CommunityViewModel?by?activityViewModels()
          ????override?fun?onCreate(savedInstanceState:?Bundle?)?{
          ????????super.onCreate(savedInstanceState)
          ????????//?1.?觀察按鈕狀態(tài)
          ????????viewModel.postBtnLiveData.observer(this)?{?updatePostBtn(it)?}
          ????????//?2.?觀察?loading?狀態(tài)
          ????????viewModel.postingLiveData.observer(this)?{?showLoading(it)?}
          ????????//?3.?觀察弱網(wǎng)狀態(tài)
          ????????viewModel.poorNetworkLiveData.observer(this)?{?showPoorNetwork(it)?}
          ????????//?4.?觀察敏感詞狀態(tài)
          ????????viewModel.badWordLiveData.observer(this)?{?showBadword(it)?}
          ????????//?5.?觀察發(fā)帖成功狀態(tài)
          ????????viewModel.successLiveData.observer(this)?{?showPostSuccess(it)?}
          ????}
          }

          “發(fā)帖”這個(gè)業(yè)務(wù)邏輯的整個(gè)生命周期中,用了 5 個(gè)數(shù)據(jù)源來表達(dá)。(這僅是社區(qū)界面的冰山一角,一長(zhǎng)串觀察 LiveData 的代碼是這種寫法的特點(diǎn))

          撲面而來的就是 “復(fù)雜度”,為了理解發(fā)帖的界面狀態(tài),必須得理清 5 個(gè)數(shù)據(jù)源之間的關(guān)系。凝視這樣的代碼,你無法想象出發(fā)帖界面會(huì)長(zhǎng)成什么樣子,因?yàn)橐鹚兓囊蛩靥啵恳粋€(gè)數(shù)據(jù)源的變化都會(huì)影響展示。

          緊跟其后的就是 “bug”,多數(shù)據(jù)源的復(fù)雜度除了理解困難,還容易催生 bug。上面的代碼就中招了。當(dāng)用戶第一次點(diǎn)擊發(fā)帖時(shí),正好網(wǎng)絡(luò)不佳,于是常駐的“發(fā)帖失敗請(qǐng)檢查網(wǎng)絡(luò)”顯示出來。用戶第二次點(diǎn)擊發(fā)帖成功了,于是發(fā)帖成功動(dòng)畫會(huì)和弱網(wǎng)提示一同顯示在界面上。因?yàn)槲彝浽诰W(wǎng)絡(luò)請(qǐng)求成功時(shí),把 _poorNetworkLiveData 的值置為 false。狀態(tài)太多,在所難免。。。

          這不是 MVVM 獨(dú)有的問題,MVP 也可以有類似的版本,對(duì)應(yīng)的表現(xiàn)形式在 View 層接口:

          interface?PostViewInterface{
          ????fun?showPoorNetwork()
          ????fun?showPostSuccess()
          ????fun?showPostLoading()
          ????fun?updatePostBtn()
          }

          解決方案是 “唯一數(shù)據(jù)源”:

          data?class?PostState(
          ????var?clickable:?Boolean?=?true,?//?是否可點(diǎn)擊
          ????var?backgroundColor:?Int?=?0xFF00FF,?//?背景色
          ????var?text:?String?=?"發(fā)帖",?//?按鈕名稱
          ????var?loading:?Boolean?=?false,?//?是否發(fā)帖中
          ????var?poorNetwork:?Boolean?=?"",?//?弱網(wǎng)失敗
          ????var?badWord:?String?=?"",//?敏感詞失敗
          ????var?success:?Boolean?=?false?//?是否發(fā)帖成功
          )

          將所有和發(fā)帖這個(gè)業(yè)務(wù)相關(guān)狀態(tài)都保存在一個(gè)數(shù)據(jù)類中。

          ViewModel 持有這個(gè)數(shù)據(jù):

          class?CommunityViewModel?:?ViewModel()?{
          ????//?發(fā)帖狀態(tài)數(shù)據(jù)源
          ????private?val?_postStateLiveData?=?MutableLiveData()
          ????val?postStateLiveData:?LiveData?=?_postStateLiveData
          ????
          ????fun?post(){
          ????????_postStateLiveData.value?=?PostState(
          ????????????clickable?=?false,?
          ????????????loading?=?true,?
          ????????????text?=?"發(fā)送中..."
          ????????)
          ????????viewModelScope.launch(Dispatchers.IO)?{
          ????????????val?response?=?api.post()
          ????????????if(response.isFailed){
          ????????????????_postStateLiveData.postValue(PostState(poorNetwork?=?true))
          ????????????}?else?{
          ????????????????when(response.code)?{
          ????????????????????CODE_BAD_WORD?->?_postStateLiveData.postValue(PostState(badWord?=?"敏感詞"))
          ????????????????????else?->?_postStateLiveData.postValue(PostState(success?=?true))
          ????????????????}
          ????????????}
          ????????}
          ????}
          }

          界面觀察這個(gè)數(shù)據(jù):

          class?CommunityActivity?:?AppCompatActivity()?{
          ????private?val?viewModel:?CommunityViewModel?by?activityViewModels()
          ????private?val?postBtn:?Button
          ????override?fun?onCreate(savedInstanceState:?Bundle?)?{
          ????????super.onCreate(savedInstanceState)
          ????????//?觀察發(fā)帖狀態(tài)
          ????????viewModel.postStateLiveData.observer(this)?{?updatePost(it)?}
          ????}
          ????
          ????private?fun?updatePost(state:?PostState)?{
          ????????postBtn.apply?{
          ????????????text?=?state.text
          ????????????clickable?=?state.clickable
          ????????????backgroundColor?=?state.backgroundColor
          ????????}
          ????????if(state.loading)?showLoading()
          ????????if(state.badWord.isNotEmpty())?showBadWord(state.badWord)
          ????????if(state.poorNetwork)?showPoorNetwork()
          ????????if(state.success)?showPostSuccess()
          ????}
          }

          在定義界面狀態(tài)時(shí),也可以使用繼承,這樣可以讓每個(gè)不同控件的狀態(tài)只包含在自己的子類中。簡(jiǎn)單起見,demo 只是把所有的屬性堆在一個(gè)類中。難道一個(gè)界面中所有控件的狀態(tài)都應(yīng)該用一個(gè) State 來表達(dá),以做到唯一數(shù)據(jù)源?

          理論上講是的,但這樣做會(huì)帶來麻煩。對(duì)于復(fù)雜界面來說,State 會(huì)變成上帝類。每次對(duì) State 的更新會(huì)超級(jí)費(fèi)勁。

          中庸之道是將整個(gè)界面分成若干個(gè)相互獨(dú)立的狀態(tài),獨(dú)立的意思是控件狀態(tài)不會(huì)相互影響,即控件A的任何變化不會(huì)引起控件B的任何變化,則AB相互獨(dú)立。這就很像 Clean Architecture 中的 Use Cases 了,即一整套業(yè)務(wù)邏輯可以被分割成相互獨(dú)立的用戶故事。

          關(guān)于 Clean Architecture 的詳解可以點(diǎn)擊我是怎么把業(yè)務(wù)代碼越寫越復(fù)雜的 | MVP - MVVM - Clean Architecture

          唯一數(shù)據(jù)源,就好比 y = f(x),即給定一個(gè) x(界面狀態(tài)),必定會(huì)得到唯一 y(界面展示)。若換成 y = f(a, b, c, d),這個(gè)函數(shù)就很復(fù)雜了,計(jì)算 y 值就容易出錯(cuò)。

          除了容易出錯(cuò),還不容易排查錯(cuò)誤,當(dāng) y 的值不符合預(yù)期時(shí)(界面 bug),因變量太多,遂找很難定位導(dǎo)致它錯(cuò)誤的變量,于是乎一個(gè)必現(xiàn) bug,硬生生地被變成一個(gè)偶現(xiàn) bug。(測(cè)試小姐姐又背鍋了,“你無法復(fù)現(xiàn),我怎么解?”)

          可信數(shù)據(jù)源

          這樣寫還是要出事,當(dāng)進(jìn)入社區(qū)時(shí),會(huì)根據(jù)用戶身份展示不同樣式的發(fā)帖按鈕。但在發(fā)帖的邏輯中是通過新建 PostState 實(shí)例來更新狀態(tài)的,這樣就會(huì)丟失原有按鈕樣式,所以得由上次狀態(tài)生成新狀態(tài):

          class?CommunityViewModel?:?ViewModel()?{
          ????private?val?_postStateLiveData?=?MutableLiveData()
          ????val?postStateLiveData:?LiveData?=?_postStateLiveData
          ????
          ????fun?post(){
          ????????//?獲取當(dāng)前狀態(tài)
          ????????val?currentState?=?_postStateLiveData.value
          ????????//?更改當(dāng)前狀態(tài)值
          ????????_postStateLiveData.value?=?currentState.apply?{
          ????????????clickable?=?false,?
          ????????????loading?=?true,?
          ????????????text?=?"發(fā)送中..."
          ????????}
          ????????viewModelScope.launch(Dispatchers.IO)?{
          ????????????//?獲取當(dāng)前狀態(tài)并修改之
          ????????????val?currentState?=?_postStateLiveData.value
          ????????????val?response?=?api.post()
          ????????????if(response.isFailed){
          ????????????????_postStateLiveData.postValue(currentState.apply?{?poorNetwork?=?true?})
          ????????????}?else?{
          ????????????????when(response.code)?{
          ????????????????????CODE_BAD_WORD?->?_postStateLiveData.postValue(
          ????????????????????????currentState.apply?{?badWord?=?"敏感詞"?}
          ????????????????????)
          ????????????????????else?->?_postStateLiveData.postValue(
          ????????????????????????currentState.apply?{success?=?true?}
          ????????????????????)
          ????????????????}
          ????????????}
          ????????}
          ????}
          }

          這樣寫接著出事。。。

          現(xiàn)在_postStateLiveData.value成了“共享變量”,會(huì)存在多線程并發(fā)讀寫,存在線程安全問題。

          解決辦法是COW,即 copy on write,在寫變量的時(shí)候先拷貝源變量,然后對(duì)副本寫。

          關(guān)于 COW 的詳細(xì)分析可以點(diǎn)擊面試題 | 有用過并發(fā)容器嗎?有!比如網(wǎng)絡(luò)請(qǐng)求埋點(diǎn)

          為了禁止變量的直接寫操作,遂把唯一數(shù)據(jù)源的所有字段都定義成 val

          data?class?PostState(
          ????val?clickable:?Boolean?=?true,
          ????val?backgroundColor:?Int?=?0xFF00FF,?
          ????val?text:?String?=?"發(fā)帖",?
          ????val?loading:?Boolean?=?false,
          ????val?poorNetwork:?Boolean?=?"",?
          ????val?badWord:?String?=?"",
          ????val?success:?Boolean?=?false
          )

          val 禁用了通過currentState.apply { poorNetwork = true }更新狀態(tài),而強(qiáng)迫使用下面這種方式:

          class?CommunityViewModel?:?ViewModel()?{
          ????private?val?_postStateLiveData?=?MutableLiveData()
          ????val?postStateLiveData:?LiveData?=?_postStateLiveData
          ????
          ????fun?post(){
          ????????val?currentState?=?_postStateLiveData.value
          ????????//?使用?copy()?淺拷貝
          ????????_postStateLiveData.value?=?currentState.copy?(
          ????????????clickable?=?false,?
          ????????????loading?=?true,?
          ????????????text?=?"發(fā)送中..."
          ????????)
          ????????viewModelScope.launch(Dispatchers.IO)?{
          ????????????val?currentState?=?_postStateLiveData.value
          ????????????val?response?=?api.post()
          ????????????if(response.isFailed){
          ????????????????//?使用?copy()?淺拷貝
          ????????????????_postStateLiveData.postValue(currentState.copy?(?poorNetwork?=?true?))
          ????????????}?else?{
          ????????????????when(response.code)?{
          ????????????????????//?使用?copy()?淺拷貝
          ????????????????????CODE_BAD_WORD?->?_postStateLiveData.postValue(
          ????????????????????????currentState.copy?(?badWord?=?"敏感詞"?)
          ????????????????????)
          ????????????????????//?使用?copy()?淺拷貝
          ????????????????????else?->?_postStateLiveData.postValue(
          ????????????????????????currentState.copy?(?success?=?true?)
          ????????????????????)
          ????????????????}
          ????????????}
          ????????}
          ????}
          }

          copy() data class 自帶的淺拷貝方法,若成員是集合結(jié)構(gòu),還需自行實(shí)現(xiàn)深拷貝。

          這就實(shí)現(xiàn)了 “可信數(shù)據(jù)源”,可信的意思就是它是安全的,不會(huì)發(fā)生不一致的情況。

          經(jīng)過如此這般地重構(gòu),從 “假唯一數(shù)據(jù)源” 到 “真唯一數(shù)據(jù)源” 最后到 “唯一可信數(shù)據(jù)源”。

          響應(yīng)式編程

          響應(yīng)式編程是相對(duì)于命令式編程來說的。

          命令式編程就是“叫你做一件事情,做完之后,就沒有然后了”,比如:

          val?a?=?1
          val?b?=?2
          var?c?=?a?+?b?//?3
          a?=?2
          b?=?2

          當(dāng) c = a + b 執(zhí)行完畢之后,c 的值就定格在 3,之后不管 a 和 b 的值如何變化,c 的值都不會(huì)受影響。可見命令式編程是 “一次性賦值”。

          而響應(yīng)式編程是 “持續(xù)地賦值”,將上面的例子做響應(yīng)式的改造:

          val?flowA?=?MutableStateFlow(1)
          val?flowB?=?MutableStateFlow(2)
          val?flowC?=?flowA.combine(flowB)?{?a,?b?->?a?+?b?}
          coroutineScope.launch?{
          ????flowC.collect?{
          ????????Log.v("ttaylor","c=$it")
          ????}
          }
          coroutineScope.launch?{
          ????delay(2000)
          ????flowA.emit(2)
          ????flowB.emit(2)
          }

          //?打印結(jié)果如下
          //?c=3
          //?c=4

          構(gòu)建了兩個(gè)流A,B,并指定初始值分別為1和2。使用 combine 將AB合流為C,用于求和。當(dāng)訂閱 flowC 時(shí),第一個(gè)和值在流上生成。當(dāng)流AB持續(xù)變化值之后,流C的值也會(huì)隨之而變。

          響應(yīng)式編程是一種面向數(shù)據(jù)流和變化傳播的聲明式編程范式

          “數(shù)據(jù)流”和“變化傳播”是相互解釋的:有數(shù)據(jù)流動(dòng),就意味著變化會(huì)從上游傳播到下游。變化從上游傳播到下游,就形成了數(shù)據(jù)流。

          “聲明式”意思是定義流上數(shù)據(jù)變換的邏輯并不是立刻執(zhí)行,只有當(dāng)數(shù)據(jù)流流動(dòng)起來之后才會(huì)執(zhí)行。

          單向數(shù)據(jù)流

          用戶界面是持續(xù)變化的,那是不是可以用數(shù)據(jù)流來理解界面持續(xù)的變化?

          界面變化是數(shù)據(jù)流的末端,界面消費(fèi)上游產(chǎn)生的數(shù)據(jù),并隨上游數(shù)據(jù)的變化進(jìn)行刷新。

          若用數(shù)據(jù)流來理解界面刷新,就必須抽象出兩個(gè)“數(shù)據(jù)”。

          第一個(gè)數(shù)據(jù)是從界面發(fā)出的事件(意圖),即 MVI 中 I(Intent)。在 MVP 和 MVVM 中,界面發(fā)出的事件是通過一個(gè) Presenter/ViewModel 的函數(shù)調(diào)用實(shí)現(xiàn)的,這是命令式的。為了實(shí)現(xiàn)響應(yīng)式編程,需把這個(gè)函數(shù)調(diào)用轉(zhuǎn)換成一個(gè)數(shù)據(jù),即Intent。

          第二個(gè)數(shù)據(jù)是返回給界面的狀態(tài),即 MVI 中的 M(Model),它通常被稱為狀態(tài)State,從字面就可以感覺到界面狀態(tài)是會(huì)時(shí)刻發(fā)生變化的。從界面發(fā)出的數(shù)據(jù)叫Intent,而界面接收的數(shù)據(jù)叫State,這樣整個(gè)界面的刷新流程就形成一條Unidirectional Data Flow(UDF),即單向數(shù)據(jù)流

          當(dāng)然還可以把數(shù)據(jù)流過網(wǎng)絡(luò),數(shù)據(jù)庫的線路也畫出來,這樣就會(huì)形成更大的圓環(huán),但數(shù)據(jù)流的方向還是單向的。

          當(dāng)然也可以把 ViewModel 換成 Presenter,單向數(shù)據(jù)流依然成立。圖中 ViewModel 這個(gè)位置可以是任何其他東西,只要它滿足下面三個(gè)要求:接收界面事件、存儲(chǔ)界面狀態(tài)、并以響應(yīng)式編程的方式將事件轉(zhuǎn)換為狀態(tài)并推送給界面。

          “單向數(shù)據(jù)流”“唯一數(shù)據(jù)源”的必然結(jié)果,即觸發(fā)界面刷新的只有唯一數(shù)據(jù)源。因?yàn)槿绻卸鄠€(gè)數(shù)據(jù)源的話就會(huì)變成下面的狀態(tài):

          此圖就像 demo 中第一個(gè)版本的代碼一樣,更新視圖狀態(tài)的代碼散落在 Activity 的各個(gè)地方,難以維護(hù),容易出bug。

          MVP - MVVM -【MVI】

          特意給 MVI 套上了一個(gè)括號(hào),因?yàn)槲矣X得它和前面兩者不在一個(gè)層面。它們的命名規(guī)則就非常地不一樣。

          它們中的 V 是沒有爭(zhēng)議的,代表著 View,即界面展示。

          MVP 中的 P 是界面狀態(tài)持有者,全名都給你想好了,叫 Presenter同樣地,MVVM 中的 VM 也是界面狀態(tài)持有者,不僅全名幫你想好了(ViewModel),代碼都幫你寫好了(androidx.lifecycle.ViewModel)

          Android 最新的架構(gòu)圖中,把界面展示+界面狀態(tài)歸為UI層:

          所以 MVP 和 MVVM 在定義中強(qiáng)行指定了“界面狀態(tài)持有者”這個(gè)實(shí)例。當(dāng)然它們不僅僅是狀態(tài)持有者,它們還負(fù)責(zé)生產(chǎn)界面狀態(tài),即業(yè)務(wù)邏輯生產(chǎn)界面狀態(tài)。

          但它們中的 M 的定義就非常讓人摸不著頭腦了。

          M 通常被解釋為“獲取數(shù)據(jù)的能力”,也就是說它不僅代表著數(shù)據(jù),還包括了獲取數(shù)據(jù)的方式:

          圖中的紫色部分都是 M。

          但 M 明明是 Model,模型(名詞)。Trygve Reenskaug,MVC 概念的發(fā)明者,在 1979 年就對(duì) MVC 中的 M 下過這樣的結(jié)論:

          The View observes the Model for changes

          看來我們一直用不太正確的方式使用著 M,就像“真唯一數(shù)據(jù)源”那一小節(jié)舉得反例一樣。因?yàn)樵?MVP,MVVM 是使用中,始終沒有一個(gè)真正的 M,所以才導(dǎo)致了混沌。

          MVI 中的 M 和 V 也是同樣的意思,即模型和視圖。M 和 V 在三個(gè)架構(gòu)中的語義是一模一樣的。

          但奇怪的是,MVI 中并沒有強(qiáng)調(diào)界面狀態(tài)持有者這個(gè)角色,反倒是增加了一個(gè) Intent。這就和前兩者的命名規(guī)則不太一樣了。

          這就容易產(chǎn)生迷霧了,我一開始以為下面的做法就是 MVI 全部的奧義:只要將界面狀態(tài)持有者的若干個(gè)函數(shù)調(diào)用合并成一個(gè)發(fā)送 Intent 的函數(shù),通過不同的 Intent 參數(shù)進(jìn)行區(qū)分事件。

          現(xiàn)在我明白了:Intent 就是在提示你,將原先命令式的函數(shù)調(diào)用轉(zhuǎn)換成一個(gè)事件數(shù)據(jù),用響應(yīng)式編程的方式進(jìn)行事件到狀態(tài)的變換,并且還得保證界面狀態(tài)有唯一可信數(shù)據(jù)源,這樣界面的刷新就形成了一條單向數(shù)據(jù)流。

          所以只要滿足“響應(yīng)式編程”、“單向數(shù)據(jù)流”、“唯一可信數(shù)據(jù)源”這三個(gè)原則的都可以稱之為 MVI。不管使用的是 ViewModel 還是 Presenter。MVI 關(guān)心的不是具體的界面狀態(tài)持有者,而是整個(gè)更新界面數(shù)據(jù)鏈路的流動(dòng)方式和方向。

          使用 MVP 模式產(chǎn)生 MVI 的效果的例子可以點(diǎn)擊 A Model-View-Presenter / Model-View-Intent library for modern Android apps (github.com)

          上述 demo 的演繹了如何進(jìn)行“唯一可信數(shù)據(jù)源”的改造,但還未進(jìn)行響應(yīng)式編程的改造,所以并未形成單向數(shù)據(jù)流,限于篇幅原因,會(huì)在我是怎么把業(yè)務(wù)代碼越寫越復(fù)雜的(二)| Flow 替換 LiveData 重構(gòu)數(shù)據(jù)鏈路,讓代碼更加 MVI中詳解介紹。

          迷霧

          至此,網(wǎng)上對(duì) MVI 的“迷霧”就不攻自破了,但我還是想攻一下~:

          1、Intent 是為了讓 View ViewModel 更加解耦。這一點(diǎn)連自圓其說都做不到。View 依然持有 ViewModel,解耦從何談起?反倒是現(xiàn)在不僅持有了 ViewModel,還會(huì)和一群 Intent 耦合,這明顯是增加耦合。

          2、Intent 使得 ViewViewModel 的契約更加清晰。說的沒錯(cuò),ViewViewModel 發(fā)送命令的全集能通過 Intent 一覽無余,但瀏覽ViewModel的公共方法不是有同樣的效果嗎?

          3、MVI 強(qiáng)調(diào)對(duì)UI State的集中管理,只需要訂閱一個(gè) ViewState 便可獲取頁面的所有狀態(tài),相對(duì) MVVM 減少了不少模板代碼。

          對(duì)于復(fù)雜界面只訂閱一個(gè) State 的話會(huì)痛苦不堪的(詳見“真唯一數(shù)據(jù)源”小節(jié))。MVI 整出個(gè)“唯一數(shù)據(jù)源”原來是為了減少模板代碼?完美避開了重點(diǎn)~

          4、對(duì)于 State 來說添加狀態(tài)只需要添加一個(gè)屬性,降低了ViewModel與View層的通信成本,將業(yè)務(wù)邏輯集中在ViewModel中,View層只需要訂閱狀態(tài)然后刷新即可

          難道 MVVM 中增加狀態(tài)不是添加一個(gè)屬性?難道 MVVM 中 View 層不是訂閱狀態(tài)即可?難道 MVVM 中業(yè)務(wù)邏輯不是集中在 ViewModel 中?

          5、MVVM 的痛點(diǎn)之一:當(dāng)頁面復(fù)雜時(shí),需要定義很多 State,并且需要定義可變與不可變兩種,狀態(tài)會(huì)以雙倍的速度膨脹,模板代碼較多且容易遺忘 這不是 MVVM 的痛點(diǎn),而是使用不當(dāng)造成的副作用。MVVM 中的 M 被錯(cuò)誤的理解并使用,如果它能做到唯一可信數(shù)據(jù)源,就不存在該痛點(diǎn)了。另外 MVI 中數(shù)據(jù)持有者也有可變和不可變兩個(gè)版本,這樣做是為了確保唯一可信數(shù)據(jù)源。

          使用場(chǎng)景

          如果 View 向 ViewModel 發(fā)送的是一次性命令,比如進(jìn)入靜態(tài)頁面拉取數(shù)據(jù),有沒有必要將一次性命令包裝成 Intent?看你喜歡了。其實(shí)靜態(tài)頁面不會(huì)發(fā)生持續(xù)的變化,直接一個(gè) viewModel.fetch() 就完事了。只有當(dāng) View 會(huì)持續(xù)地向 ViewModel 發(fā)起命令時(shí),Intent 就有了用武之地。

          比如用戶在直播間瘋狂點(diǎn)擊送禮按鈕,當(dāng)然可以在 View 層做點(diǎn)擊事件防抖,這樣直接在 View 對(duì) UI 事件限流有局限性。比如:若產(chǎn)品希望在前 10 次快速點(diǎn)擊時(shí)提示“你有點(diǎn)猛,請(qǐng)慢一點(diǎn)~”。超過 10 次還在狂點(diǎn),就將用戶的送禮個(gè)數(shù)緩存起來,屏幕不停刷新連送次數(shù),但送禮的請(qǐng)求會(huì)打包,每 10 次點(diǎn)擊調(diào)一次接口。

          若直接在 UI 層做限流破壞了源數(shù)據(jù),就無法在 ViewModel 層拿到完整的點(diǎn)擊事件流。按照 MVI 的套路,應(yīng)該將點(diǎn)擊事件封裝成 ClickIntent,然后無差別地推向 ViewModel 用于接受該事件的一個(gè)管道,該管道通常外面套了一層流 Flow,可以使用各種限流操作符輕松的實(shí)現(xiàn)這個(gè)效果。關(guān)于 Flow 的應(yīng)用可以點(diǎn)擊Kotlin 異步 | Flow 限流的應(yīng)用場(chǎng)景及原理

          總結(jié)

          MVI 用數(shù)據(jù)流來理解界面刷新:界面是數(shù)據(jù)流的起點(diǎn)(生產(chǎn)者)也是終點(diǎn)(消費(fèi)者),界面發(fā)出的數(shù)據(jù)叫事件,事件會(huì)用響應(yīng)式編程的方式被變換為狀態(tài),最終狀態(tài)又流向界面,界面通過消費(fèi)狀態(tài)完成刷新。在這個(gè)流動(dòng)的過程中,若保證了唯一可信數(shù)據(jù)源,就能實(shí)現(xiàn)單向數(shù)據(jù)流

          所以 MVI 和 MVP, MVVM 不同,它關(guān)心的不是具體的界面狀態(tài)持有者,而是整個(gè)更新界面的數(shù)據(jù)鏈路流動(dòng)方式和方向。

          參考

          • GoDaddy Studio’s Journey with State Management and MVI / Unidirectional Data Flow on Android — GoDaddy Engineering Blog
          • 響應(yīng)式編程 - 維基百科,自由的百科全書 (wikipedia.org)
          • MVI架構(gòu)模式?到底是誰在卷?《官方架構(gòu)指南升級(jí)》
          • MVI Architecture for Android Tutorial: Getting Started | raywenderlich.com
          • MVI Architecture - Android Tutorial for Beginners - Step By Step Guide (mindorks.com)
          • Create an Android App with MVI Architecture Pattern | maximCode (merklol.github.io)
          • Reactive Apps with Model-View-Intent - Part 1: Model
          • 在 Jetpack Compose 中使用狀態(tài) (android.com)
          • Modern Android Architecture with MVI - part 2 (amsterdamstandard.com)
          • Android MVI with Kotlin Coroutines & Flow | QuickBird Studios Blog






          為了防止失聯(lián),歡迎關(guān)注我的小號(hào)

          ??微信改了推送機(jī)制,真愛請(qǐng)星標(biāo)本公號(hào)??
          瀏覽 49
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产AV一级片 | 人人肏屄 | 日本三级乱伦视频 | 日韩精品综合在线 | 男女啪啪啪啪啪啪网站 |