In this series, I translated a series of articles of coroutines and Flow developers, aiming to understand the reasons for the current design of coroutines, flows and LiveData, find out their problems from the perspective of designers, and how to solve these problems. PLS Enjoy it.

The best of the MVVM and MVI architectural patterns are combined to provide the perfect architecture for any Android project.

If you already know the basics of architectural patterns, as well as the details of MVVM and MVI patterns, skip the basics and skip to the MVI+LiveData+ViewModel (or part 2) of this article.

Preface

There are so many architectural patterns, each with some advantages and disadvantages. All of these patterns attempt to implement the same architectural fundamentals.

  1. Separation of concerns (SoC) This is a design principle that separates a computer program into different parts so that each part solves a separate problem. A focus is anything that provides a solution to a problem. This principle is closely related to the single responsibility principle of object-oriented programming, which states that “each module, class, or function should be responsible for a single part of the functionality provided by the software, and that responsibility should be completely encapsulated by the class, module, or function.” – Wikipedia
  2. Drive UI from a model. The application should drive the user interface from a Model, preferably a persistent Model. Models are independent of view objects and application components, so they are not affected by the application lifecycle and related concerns.

Let’s also take a look at a summary of some popular architectural patterns.

⭐ MVC Architecture:

Trygve Reenskaug’s Model-View-controller architecture is the foundation of all modern architectural patterns. Let’s look at the responsibilities of each component as defined on Wikipedia.

  • The Model is responsible for managing the application’s data. It receives input from the Controller.
  • View means to present the Model in a specific format.
  • Controllers react to user input and interact with data Model objects. The Controller takes the input, validates it selectively, and passes the input to the Model.

So, the Model is responsible for representing the state, structure, and behavior of the view, and the view is just a representation of that Model.

⭐ MVVM Architecture:

In the Model-view-ViewModel architecture, the View has an instance of the ViewModel that calls the corresponding function based on the user’s input/action. At the same time, the view observes changes in the different observable properties of the ViewModel. The ViewModel processes user input and modifies the respective observable properties based on the business logic.

⭐ MVI Architecture:

In the Model-view-intent architecture, views expose view-events (user inputs/actions) and observe changes in the Model’s View state. We process view events, convert them to their respective intentions, and pass them to the Model. The Model layer uses intents and previous view state to create a new immutable view state. Therefore, this approach follows the one-way data flow principle, that is, data flows in only one direction. The View > Intent > Model > View.


In summary, the best part of the MVVM architecture is the ViewModel, but I don’t think it follows the Model concept defined in the MVC pattern, because in MVVM, the DAO (data access object) abstraction is thought of as Model, and the view observes the state changes of multiple observable properties from the ViewModel, The view is not directly driven by the Model. In addition, these multiple observable properties from the ViewModel can cause state overlap (two different states are accidentally displayed).

The MVI pattern solves this problem by adding an actual “Model “layer, which views state changes. Since the Model is immutable and the single source of truth for the current view state, state overlap does not occur.

In the architecture below, I tried to combine the best of the MVVM and MVI patterns to provide a better architecture for any Android project, and on top of that, I abstracted as much as I could by creating base classes for the View and ViewModel.

🌟 MVI + LiveData + ViewModel = ❤️ Architecture:

Before continuing, let’s reemphasize some of the basic terms in the MVI architecture.

ViewState: as the name suggests, this is the part of the Model layer that our view watches for changes in the state of the Model. ViewState should represent the current state of the view at any given time. So this class should have all the variable content that our view depends on. Every time there is any user input/action, we expose a modified copy of the class (to preserve the previously unmodified state). We can use Kotlin’s Data Class to create the Model.

data class MainViewState(val fetchStatus: FetchStatus, val newsList: List<NewsItem>)

sealed class FetchStatus {
    object Fetching : FetchStatus()
    object Fetched : FetchStatus()
    object NotFetched : FetchStatus()
}
Copy the code

ViewEffect: in Android, we have some actions that are more like fire-and-forget, such as Toast, and in those cases we can’t use ViewState because it remains state. This means that if we use ViewState to display the Toast, it will be shown again when the configuration changes or every time there is a new state, unless we reset its state via the “Toast is shown” event. If you don’t want to do this, you can use ViewEffect as it is based on SingleLiveEvent and does not need to maintain state. ViewEffect is also part of our Model, and we can create it using Kotlin’s sealed class.

sealed class MainViewEffect {
    data class ShowSnackbar(val message: String) : MainViewEffect()
    data class ShowToast(val message: String) : MainViewEffect()
}
Copy the code

ViewEvent: This represents all actions/events that the user can perform on the view. It is used to pass user input/actions to the ViewModel. We can use Kotlin’s Sealed Class to create this set of events.

sealed class MainViewEvent {
    data class NewsItemClicked(val newsItem: NewsItem) : MainViewEvent()
    object FabClicked : MainViewEvent()
    object OnSwipeRefresh : MainViewEvent()
    object FetchNews : MainViewEvent()
}
Copy the code

I recommend putting these three classes in one file, because it gives you an overview of all the actionable actions and variable content handled by the target view.

Now, let’s take a closer look at this architecture.

The diagram above probably gives you the core idea of the architecture. If not, the core idea of this architecture is that we include an actual immutable Model layer in the MVVM architecture, and our views depend on the state changes of this Model. Thus, the ViewModel must modify and expose the single Model.

To avoid redundancy and simplify the use of this architecture in multiple places, I created two abstract classes, one for our view (separate for Activity, Fragment, and custom views) and one for the ViewModel.

AacMviViewModel. A generic base class to create the ViewModel. It requires three classes STATE, EFFECT, and EVENT. We’ve seen an example of these classes above.

open class AacMviViewModel<STATE, EFFECT, EVENT>(application: Application) : AndroidViewModel(application), ViewModelContract<EVENT> { private val _viewStates: MutableLiveData<STATE> = MutableLiveData() fun viewStates(): LiveData<STATE> = _viewStates private var _viewState: STATE? = null protected var viewState: STATE get() = _viewState ? : throw UninitializedPropertyAccessException("\"viewState\" was queried before being initialized") set(value) { Log.d(TAG,  "setting viewState : $value") _viewState = value _viewStates.value = value } private val _viewEffects: SingleLiveEvent<EFFECT> = SingleLiveEvent() fun viewEffects(): SingleLiveEvent<EFFECT> = _viewEffects private var _viewEffect: EFFECT? = null protected var viewEffect: EFFECT get() = _viewEffect ? : throw UninitializedPropertyAccessException("\"viewEffect\" was queried before being initialized") set(value) { Log.d(TAG, "setting viewEffect : $value") _viewEffect = value _viewEffects.value = value } @CallSuper override fun process(viewEvent: EVENT) { Log.d(TAG, "processing viewEvent: $viewEvent") } }Copy the code

As you can see, we have viewState. The STATE and viewEffect. EFFECT and two private LiveData containers, _viewStates. MutableLiveData and _viewEffect: SingleliveEvents, which are exposed through the public functions viewStates() and viewEffects(). Please note that we are extending AndroidViewModel as it will allow us to use the application context (only) as needed. In addition, we are logging every viewEvent that we will handle.

This is how we create a ViewModel for any of our activities/fragments/views.

class MainActVM(application: Application) :
    AacMviViewModel<MainViewState, MainViewEffect, MainViewEvent>(application) {

    init {
        viewState = MainViewState(fetchStatus = FetchStatus.NotFetched, newsList = emptyList())
    }

    override fun process(viewEvent: MainViewEvent) {
        super.process(viewEvent)
    }
}
Copy the code

All we need to do is initialize viewState in the init{} block and further modify viewState using the copy() function of the data class if needed. Please do not modify the same instance of viewState, but modify its copy so that we can keep viewState immutable. If you modify the same instance of viewState, you might encounter unexpected behavior because you might change some of the properties being processed by the view.

viewState = viewState.copy(fetchStatus = FetchStatus.Fetched, newsList = result.data)
Copy the code

That’s it, and the rest (that is, _viewstates.setValue ()) is handled by the AacMviViewModel class.

AacMviActivity. A generic abstract class for creating compatible activities for this architecture.

(Refer to this repository for generic classes for Fragments and custom views: github.com/RohitSurwas…)

abstract class AacMviActivity<STATE, EFFECT, EVENT, ViewModel : AacMviViewModel<STATE, EFFECT, EVENT>> :
    AppCompatActivity() {

    abstract val viewModel: ViewModel

    private val viewStateObserver = Observer<STATE> {
        Log.d(TAG, "observed viewState : $it")
        renderViewState(it)
    }

    private val viewEffectObserver = Observer<EFFECT> {
        Log.d(TAG, "observed viewEffect : $it")
        renderViewEffect(it)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        viewModel.viewStates().observe(this, viewStateObserver)
        viewModel.viewEffects().observe(this, viewEffectObserver)
    }

    abstract fun renderViewState(viewState: STATE)

    abstract fun renderViewEffect(viewEffect: EFFECT)
}
Copy the code

It has abstract properties/functions for viewModel, renderViewState(), and renderViewEffect() that we need to implement. In addition, it has created viewStateObserver internally, viewEffectObserver LiveData-observers, And start observing viewStates() and viewEffects() that the ViewModel exposes in the Activity’s onCreate(). Therefore, this abstract Activity does all the things we have to do in each Activity. In addition, it records the viewState and viewEffect for each observation.

Now, it is very easy to create a new Activity for this architecture.

class MainActivity : AacMviActivity<MainViewState, MainViewEffect, MainViewEvent, MainActVM>() {
    override val viewModel: MainActVM by viewModels()

    override fun renderViewState(viewState: MainViewState) {
      //Handle new viewState
    }

    override fun renderViewEffect(viewEffect: MainViewEffect) {
      //Show effects
    }
}
Copy the code

And just like that, we have everything, working seamlessly, recording every action and content we’re working with. Since the Model is the single source of truth for the view’s state changes, there is no possibility of state overlap.

Note: If you’re new to this “Model-driven user interface,” you might think that we’re adding more complexity than dealing with it directly, because for some complex views, the ViesState data class has a lot of properties, because it has to have the contents of each widget, its visibility, and so on. But trust me, it will pay off because tracking down the cause of any problems/crashes will become very easy.

Original link: proandroiddev.com/best-archit…

I would like to recommend my website xuyisheng. Top/focusing on Android-Kotlin-flutter welcome you to visit