So why do we need MVI in mobile development?

Much has already been said about MVI, about how to properly fry and configure it. However, not much time is spent on how this method simplifies life in certain situations, compared to other approaches.



Purpose of this article



I will not delve into how MVI is technically implemented (there is more than one way and each has its own pros and cons). My main goal in a short article is to interest you to study this topic in the future and possibly encourage you to implement this pattern on your combat projects, or at least check it on your homework.



What problem can you face



My dear friend, let's imagine this situation, we have a view interface with which

to work:



interface ComplexView { 
   fun showLoading()   
   fun hideLoading()   
   fun showBanner()    
   fun hideBanner()    
   fun dataLoaded(names: List<String>)    
   fun showTakeCreditDialog()
   fun hideTakeCreditDialog()
}


At first glance, it seems that nothing is complicated. You just select a separate entity to work with this view, call it a presenter (voila, here's the MVP), and these are big problems, small difficulties, and now I will try to explain why.



And here is the presenter itself:



interface Presenter {  
   fun onLoadData(dataKey: String)    
   fun onLoadCredit()
}


It's simple, the view pulls the presenter's methods when it is necessary to load data, the presenter, in turn, has the right to pull the view in order to display the loaded information, as well as display progress. But here the problem of complexity appears - this is the absolute lack of control over the consistency of your UI, my friend.



For example, we want to display a dialogue offering a loan a favorable offer to the user and make this call from the presenter, having a link to the view interface in our hands:



view.hideTakeCreditDialog ()



But at the same time, you should not forget that when displaying a dialog, you need to hide the loading and not show it while you have a dialog on the screen. Plus, there is a method that displays a banner, which we should not call while we are displaying a dialog (or close the dialog and only after that display the banner, it all depends on the requirements). You have the following picture.



In no case should you call:



view.showBanner ()



view.showLoading ()



While the dialogue is showing. Otherwise, cats, testers and users, will cry from pain in their eyes from one glance at a banner over an important dialogue with a loan with a favorable offer.



Now let's think with you and suppose that you still wanted to show a banner (such a requirement from a business). What do you need to remember?

The fact is that when calling this method:



view.showBanner ()



Be sure to call:



view.hideLoading ()



view.hideTakeCreditDialog ()



Again, so that nothing jumped over the rest of the elements on the screen, the notorious consistency.



So the question comes up, who will hit you on the hands if you do something wrong? The answer is simple - NOBODY . In such a realization, you have absolutely no control.



Perhaps in the future you will need to add some more functionality to the view, which will also be related to what is already there. What are the disadvantages we get from this?



  1. Noodles from state dependencies of yuan elements
  2. The logic of transitions from one display state to another will be smeared across the

    presenter
  3. It is quite difficult to add a new screen state, as there is a high risk that

    you will forget to hide something before displaying a new banner or dialog


And it was you and I who analyzed the case when there are only 7 methods in the view. And even here it turned out to be a problem.



But there are such views:



interface ChatView : IView<ChatPresenter> {  
    fun setMessage(message: String)      
    fun showFullScreenProgressBar()      
    fun updateExistingMessage(model: ChatMessageModel)      
    fun hideFullScreenProgressBar()      
    fun addNewMessage(localMessage: ChatMessageModel)      
    fun showErrorFromLoading(message: String)         
    fun moveChatToStart()      
    fun containsMessage(message: ChatMessageModel): Boolean      
    fun getChatMessagesSize(): Int      fun getLastMessage(): ChatMessageModel?      
    fun updateMessageStatus(messageId: String, status: ChatMessageStatus)      
    fun setAutoLoading(autoLoadingEnabled: Boolean) 
    fun initImageInChat(needImageInChat: Boolean)      
    fun enableNavigationButton()     
    fun hideKeyboard()     
    fun scrollToFirstMessage()      
    fun setTitle(@StringRes titleRes: Int)      
    fun setVisibleSendingError(isVisible: Boolean)      
    fun removeMessage(localId: String)      
    fun setBottomPadding(hasPadding: Boolean)      
    fun initMessagesList(pageSize: Int)      
    fun showToast(@StringRes textRes: Int)     
    fun openMessageDialog(message: String)     
    fun showSuccessRating()      
    fun setRatingAvailability(isEnabled: Boolean)      
    fun showSuccessRatingWithResult(ratingValue: String)
}


It will be quite difficult to add something new here or edit the old, you have to see what is connected and how, and then start crying to work and pray that the tester will not miss anything. And in the moment of your despair he appears.



MVI





image

The whole point



The bottom line is that we have an entity called a state. Based on this state, the view will render its display. I will not go deep, so my task is to arouse your interest, so I will go straight to examples. And at the end of the article there will be a list of very useful sources if you are interested.



Let's remember our position at the beginning of the article, we have a view on which we show dialogs, banners and magic . We will describe how you and I state views



data class UIState(
       val loading: Boolean = false, 
       val names: List<String>? = null,     
       val isBannerShowing: Boolean = false,     
       val isCreditDialogShowing: Boolean = false 
)


Let's set the rule, you and I can change the view only with the help of this state, there will be such an interface:



interface ComplexView { 
   fun renderState(state: UIState)
}


Now let's set one more rule. We can contact the owner of the state (in our case it will be a presenter) only through one entry point. By sending events to him. It's a good idea to call these events actions.



sealed class UIAction {     
       class LoadNamesAction(dataKey: String) : UIAction()     
       object LoadBannerAction : UIAction()     
       object LoadCreditDialogInfo : UIAction()
 }


Just don't throw tomatoes at me for the sealed classes, they simplify life in the current situation, eliminating additional castes when processing actions in the presenter, an example will be below. The presenter interface will look like this:



interface Presenter { 
   fun processAction(action: UIAction)
}


Now let's think about how to connect the whole thing:



fun processAction(action: UiAction): UIState {     
return when (action) {         
    is UiAction.LoadNamesAction -> state.copy(
                loading = true, 
                isBannerShowing = false,          
                isCreditDialogShowing = false
    )        
    is UiAction.LoadBannerAction -> state.copy(             
                loading = false,            
                isBannerShowing = true,             
                isCreditDialogShowing = false
    )         
    is UiAction.LoadCreditDialogInfo -> state.copy(     
                loading = false,             
                isBannerShowing = false,             
                isCreditDialogShowing = true
    )     
  } 
}


If you paid attention, the flow from one display state to another now occurs in one place and it is already easier to put together a picture of how everything works in your head.



It's not super easy, but your life should be easier. Plus, in my example, this is not visible, but we can decide how to process our new state based on the previous state (there are also several notions to implement this). Not to mention the insane reusability that the guys at badoo achieved, one of their assistants in achieving this goal was MVI.



However, you should not rejoice early, everything in this world has both pros and cons, and here they are



  1. Usual toast show breaks us
  2. When you update one checkbox, the entire state will be copied again and sent to the

    view, that is, an unnecessary redrawing will occur if nothing is done about it


Suppose we want to display a normal android toast, according to the current logic, we will set a flag in our state to display our toast.



data class UIState(
       val showToast: Boolean = false, 
)


The first



We take and change the state in the presenter, set showToast = true and the simplest thing that can happen is screen rotation. Everything is destroyed, explosions and destruction of the activity is re-created, but since you are a cool developer, your state is going through this whole thing. And in the state we have a magic flag that says to display toast. Result - toast is shown twice. There are several ways to solve this problem, and they all look like crutches . Again, this will be written in the sources attached to this article.



Well, the second



This is already a problem of unnecessary rendering in the view, which will occur every time even when only one of the fields in the state changes. And this problem is solved in several sometimes not the most beautiful ways (sometimes by a dull check before complaining about a new meaning, that it is different from the previous one). But with the release of compose in a stable version, this problem will be solved, then my friend will live with you in a transformed and happy world!



Time for the pros:



  1. One entry point to the view
  2. We always have the current state of the screen at hand
  3. Even at the implementation stage, you have to think about how one state will flow

    into another and what is the connection between them
  4. Unidirectional Data Flow


Love android and never lose your motivation!



List of my inspirers








All Articles