preface
I just had a deep understanding of MVC, MVP, MVVM, and recently started to push various articles on MVI. It happened that Google released the latest Android application architecture guide, and I found that MVI is officially hot recently. This can be really qiao Qiao’s mother qiao Qiao, so started to practice, can only say that there are norms is good, really sweet
The guide helped me solve a lot of doubts I had encountered during the development process. I was no longer afraid to argue with my friends
The architecture overview
The latest architecture divides applications into three layers based on the principles of interface and data separation and data model-driven interfaces
The most important are the interface layer and data layer
Interface layer
Duties and responsibilities
- Convert application data to interface data and display it on the UI
- Provide a state container (ViewModel)
- Handling interface Events
Focus on
Business logic and interface logic
- Business logic determines how state changes are handled. Business logic usually resides in the domain layer or data layer, but never in the interface layer.
- Interface behavior logic (interface logic) determines how state changes are displayed on the screen. For example, click the button to jump, message prompt and so on
Interface logic (especially when it comes to interface types such as Context) should be in the interface, not in the ViewModel. The business logic of the same application remains the same across different mobile platforms or device types, but the interface behavior logic may differ in implementation. The interface layer pages define these types of logic
Interface Event Processing
Event classification
- Interface events: Operations that should be handled at the interface layer
- User events: Events generated when a user interacts with an application
Viewmodels are typically responsible for handling the business logic for specific user events
Event decision tree
If the operations are generated further down the interface tree, such as in RecyclerView items or custom views, the ViewModel should remain the operations that handle user events.
Interface state and state containers
The interface state
- The interface status must contain all the information displayed on the interface
- The interface state definition is immutable
Only the data source or data owner is responsible for updating the data it exposes
State of the container
Responsible for providing the state of the interface and containing the logic necessary to perform the corresponding tasks. State containers come in a variety of sizes, depending on the scope of the interface elements being managed
The ViewModel type is the recommended implementation for managing screen-level interface state with data-layer access. In addition, it automatically continues to exist after configuration changes. The ViewModel class defines the logic to be applied to the events in the application and provides the updated state as a result
Data conversion
Create a new model when the data received by the data source does not match the data required for the rest of the application
Unidirectional data flow management status
This pattern of state flowing down and events flowing up is called unidirectional data Flow (UDF)
The basic flow
-
The ViewModel stores and exposes the state to be used by the interface. Interface state is the application data transformed by the ViewModel.
-
The interface sends user event notifications to the ViewModel.
-
The ViewModel handles user actions and updates status.
-
The updated status is fed back to the interface for rendering.
-
The system repeats this for all events that cause the state to change.
Single data stream and multiple data streams
Single data stream
The biggest advantages are convenience and data consistency: users of the state have immediate access to the latest information at all times
Multiple data streams
- Unrelated data types: Some of the states required to render the interface may be completely independent of each other. In such cases, the costs of bundling these different states together may outweigh the benefits, especially if one of them is updated more frequently than the others.
- UiState Diffing: The more fields in a UiState object, the more likely it is that the data flow will be emitted because one of the fields is updated. Because the view does not have a Diffing mechanism to know if the data flow is the same for successive emitted data, each emitted result in a view update. This means that you may have to use methods such as the Flow API or distinctUntilChanged() on LiveData to alleviate the problem
The data layer
role
Manage application data and business logic
Other layers in the hierarchy must not directly access the data source; The entry point for the data layer is always the repository class
Focus on
The responsibilities of the Repository
-
Expose data to the rest of the application.
-
Centralized processing of data changes.
-
Resolve conflicts between multiple data sources.
-
Abstract the data sources for the rest of the application.
- Contains business logic.
Handle data layer errors
The interface layer is responsible for handling exceptions that occur when calling the data layer
Public API
- One-time operations: In Kotlin, the data layer exposes suspend functions; For the Java programming language, the data layer should expose functions that provide callbacks to notify the results of operations, or expose types RxJava Single, Maybe, or Completable.
- Receive notifications about changes in data over time: in Kotlin, the data layer exposes the data flow; For the Java programming language, the data layer should expose callbacks for issuing new data, or expose RxJava Observable or Flowable types
This is just a summary of what I think is important, see the app Development guide for details
practice
Architecture diagram
The sample analysis
Let’s start with a rendering
Very general a home page, it contains the following functions
-
A scrolling list that supports pull-up loading and pull-up refreshing
-
Default page (display error, network exception, no data)
-
Error message
The basic flow
The basic architecture consists of the following four parts
Define UI State and Event
/** * Data class HomePageViewState(val pageStatus: pageStatus = pageStatus.Empty, val refreshStatus: RefreshStatus = RefreshStatus.RefreshIdle, val loadMoreStatus: LoadStatus = LoadStatus.LoadMoreIdle, val data: List<Any> = emptyList()) : UIState /** * UIEffect { data class ShowToast(val message: String) : HomePageViewEffect() object ShowLoadingDialog : HomePageViewEffect() object DismissLoadingDialog : HomePageViewEffect()} / sealed class HomePageViewEvent: UIEvent {object LoadData: HomePageViewEvent() object LoadDataMore : HomePageViewEvent() object RefreshData : HomePageViewEvent() }Copy the code
- When defining interface state, you must use data class and the property must be val
The Data class provides a copy method for updating status and an equal method for diff
Val ensures that the state cannot be modified
- Status attributes must have default values
Create the state container ViewModel
@HiltViewModel class HomePageViewModel @Inject constructor (private val homeModel: HomeModel) : BaseViewModel<HomePageViewState, HomePageViewEvent, HomePageViewEffect>() { ... override fun providerInitialState(): HomePageViewState = HomePageViewState() override fun handleEvent(event: HomePageViewEvent) { when (event) { is HomePageViewEvent.LoadData -> loadData() is HomePageViewEvent.LoadDataMore -> loadDataMore() is HomePageViewEvent.RefreshData -> refreshData() } } init { loadData() } private fun loadData() { ... }... }Copy the code
The ViewModel’s responsibilities are simple
-
Provides the initial interface state
-
Updating interface Status
-
Handling user events
Update the UI State
private fun loadData() { viewModelScope.launch { combine(homeModel.loadData(0)) { array -> val bannerData = array[0] as List<BannerData> val wxData = array[1] as List<WxData> val hotProjectData = array[2] as HotProjectData currentPage = 0 SetState {copy(pageStatus = pageStatus.Success, // convert VO data = convertPoToVo(bannerData, wxData, hotProjectData), loadMoreStatus = LoadStatus.LoadMoreSuccess(hotProjectData.curPage < hotProjectData.pageCount) ) } }.onStart { setState { copy(pageStatus = PageStatus.Loading) } }.catch { setEffect { HomePageViewEffect.ShowToast(it.errorMsg) } setState { copy(pageStatus = PageStatus.Error(it)) } LogUtils.e(it.errorMsg,it.errorCode) }.collect() } }Copy the code
- If the application data obtained from the data layer is different from the data displayed on the interface, you need to create a new model
In the sample, the home page is built by a RecyerView, so the data state needs to be set as List
- The copy method eliminates the need to create a new interface state each time
- Errors in the data layer are handled by the interface layer
Flow is used here, so the interface status is directly updated in catch and a friendly reminder is given
- Prompt, navigation,loading popup single event processing to avoid “data flooding”
If event Flow is developed, SharedFlow and Channel can be used
Use the UI State
/ / processing load more viewLifecycleOwner. LifecycleScope. LaunchWhenStarted { viewModel.state.collectState(HomePageViewState::loadMoreStatus) { state -> when (state) { is LoadStatus.LoadMoreSuccess -> { if (state.hasMore) { refreshView.finishLoadMore() } else { refreshView.finishLoadMoreWithNoMoreData() } } is LoadStatus.LoadMoreLoading -> { if (! refreshView.isLoading) { refreshView.autoLoadMoreAnimationOnly() } } is LoadStatus.LoadMoreFail -> { RefreshView. FinishLoadMore (false)}}}} / / handle the drop-down refresh viewLifecycleOwner lifecycleScope. LaunchWhenStarted { viewModel.state.collectState(HomePageViewState::refreshStatus) { state -> when (state) { is RefreshStatus.RefreshSuccess -> { refreshView.finishRefresh() } is RefreshStatus.RefreshLoading -> { if (! refreshView.isRefreshing) { refreshView.autoRefreshAnimationOnly() } } is RefreshStatus.RefreshFail -> { RefreshView. FinishRefresh (false)}}}} / / processing. The default page viewLifecycleOwner lifecycleScope. LaunchWhenStarted { viewModel.state.collectState(HomePageViewState::pageStatus) { state -> refreshView.closeHeaderOrFooter() refreshView.setEnableLoadMore(false) refreshView.setEnableRefresh(false) when (state) { is PageStatus.Empty -> stateView.showEmpty() is PageStatus.Success -> { refreshView.setEnableLoadMore(true) refreshView.setEnableRefresh(true) stateView.showContent() } is PageStatus.Error -> stateView.showError() is PageStatus.Loading -> stateView.showLoading() }}} / / processing page list viewLifecycleOwner lifecycleScope. LaunchWhenStarted { viewModel.state.collectState(HomePageViewState::data) { baseBinderAdapter.setDiffNewData(it.toMutableList()) } } / / handle other hints, for example, jump a one-off event, such as viewLifecycleOwner. LifecycleScope. LaunchWhenStarted {viewModel. Effect. Collect {the when (it) {is HomePageViewEffect.ShowToast -> ToastUtils.showShort(it.message) else -> { } } } }Copy the code
- Every status update causes the interface to update, and the View doesn’t have a Diff mechanism, so we need to mitigate this by using LiveData using the Flow API or distinctUntilChanged() etc
The collectState here is a custom extension method
suspend fun <T, A> Flow.collectState(prop1: KProperty1<T, A>, action: // distinctUntilChanged() // distinctUntilChanged. CollectLatest {(A) -> Unit) {this.map {StateTuple1(prop1.get(it))} action.invoke(a) } }Copy the code
Recyclerview can be optimized through DiffUtil
adapter.apply {
addItemBinder(HomeBannerBinder())
addItemBinder(HomeWxBinder())
addItemBinder(HomeCategoryBinder(), HomeCategoryBinder.Differ())
addItemBinder(HomeTitleBinder(), HomeTitleBinder.Differ())
addItemBinder(HomeProjectBinder(), HomeProjectBinder.Differ())
}
Copy the code
- Don’t execute multiple collect in a lifecycleScope, only the first one will work because it is suspended and blocked from the end (I was stuck for a long time and wanted to give up at one point)
Here is the main part of the introduction, pit or a lot of, need to start to try
The source address
conclusion
advantages
-
Unified management of status and events
-
Use one-way data flow to ensure data consistency and enhance testability and maintainability
-
You don’t have to define LiveData twice to control access
disadvantages
- Because there is no Diff in the native View, any state refresh caused by any operation will result in the entire interface refresh, which needs to be handled by tools, which is very troublesome
- Template code code is still a lot of code
reference
Application Architecture Guide
Solve pain points in Android development with Kotlin Flow
eggs
Building your own wheels is a hassle, but are there any commercially available wheels?
Of course there is Mavericks from Airbnb, others have been using it for years, and it’s been maintained consistently until now, which is really open source
Mavericks is an Android MVI framework that is both easy to learn and powerful enough to handle the most complex processes in Airbnb, Tonal, and other large applications.
When we started Mavericks, our goal was to make building products easier, faster, and more fun. We believe that for Mavericks to succeed, developing their first app must be easy for new Android developers to learn and powerful enough to support Airbnb’s most complex screens.
Mavericks uses hundreds of Airbnb screens, including 100% new screens. It has also been adopted by countless other applications, ranging from small sample applications to more than a billion downloaded apps.
Mavericks is built on Android Jetpack and Kotlin Coroutines, so it can be seen as a complement to, rather than a departure from, Google’s standard library