Article directory
Simple understanding of MVI
MVI is the abbreviation of Model-View-Intent, and it is also a responsive + stream processing thought architecture.
MVI's Model represents the concept of a subscribable state model, adds the concept of Intent to represent user behavior, and uses one-way data flow to control data flow and various layers of dependencies.
The single-item data flow workflow in MVI is as follows:
- User operations, data initialization operations, etc., notify the Model in the form of Intent
- Model updates State based on Intent
- View receives State changes and refreshes the UI
In ViewModel, hold and expose Intent and State. The data Model is dependent on the State and is transferred. Therefore, the VM is responsible for the state storage of Intent and State, and the relay of the data layer.
The specific performance is:
- View layer (Activity/Fragment), send I (Intent) through VM (ViewModel);
- In the VM, according to the received Intent, it undergoes certain processing and transformation, and then sends UI-State;
- The View layer observes this UI-State and displays corresponding VIew or other UI operations.
- The View layer sends Intent, observes UI-State, and holds a reference to VM.
- The VM should belong to the M layer (in my opinion, some people think that this is an intermediate layer, you can understand it anyway). The M layer includes the model definition of data source (data model), business data processing, UI-State and Intent.
This article refers to some articles. In practice, abstraction encapsulates
IUiIntent
andIUiState
base classes; abstractsBaseMviViewModel
andBaseMviUi
base classes, etc.
Define Intent and State
Define the base class interface, and the generic implementation class
Intent:
/**
* desc: UI 事件意图,或 数据获取的意图
* author: stone
* email: [email protected]
* time: 2022/11/24 12:19
*/
interface IUiIntent
class InitDataIntent: IUiIntent
State:
interface IUiState
/**
* 正在加载
*/
class LoadingState(val isShow: Boolean) : IUiState
/**
* 加载失败
*/
class LoadErrorState(val error: String) : IUiState
// 加载成功
class LoadSuccessState<T>(val subState: IUiState, val data: T?) : IUiState
// 分页加载成功
class LoadSuccessMultiDataState<T>(val subState: IUiState, val data: List<T>?, val page: Int) : IUiState
illustrate:
- LoadSuccessState(subState, ...), use subState to distinguish specific UI-State
defineBaseViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
/**
* desc:
* author: stone
* email: [email protected]
* time: 2022/11/24 13:41
*/
abstract class BaseMviViewModel : ViewModel() {
/**
* UI 状态
*/
private val _uiStateFlow by lazy {
MutableStateFlow(initUiState()) }
val uiStateFlow: StateFlow<IUiState> = _uiStateFlow.asStateFlow()
/**
* 事件意图, 点击事件、刷新等都是Intent。表示用户的主动操作
*/
private val _userIntent = MutableSharedFlow<IUiIntent>()
protected val userIntent = _userIntent.asSharedFlow()
init {
viewModelScope.launch {
userIntent.distinctUntilChanged().collect {
handleUserIntent(it)
}
}
}
abstract fun handleUserIntent(intent: IUiIntent)
protected open fun initUiState(): IUiState {
return LoadingState(true)
}
protected fun sendUiState(block: IUiState.() -> IUiState) {
_uiStateFlow.update {
block(it) } // 更新值
// _uiStateFlow.update { _uiStateFlow.value.block() } // 作用和上一句一样的
}
/**
* 分发意图
*
* 仅此一个 公开函数。供 V 调用
*/
fun dispatch(intent: IUiIntent) {
viewModelScope.launch {
_userIntent.emit(intent)
}
}
/**
* @param showLoading 是否展示Loading
* @param request 请求数据
* @param successCallback 请求成功
* @param failCallback 请求失败,处理异常逻辑
*/
protected fun <T : Any> requestDataWithFlow(
showLoading: Boolean = true,
request: Flow<BaseResult<T?>>,
successCallback: (T?) -> Unit,
failCallback: suspend (String) -> Unit = {
errMsg -> //默认异常处理,子类可以进行覆写
sendUiState {
LoadErrorState(errMsg) }
}
) {
viewModelScope.launch {
request
.onStart {
if (showLoading) {
sendUiState {
LoadingState(true) }
}
}
.flowOn(Dispatchers.Default)
.catch {
// 代码运行异常
failCallback(it.message ?: "发生了错误")
sendUiState {
LoadingState(false) }
}
.onCompletion {
sendUiState {
LoadingState(false) }
}
.flowOn(Dispatchers.Main)
.collect {
if (it.status == NET_STATUS_SUCCESS) {
successCallback(it.data)
} else {
failCallback(it.statusText ?: "服务响应发生了错误")
}
}
}
}
}
Some notes on BaseVM:
- Defines the common UI state, _uiStateFlow for private use inside the VM. uiStateFlow is an immutable type for use by the external V layer.
- _userIntent is also privately used inside the VM, and userIntent is only used by subclasses of BaseVM.
- When MutableStateFlow is initialized, it must have an initial value. Provides an overridable by subclasses
initUiState()
to set the initial State. - Provides an abstract function for handling Intents that must be overridden by subclasses
handleUserIntent()
- Provides a method called by subclasses
sendUiState(state)
to send UIState - Provide a public
dispatch(intent)
to dispatch the Intent - Provides a callable by subclasses
requestDataWithFlow()
. The parameter request is aFlow<BaseResult<T?>>
type.BaseResult<T?>
It is a combination of generic response type + actual response type. Then find one on githubFlowCallAdapter
, and convert retrofit's network request response to Flow.
ViewModel implementation class
VM implementation strongly related to View
/**
* desc:
* author: stone
* email: [email protected]
* time: 2022/11/23 14:31
*/
class TableViewModel(
private val datasource: TableDatasource = TableDatasource()
) : BaseMviViewModel() {
override fun handleUserIntent(intent: IUiIntent) {
when (intent) {
is InitDataIntent -> initLoad()
is TableIntent.SaveIntent -> saveData(intent.data)
}
}
private fun initLoad() {
viewModelScope.launch {
// 检查类型
requestDataWithFlow(request = datasource.queryCheckType(), successCallback = {
sendUiState {
LoadSuccessState(TableUiState.CheckTypeState, it) }
})
// 单位类别
requestDataWithFlow(request = datasource.queryUnitType(), successCallback = {
sendUiState {
LoadSuccessState(TableUiState.UnitTypeState(), it) }
})
// 处理措施
requestDataWithFlow(request = datasource.queryHandle(), successCallback = {
sendUiState {
LoadSuccessState(TableUiState.HandleState(), it) }
})
}
}
private fun saveData(body: PostBody) {
viewModelScope.launch {
// 添加检查信息
requestDataWithFlow(request = datasource.addCheckInfo(body), successCallback = {
sendUiState {
LoadSuccessState(TableUiState.AddSuccessState(), it?.messeage) }
})
}
}
}
sealed class TableUiState {
object CheckTypeState : IUiState // 检查表类型 需要请求网络,并在UI 上展示
class UnitTypeState : IUiState // 单位类型 需要请求网络,并在UI 上展示
class HandleState : IUiState // 处理措施 需要请求网络,并在UI 上展示
class AddSuccessState : IUiState // 保存成功 需要在UI 上展示
}
sealed class TableIntent {
class SaveIntent(val data: PostBody) : IUiIntent // 用户点击保存
}
A generic VM implementation
/**
* desc: 提供 区域查询。 这是一个可供其它 V 层,所使用的 VM。
* author: stone
* email: [email protected]
* time: 2022/11/23 14:31
*/
class AreaViewModel(private val areaDatasource: AreaDatasource = AreaDatasource()) : BaseMviViewModel() {
private val _areaIntent = MutableSharedFlow<IUiIntent>()
protected val areaIntent = _areaIntent.asSharedFlow()
override fun handleUserIntent(intent: IUiIntent) {
when (intent) {
is LoadAddressIntent -> loadArea()
}
}
private fun loadArea() {
viewModelScope.launch {
// 区域查询
requestDataWithFlow(request = areaDatasource.queryArea(), successCallback = {
sendUiState {
LoadSuccessState(AreaState(), it) }
})
}
}
}
class LoadAddressIntent : IUiIntent
class AreaState : IUiState // 区域 需要请求网络,并在UI 上展示
illustrate:
- Regarding the datasource data source, some functions that return
Flow<BaseResult<T?>>
the type
View layer implementation
class MyActivity : BaseMviActivity() {
private val mViewModel by viewModels<TableViewModel>()
private val mAreaViewModel by viewModels<AreaViewModel>()
fun oncreate() {
...
initObserver()
initBiz()
initListener()
}
private fun initObserver() {
stateFlowHandle(mViewModel.uiStateFlow) {
if (it !is LoadSuccessState<*>) return@stateFlowHandle
when (it.subState) {
is TableUiState.CheckTypeState -> showCheckTypeState(it.data as? CheckTypeBean)
is TableUiState.UnitTypeState -> showUnitTypeState(it.data as UnitTypeBean?)
is TableUiState.HandleState -> showHandleState(it.data as HandleBean?)
is TableUiState.AddSuccessState -> showToast(it.data?.toString() ?: "操作成功")
}
}
stateFlowHandle(mAreaViewModel.uiStateFlow) {
if (it !is LoadSuccessState<*>) return@stateFlowHandle
when (it.subState) {
is AreaState -> showAreaState(it.data as ArrayList<AreaBean>?)
}
}
}
private fun initBiz() {
mViewModel.dispatch(InitDataIntent())
mAreaViewModel.dispatch(LoadAddressIntent())
}
private initListener() {
btn_save.setOnClickListener {
val body = PostBody()
mViewModel.dispatch(TableIntent.SaveIntent(body))
}
}
private fun showCheckTypeState(data: CheckTypeBean?) {
// 检查表类型 }
private fun showUnitTypeState(data: UnitTypeBean?) {
// 单位类型 }
private fun showHandleState(data: HandleBean?) {
// 处理措施 }
private fun showAreaState(data: ArrayList<AreaBean>?) {
// 区域信息 }
private fun showToast(msg: String) {
}
}
illustrate
- Observe UIState, process corresponding data, and display corresponding view
- Initially through different VMs, send the corresponding Intent to obtain the data; after the data is processed normally in the VM, the corresponding UIState will be sent. Different VMs observe their associated UIState separately.
- In the button click event, a SaveIntent is sent, and the save() that saves the data is called internally by TableVM, and the call is normal, and then AddSuccessState is sent
BaseMviActivity、BaseMviFragment、BaseMviUi implementation
I. BaseMviActivity
abstract class BaseMviActivity: AppCompatActivity() {
...
private val mBaseMviUi by lazy {
BaseMviUi(this, this) }
/**
* 显示用户等待框
* @param msg 提示信息
*/
protected fun showLoadingDialog(msg: String = "请等待...") {
mBaseMviUi.showLoadingDialog(msg)
}
/**
* 隐藏等待框
*/
protected fun dismissLoadingDialog() {
mBaseMviUi.dismissLoadingDialog()
}
protected fun showToast(msg: String) {
mBaseMviUi.showToast(msg)
}
protected fun showToastLong(msg: String) {
mBaseMviUi.showToastLong(msg)
}
protected fun stateFlowHandle(flow: Flow<IUiState>, block: (state: IUiState) -> Unit) {
mBaseMviUi.stateFlowHandle(flow, block)
}
}
A AN. They are MviFragment
/**
* desc:
* author: stone
* email: [email protected]
* time: 2022/12/4 11:46
*/
abstract class BaseMviFragment: Fragment() {
private val mBaseMviUi by lazy {
BaseMviUi(requireContext(), this) }
/**
* 显示用户等待框
* @param msg 提示信息
*/
protected fun showLoadingDialog(msg: String = "请等待...") {
mBaseMviUi.showLoadingDialog(msg)
}
/**
* 隐藏等待框
*/
protected fun dismissLoadingDialog() {
mBaseMviUi.dismissLoadingDialog()
}
protected fun showToast(msg: String) {
mBaseMviUi.showToast(msg)
}
protected fun showToastLong(msg: String) {
mBaseMviUi.showToastLong(msg)
}
protected fun stateFlowHandle(flow: Flow<IUiState>, block: (state: IUiState) -> Unit) {
mBaseMviUi.stateFlowHandle(flow, block)
}
}
III. They are from MviUi
/**
* desc:
* author: stone
* email: [email protected]
* time: 2022/12/4 11:24
*/
class BaseMviUi(private val context: Context, private val lifecycleOwner: LifecycleOwner) {
private var mLoading by Weak<LoadingDialog>()
fun stateFlowHandle(flow: Flow<IUiState>, block: (state: IUiState) -> Unit) {
lifecycleOwner.lifecycleScope.launchWhenCreated {
// 开启新的协程
// repeatOnLifecycle 是一个挂起函数;低于目标生命周期状态会取消协程,内部由suspendCancellableCoroutine实现
// STATE.CREATED 低于 STARTED 状态;若因某种原因,界面重建,重走 Activity#onCreate 生命周期,就会取消该协程,直到 STARTED 状态之后,被调用者重新触发
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
flow.collect {
when (it) {
is LoadingState -> {
if (it.isShow) showLoadingDialog() else dismissLoadingDialog() }
is LoadErrorState -> showToast(it.error)
else -> block(it)
}
}
}
}
}
/**
* 显示用户等待框
* @param msg 提示信息
*/
fun showLoadingDialog(msg: String = "请等待...") {
if (mLoading?.isShowing == true) {
mLoading?.setLoadingMsg(msg)
} else {
mLoading = LoadingDialog(context)
mLoading?.setLoadingMsg(msg)
mLoading?.show()
}
}
/**
* 隐藏等待框
*/
fun dismissLoadingDialog() {
if (mLoading?.isShowing == true) {
mLoading?.dismiss()
}
}
fun showToast(msg: String) {
ToastUtil.showToast(msg)
}
fun showToastLong(msg: String) {
ToastUtil.showToastLong(msg)
}
}
some notes
- Non-generic UiState and UiIntent can be defined
sealed class
in . - For
_userIntent: MutableSharedFlow<IUiIntent>()
this type, usedistinctUntilChanged()
, to prevent duplicate data. If a State is definedobject XxState
, if more than one is sent continuouslyXxState
, only the first one will be observed and processed. Subsequent ones aredistinctUntilChanged()
affected and will not be reprocessed. For example, it is best not to define a data refresh action as aobject
singleton .
reference:
MVI architecture of Android Jetpack series
postscript
Added default error message handling (updated 2022-12-25)
In actual scenarios, sometimes it may be necessary to handle errors separately, rather than simply unified toast error messages.
For this reason, rewritten BaseMviUi#stateFlowHandle()
and added a parameterhandleError: Boolean
fun stateFlowHandle(flow: Flow<IUiState>, handleError: Boolean, block: (state: IUiState) -> Unit) {
lifecycleOwner.lifecycleScope.launchWhenCreated {
// 开启新的协程
// repeatOnLifecycle 是一个挂起函数;低于目标生命周期状态会取消协程,内部由suspendCancellableCoroutine实现
// STATE.CREATED 低于 STARTED 状态;若因某种原因,界面重建,重走 Activity#onCreate 生命周期,就会取消该协程,直到 STARTED 状态之后,被调用者重新触发
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
flow.collect {
when (it) {
is LoadingState -> {
if (it.isShow) showLoadingDialog() else dismissLoadingDialog() }
is LoadErrorState -> if (handleError) showToast(it.error) else block(it)
else -> block(it)
}
}
}
}
}
Correspondingly, this parameter is added at the calls of BaseMviFragment and BaseMviActivity.
If errors need to be handled uniformly, handleError passes true; otherwise, custom processing
call like,
// activity/fragment:
fun initObserver() {
lifecycleScope.launchWhenCreated {
stateFlowHandle(mViewModel.uiStateFlow, false) {
if (it is LoadSuccessPageDataState<*>) {
when (it.subState) {
is PackUiState.RefreshPageDataSuccess -> {
mLastPage = mPage
logi("rv show data, refresh page ${
it.page}")
mAdapter.updateData(it.data as ArrayList<MviData>?)
}
is PackUiState.LoadPageDataSuccess -> {
mLastPage = mPage
mAdapter.addAll(it.data as ArrayList<MviData>?)
logi("rv show data, add data of page ${
it.page}")
}
}
}
if (it is LoadErrorState) {
mPage = mLastPage
// if (mPage > 0) mPage--
stoneToast("show error view tips, error message is \"${
it.error}\" mPage = $mPage")
logi("show error view tips, error message is \"${
it.error}\" mPage = $mPage")
}
}
}
}
BaseMviViewModel optimization (updated 2023-03-17)
BaseMviViewModel#requestDataWithFlow()
Used internally viewModelScope.launch { }
; and in every requestDataWithFlow
place where the function is called, used again viewModelScope.launch
. The reason is that request
the parameter value is suspend
passed through the value returned by a function. Defining the parameter
type as a function type can make it unnecessary to define a coroutine when subclasses use the functionrequest
suspend
requestDataWithFlow
protected fun <T : Any> requestDataWithFlow(
showLoading: Boolean = true,
request: suspend () -> Flow<BaseResult<T?>?>,
successCallback: (T?) -> Unit,
failCallback: suspend (String) -> Unit = {
errMsg -> //默认异常处理,子类可以进行覆写
sendUiState {
LoadErrorState(errMsg) }
}
) {
viewModelScope.launch {
request() // 这里是函数调用
.onStart {
...
}
Subclass calls such as
private fun loadPageData(page: Int) {
requestDataWithFlow(request = {
repository.getListMviData(page, PAGE_SIZE) }, successCallback = {
sendUiState {
LoadSuccessPageDataState(if (page == 1) PackUiState.RefreshPageDataSuccess() else PackUiState.LoadPageDataSuccess(), it, page)
}
})
}
Demo has been added (about the network adaptation of FlowCallAdapter, it is not applied in Demo; the data source is only simulated in Demo); if necessary, you can go to Github to have a look