Android Jetpack系列的MVI架構
1.差別:LiveData < T >更改為Flow< UIState >
關于LiveData的缺點:
LiveData隻能在主線程中接收;
LiveData發送的資料是一次性交易,不能多次發送;
LiveData發送資料的線程是固定的,無法切換線程。setValue/postValue本質上是在主線程上發送的。需要來回切換線程的時候,LiveData是無能為力的。
Flow可以完美解決LiveData遇到的問題。它不僅可以多次從上遊發送資料,還可以靈活地切換線程,是以如果涉及到來回切割線程,那麼使用Flow是一個比較好的解決方案。關于Flow的詳細用法,有興趣的同學可以參考:Android Kotlin的Flow資料流。
注意:如果在項目中沒有切換到Kotlin,仍然可以使用LiveData發送資料;如果已經改用Kotlin,更推薦使用Flow發送資料。
還有一個差別。在舊的架構中,LiveData傳輸單個實體資料,即每個資料将對應一個LiveData。顯然,如果頁面邏輯複雜,會導緻ViewModel中LiveData的擴充;在新架構中,Flow發送的UIState是統一的,UIState本質上是一個資料類。不同的是UIState會統一控制視圖層相關的實體狀态,是以在ViewModel中隻需要一個流來統一互動。
2.差異,互動規範
新架構中提出了單向資料流管理頁面狀态的概念:即資料流方向是固定的,整個資料流方向是視圖->視圖模型->模型資料層->視圖模型擷取資料->根據UiState重新整理視圖層。其中,事件Events向上流,狀态UiState向下流。整個過程如下:
ViewModel存儲并公開界面使用的狀态。界面是ViewModel轉換的應用資料。
該接口将向ViewModel發送使用者事件通知。
ViewModel處理使用者操作并更新狀态。
更新後的狀态将回報到界面進行展示。
系統将對導緻狀态變化的所有事件重複上述操作。
單向資料流提高了代碼的可讀性和修改的便利性。單向資料流有以下好處:
資料一緻性。該接口隻有一個可信來源。
可測試性。源代碼是獨立的,是以可以獨立于接口進行測試。
可維護性。的狀态變化遵循一個明确定義的模式,即狀态變化是使用者事件及其資料拉取源互動的結果。
MVI實戰
定義UIState & Write視圖模型
類MViewModel : BaseViewModel() {
//Repository中間層管理所有資料源,包括本地和網絡。
private val mwan repo = wan repository()
覆寫fun initUiState(): MviState {
傳回MviState(BannerUiState。INIT,DetailUiState。初始化)
}
//請求橫幅資料
fun loadBannerData() {
requestDataWithFlow(
showLoading = true,
request = { mwan repo . request wandata(" " },
success callback = { data-->
sendUiState {
copy(banner uistate = banner uistate。成功(資料))
}
},
failCallback = {}
)
}
//請求清單資料
fun loadDetailData() {
requestDataWithFlow(
showLoading = false,
request = { mwan repo . request rank data()},
success callback = { data-->
sendUiState {
copy(detail uistate = detail uistate。成功(資料))
}
},
)
}
有趣的showToast() {
Sendleustate (mvisinglestate("觸發了一次性消費事件!"))
}
}
/**
*在這裡定義UiState包含所有與視圖層相關的實體類,可以有效避免模闆代碼(StateFlow隻需要定義一個)
*/
資料類mvi state(val bannerUiState:bannerUiState,val detailUiState: DetailUiState?):IUiState
資料類mvISingleUiState(val消息:字元串):isingleistate
密封類BannerUiState {
對象初始化:BannerUiState()
資料類成功(val模型:清單):BannerUiState()
}
密封類DetailUiState {
對象初始化:DetailUiState()
資料類成功(val detail:rank model):DetailUiState()
}
複制代碼
MviState中定義的UIState是視圖層相關的資料類,而一次性消費事件,如吐司、頁面跳轉等。,在MViState中定義。是以,用于互動的通道在上一篇文章中已經提到,這裡不再贅述。
相關接口:
接口IUiState //重複事件可以多次使用。
界面白榴石//一次性事件,不支援多次消費。
對象EmptySingleState:isingleistate
//一次性事件,不支援多次消費。
密封類LoadUiState {
資料類加載(var isShow: Boolean) : LoadUiState()
對象ShowMainView : LoadUiState()
資料類錯誤(val msg: String) : LoadUiState()
}
複制代碼
LoadUiState定義了頁面加載的幾種狀态:加載中、ShowMainView加載成功、Error加載失敗。幾種狀态的使用和切換封裝在BaseViewModel的資料請求中。具體使用請參考樣本代碼。
如果頁面請求中沒有一次性消費事件,初始化ViewModel時可以直接傳入EmptySingleState。
BaseViewModel
/**
* ViewModel基類
*
* @param UiState循環事件,視圖層可以多次接收和重新整理。
* @ param SingleUiState一次性事件,視圖層不支援吐司、導航活動等多次消費。
*/
抽象類BaseViewModel : ViewModel() {
/**
*可重複消費的事件
*/
private val _ uiStateFlow = mutable Stateflow(initUiState())
val uiStateFlow:StateFlow = _ uiStateFlow
/**
*一次性事件和一對一訂閱關系
*例如:播放吐司、導航片段等。
*頻道功能
* 1.對于一對一通信,隻有一個使用者可以接收每條消息。
* 2.第一個訂閱者可以在收集之前接收事件。
*/
private val _ sUiStateFlow:Channel = Channel()
val suis tateflow:Flow = _ suis tateflow . receiveasflow()
private val _ loadUiStateFlow:Channel = Channel()
val loadUiStateFlow:Flow = _ loadUiStateFlow . receiveasflow()
受保護的抽象fun initUiState(): UiState
受保護的fun sendUiState(副本:UiState。()-> UiState) {
_ ui Stateflow . update { _ ui Stateflow . value . copy()}
}
受保護的資金發送SingleUiState(sui state:SingleUiState){
viewModelScope.launch {
_sUiStateFlow.send(sUiState)
}
}
/**
*發送目前加載狀态:加載、錯誤、正常
*/
私人融資sendLoadUiState(loadState:LoadUiState){
viewModelScope.launch {
_loadUiStateFlow.send(加載狀态)
}
}
/**
* @param showLoading顯示加載?
* @param request請求資料
* @ param success回調請求成功。
* @param failCallback請求失敗,處理異常邏輯。
*/
受保護的fun requestDataWithFlow(
showLoading: Boolean = true,
請求:suspend () -> BaseData,
successCallback: (T) ->機關,
fail callback:suspend(String)-> Unit = { errMsg-->
//預設異常處理,子類可以重寫。
sendLoadUiState(LoadUiState。錯誤(errMsg))
},
) {
viewModelScope.launch {
//顯示加載或不加載
if (showLoading) {
sendLoadUiState(LoadUiState。正在加載(真))
}
val基本資料:基本資料
嘗試{
baseData = request()
when (baseData.state) {
請求狀态。成功-> {
sendLoadUiState(LoadUiState。ShowMainView)
baseData.data?。讓{ successCallback(it) }
}
請求狀态。錯誤-> baseData.msg?。讓{
錯誤(it)
}
}
} catch (e: Exception) {
e .消息?。讓{ failCallback(it) }
}最後{
if (showLoading) {
sendLoadUiState(LoadUiState。加載(假))
}
}
}
}
}
複制代碼
基類中StateFlow的預設值由initUiState()定義,子類實作是強制性的:
覆寫fun initUiState(): MviState {
傳回MviState(BannerUiState。INIT,DetailUiState。初始化)
}
複制代碼
這樣,當你進入頁面時,你将會監聽這些初始化事件并對它們做出響應。如果你不需要處理它們,你可以跳過它們。整個請求邏輯封裝在datawithflow中,
知識庫資料支援
定義資料庫資料類:
類别基礎資料{
@SerializedName("errorCode ")
var代碼= -1
@SerializedName("errorMsg ")
var msg:字元串?=空
var資料:T?=空
變量狀态:ReqState = ReqState。錯誤
}
枚舉類請求狀态{
成功,錯誤
}
複制代碼
基類存儲庫:
打開類庫基本存儲庫{
暫停資金執行請求(
block: suspend () -> BaseData
):BaseData {
val baseData = block.invoke()
if (baseData.code == 0) {
//正确
baseData.state = ReqState。成功
}否則{
//錯誤
baseData.state = ReqState。錯誤
}
傳回基礎資料
}
}
複制代碼
請求邏輯在基類中定義,并直接在子類中使用:
WanRepository類:BaseRepository() {
val service = retro fitutil . get service(drink service::class . Java)
挂起fun request wandata(drink id:String):base data {
傳回executeRequest { service . get banner()}
}
挂起fun requestRankData(): BaseData {
傳回executeRequest { service . getranklist()}
}
}
複制代碼
視圖層
/**
* MVI的例子
*/
類MviExampleActivity:BaseMviActivity(){
private val mBtnQuest:按id按鈕(R.id.btn_request)
private val mToolBar:按id排序的工具欄(R.id.toolbar)
private val mContentView:view group by id(r . id . cl _ content _ view)
private val mview page 2:mvpage 2 by id(r . id . MVP _ page 2)
private val MRV rank:recycler按id檢視(R.id.rv_view)
私有val mViewModel:由viewModels()建立的mViewModel
覆寫fun getLayoutId(): Int {
return r . layout . activity _ wan _ Android _ mvi
}
覆寫fun initViews() {
initToolBar(mToolBar,“Jetpack MVI”,true,true,BaseActivity。TYPE_BLOG)
mrvrank . layout manager = GridLayoutManager(this,2)
}
覆寫fun initEvents() {
注冊事件()
mBtnQuest.setOnClickListener {
MViewModel.showToast() //一次性消費
mViewModel.loadBannerData()
mViewModel.loadDetailData()
}
}
私人基金注冊事件(){
/**
* Load Loading事件Loading,Error,ShowMainView
*/
mview model . loaduistateflow . flow with life cycle 2(this){ state-->
何時(狀态){
是LoadUiState。錯誤-> mstatusviewutil . showerrorview(state . msg)
是LoadUiState。ShowMainView-> mstatusviewutil . ShowMainView()
是LoadUiState。正在加載-> mstatusviewutil . showloadingview(state . is show)
}
}
/**
*一次性消費事件
*/
mview model . suis tateflow . flowithlifecycle 2(this){ data-->
showToast(data.message)
}
mview model . uistateflow . flow with life cycle 2(this,prop 1 = mvi state::banner uistate){ state-->
何時(狀态){
是BannerUiState。初始化-> {}
是BannerUiState。成功-> {
mview page 2 . visibility = View。看得見的
mBtnQuest.visibility = View。過去的
val imgs = mutableListOf()
對于(狀态中的模型.模型){
imgs.add(model.imagePath)
}
mview page 2 . setindicatorshow(true)。set models(img)。開始()
}
}
}
mview model . uistateflow . flow with Lifecycle 2(this,life cycle。狀态。開始,
prop 1 = mvi state::detailUiState){ state-->
何時(狀态){
是DetailUiState。初始化-> {}
是DetailUiState。成功-> {
mRvRank.visibility = View。看得見的
val list = state.detail.datas
mRvRank.adapter = RankAdapter()。應用{ setModels(list) }
}
}
}
}
覆寫fun retryRequest() {
//點選螢幕再試一次
MViewModel.showToast() //一次性消費
mViewModel.loadBannerData()
mViewModel.loadDetailData()
}
/**
*顯示加載、清空、錯誤視圖等。
*/
覆寫fun getStatusOwnerView(): View?{
傳回mContentView
}
}
複制代碼
讓我們回頭看看新的架構圖。當View->ViewModel請求資料時,它通過事件傳輸,這些事件可以像ViewModel中那樣封裝:
密封的類事件:IEvent {
對象橫幅:事件()
對象詳細資訊:事件()
}
覆寫資金排程事件(事件:事件){
何時(事件){
事件。橫幅--> { loadBannerData()}
事件。詳細資訊--> { loadDetailData()}
}
複制代碼
那麼視圖層可以如下調用:
mViewModel.dispatchEvent(事件。橫幅)
mViewModel.dispatchEvent(事件。詳情)
複制代碼
但是,在示例中,當資料請求在視圖層發送時,請求沒有封裝在ViewModel中,而是通過mViewModel.loadBannerData()直接請求。個人認為,把事件封裝起來有點多餘。
摘要
與舊的MVVM建築相比,更新後的MVI建築更加規範和有限制力。具體來說: