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
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
For example, we want to display a dialogue offering a
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,
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?
- Noodles from state dependencies of yuan elements
- The logic of transitions from one display state to another will be smeared across the
presenter - 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
MVI
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
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
- Usual toast show breaks us
- 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,
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:
- One entry point to the view
- We always have the current state of the screen at hand
- Even at the implementation stage, you have to think about how one state will flow
into another and what is the connection between them - Unidirectional Data Flow
Love android and never lose your motivation!
List of my inspirers
- www.youtube.com/watch?v=VsStyq4Lzxo&t=592s - Declarative UI Patterns (Google
I / O'19) - www.youtube.com/watch?v=pXw6r2kAvq8&t=2s - Architectural journey by Zsolt
Kocsi, Badoo EN
- www.youtube.com/watch?v=hBkQkjWnAjg&t=318s - How to cook a well-
done MVI for Android - www.youtube.com/watch?v=0IKHxjkgop4 - Managing State with RxJava by Jake
Wharton - hannesdorfmann.com/android/model-view-intent - Article by Hannes Doorfmann