Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Single time events in MVI architecture

Trying new architecture paradigm where presenter creates immutable state(model) stream and view just renders it.

Can't understand how to handle situations where we need to make some event for the one time only. There are couple examples.

1) Notes app. We have editText and saveButton. User clicks saveButton, some processing happens and editText should be cleared. Could you guys please describe what will be in our ViewState here and approximate logic flow?

Questions and pitfalls I see now:

  1. We subscribe to editText.textChanges() in presenter. If we will have text in our ViewState and render it each render call then we will fall into recursion because it will emit new textChange and will update state and render again.
  2. Do we need text in ViewState to restore it on orientation text or process kill/restore, looks like it works out of box here. But imagine recyclerViews scroll position. We definitely need to save it to restore. We can't restore it on each render call cause it looks weird, isn't it?
  3. If we consider such logic as side effect and will call .doOnNext{ view.clearText() } it makes sense, but do we have reference to view in canonical MVI implementation? Mosby doesn't have it as I see.
  4. It makes sense but there is possibility of view being dead in the moment of doOnNext call. MVI should help us with this but only if it's the part of ViewState, right?

2) Github app. First screen(Org): orgEditText, okButton, progressBar. Second screen (Repos): recyclerView. When user enters organization into orgEditText and clicks okButton app should make request to API and on success navigate to Repos screen on success or show toast on failure. Again could you please describe ViewState for Org screen and what logic should look like?

Questions and pitfalls I see now:

  1. We should show progressBar and disable okButton while loading. We should have like loading/content/error sealed class(lets call it ContentState) and have its instance in our ViewState. View knows how to render ContentState.loading and shows progressBar and disables okButton. Am I right?
  2. How to handle navigation then? The same questions as 1.3 and 1.4.
  3. I've seen opinions that navigation should be considered as side effect, but again 1.4.
  4. Toast - is there something in state or we consider this as side effect? Same problems.

Google suggests SingleLiveEvent solution, but it looks weird and then there should be as much LiveData<SingleLiveEvent> streams as we have such things, not really a single source of truth. Others suggest new intent generated from render func which is better but there is a possibility that some async operations will change state again and we'll get second Toast while first is showing and so on.

like image 686
pavelkorolevxyz Avatar asked Dec 12 '17 16:12

pavelkorolevxyz


People also ask

What is MVI architecture?

MVI (Model-View-Intent) streamlines the process of creating and developing applications by introducing a reactive approach. In a way, this pattern is a mixture of MVP and MVVM adapted to reactive programming. It eliminates the use of callback and significantly reduces the number of input/output methods.

What is the difference between MVVM and MVI?

But the major difference is communication back to the view. Whereas in MVVM there is usually a separate publisher for each piece of data, in MVI a single object defining the entire state of the view is published.

What is MVI in Kotlin?

MVI stands for Model-View-Intent. It is an architectural pattern that utilizes unidirectional data flow. The data circulates between Model and View only in one direction - from Model to View and from View to Model .

What is model-view-intent?

Model-View-Intent is a tool to create maintainable and scalable apps. The main advantages of MVI are: A unidirectional and cyclical data flow. A consistent state during the lifecycle of Views.


1 Answers

1) Notes App: In a perfect world: yes your ViewState would have an text changes whenever the user inserts the text and renders. Regarding the recursion: I might be wrong but I think that RxBindings somewhere offers an Observable that not only contains the changed text, but also a boolean flag if this change has been caused by user input or by programmatically setting the text. Anyways, I think you could also workaround the recursion if you check if (editText.text != viewState.text) and only set the text in case that they are different (keep in mind that you may have to use the TextWatcher callback that is triggered afterward text has been changed to start the intent, not "before is going to be changed").

With that said, on Android we are not living in a perfect world. As you have already said, the text will be restore automatically by android. Therefore it makes sense to not make text part of the ViewState.

So it sounds that in that case the ViewState is just an enum like this:

enum ViewState {
   // The user can type typing text
   IDLING,

   // The app is saving the note
   PROCESSING,

   // After having saved (PROCESSING) the note, CLEARED means, show a new empty note  
   CLEARED
}

So the initial state is IDLING. Then once the note should be saved the next emitted ViewState is PROCESSING. Once this was successful, your business logic immediately fires a CLEARED followed immediately by IDLING so at the end the user sees an empty note again and can start typing the new note.

Don't use doOnNext() for manipulating the view. ViewState is the single source of truth for the view.

Regarding RecyclerView: RecyclerView restores it's scroll position automatically, if not (you set the LayoutManager and / or adapter to late, after state has been restored). Nevertheless, if you would like to model the scroll position in ViewState, which again in a perfect world would be the best solution I guess, you should consider not update the scroll position in your ViewState on each pixel scrolled but rather do it once the user is not scrolling anymore / fling has finished.

2) Github app:

  1. We should show progressBar and disable okButton while loading. We should have like loading/content/error sealed class(lets call it ContentState) and have its instance in our ViewState. View knows how to render ContentState.loading and shows progressBar and disables okButton. Am I right?

Yes

  1. How to handle navigation then?

For me, handling this as a side effect works well: I have a class Navigator that is injected into the presenter and used in doOnNext { navigator.goToX() } . The Navigator then dispatches that to another component that can be temporarily attached / detached. So this other component is observing the Navigator for "navigation events" The reason why I would do that is that then this component is not leaking activity / fragment context. "This component" can be directly the Activity or Fragment or whatever, but I tend to have a dedicated class, let's call it Router that observes the Navigator for navigation events and does the FragmentTransactions or whatever you use in your app.

  1. Toast - is there something in state or we consider this as side effect? Same problems.

This can be handled similar to what you can do with Snackbar (see here). Toast doesn't have an API to hide a Toast. So instead of a timer you can immediately fire two ViewState one after another: the first one with the error flag set (which then causes the Toast do be displayed on screen) and the second one where you "clear" this flag. Something like this:

Observable.just( ViewState(error = true, ...), new ViewState( error = false, ... )

I hope that clarifies some things, but as always: Don't take them as a silverbullet. Do whatever works best for your app and use case. Don't be super religious, it's always a case by case decision.

like image 162
sockeqwe Avatar answered Oct 03 '22 12:10

sockeqwe