Encapsulation implementation of Android MVI mode (based on kotlin FLow and ViewModel)

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 IUiIntentand IUiStatebase classes; abstracts BaseMviViewModeland BaseMviUibase 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 subclasseshandleUserIntent()
  • 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 a Flow<BaseResult<T?>>type. BaseResult<T?>It is a combination of generic response type + actual response type. Then find one on github FlowCallAdapter, 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 classin .
  • For _userIntent: MutableSharedFlow<IUiIntent>()this type, use distinctUntilChanged(), to prevent duplicate data. If a State is defined object XxState, if more than one is sent continuously XxState, only the first one will be observed and processed. Subsequent ones are distinctUntilChanged()affected and will not be reprocessed. For example, it is best not to define a data refresh action as a object 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 requestDataWithFlowplace where the function is called, used again viewModelScope.launch. The reason is that requestthe parameter value is suspendpassed 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 functionrequestsuspendrequestDataWithFlow

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

Guess you like

Origin blog.csdn.net/jjwwmlp456/article/details/128069630