An approach to MVP/MVI using RxJava 2 & Dagger 2 (Part 2 of 2— One chain of events)

Frederico Sabino
6 min readMay 27, 2017

--

In Part 1 I’ve mentioned some of the solutions which allow the survival of components in configuration changes. Google IO 2017 introduced an easy way to handle that problem with ViewModels. I’ve decided to update one of my old MVP projects with one goal in mind: having one simple flow of events from and to the UI.

What were the issues with my previous MVP solution?

Looking back at the project I can conclude that it has 2 main issues:

  1. I broke the chain of events. My presenters listened to events in the stream and notified the view. Since the view is a component that could be destroyed and recreated while the presenter survived, I had yet another flow to bridge that gap. I’ve used Subjects (from RxJava) and Relays (from RxRelay).
  2. Events arrived from multiple sources. The way I closed the gap was with multiple bridges. One bridge for the loading of data, one for the data, one for the errors in the stream. The problem with this solution is that the view needs to keep track of what is the current state from multiple sources (regarding network requests, data loading, etc…).

These problems are explained in greater detail in this article series from Hannes Dorfmann.

Current solution

My current solution has 3 main components for the UI/Presenter part:

  • Activity/Fragment — The View part from MVP
  • Presenter — holds the business logic and communicates with the rest of the app components (extends ViewModel).
  • Contract interface — it serves as the communication interface between the View and Presenter

We already have a presenter that survives configuration changes. But we still need to simplify the flow of events. The View produces UI events (clicks, pull to refresh, swipes, etc…) and consumes the results of these actions. In order to have one single source of events we are going to create an abstraction for the UI. Let’s call it UIModel. This UIModel is a simple container holding the necessary data for the view to display (collections, error messages, loading status). It allows two things:

  1. Given a specific UIModel we have a specific representation for it. Since these UIModels are produced by the respective Presenters we can abstract the Presenter and produce a series of UIModels in order to test how the UI renders each state.
  2. We can now have one single stream with the representation of the UI state. That means that the Activity/Fragment sends upstream a series of UI events and the downstream represents a sequence of UIModels to be rendered.

This upstream/downstream concept is something that can be represented by an ObservableTransformer<Upstream, Downstream> and it’s this concept that will help us construct a single chain of events. More on the topic can be found on this article by Dan Lew.

Designing the single stream

To merge the UIEvents on a single stream we can use the .merge() operator from Rx. Having reactive streams associated with the view events is really helpful and for that I recommend using RxBinding. You can also use Subjects to emit those events.

val uiEvents: Observable<UiEvent> = Observable.merge(
login.clicks().map{ LoginClick() },
forgotPassword.clicks().map { ForgotPasswordClick() }, ...)

As you might have noticed I’m mapping these events clicks to a representation of those actions. These representations implement an UiEvent interface (which can be empty). They are simply representations of the each UIEvent that you want to handle.

Now how can we handle each distinct UIEvent? We can use the .publish() operator in order to check the type of each event.

uiEvents.publish { events ->
when(events) {
is LoginClick -> handleLoginClickEvents(events)
is ForgotPasswordClick -> handleForgotPwClickEvents(events)
}

But we want to merge the results produced by handleLoginClickEvents and handleForgotPwClickEvents back to a single stream. We can use again the .merge() operator to achieve that!

In addition we can use a combination of the .ofType() operator in order to get rid of the explicit cast with the when block.

uiEvents.publish { events ->
Observable.merge(
events.ofType(LoginClick::class.java).flatMap { ... },
events.ofType(ForgotPasswordClick::class.java).flatMap {...}
}

We can now extract the .publish() to its own ObservableTransformer . We can even call .compose() for each type of event. It’s like creating a new operator and extracting it.

Let’s see what is happening when the stream receives a Login click event:

  1. It is matched up with the correct transformer through the .ofType() operator. Since it is a LoginClick it is going to be handled by the LoginTransformer
  2. When the LoginTransformer receives the event we immediately send an event to the stream with .startWith() . This event represents that a request is in flight.
  3. For each LoginClick event we are going to call .getAccount() on the repository. If we get a result we map it to a representation of that result. If we get an error we map to a representation of that error.

But there’s actually an issue with the code above. The transformer receives UiEvent and produces MainActivityUiModel which is not what the loginTransformer is doing!

I actually missed one line in the transformer :

The .scan() operator allows to have an initial state and produce a new state with the changes produced by the other Transformers. I start with the default MainActivityUiModel which represents the UI as it is initially shown (ie.: no network calls/no data loaded). For each result that we receive (produced by the transformers) we apply it to the current UI state using the stateReducer function. The stateReducer will simply check what’s the result and modify the UI state accordingly. Thus we get what we wanted: UI events upstream, UI models downstream.

How it all fits together with MVP/MVI

If you remember correctly, our uiEvents was an Observable.merge() of all the events that occurred in the Activity/Fragment so it must have been on the Activity/Fragment side right?

Yes it was, but we need to save that Observable in a layer that survives configuration changes or we will be back to the original problem of cancelling network calls or other background tasks associated with that Observable. So instead of having that Observable.merge() on the UI side we move it to the Presenter but we also change it to a Subject — we can’t have the same Observable.merge() on the Presenter side because we don’t have access to the views (and rightfully so)!

So again… What is in the Presenter?

The presenter has the following important components:

  • All the ObservableTransformer logic
  • The Subject<UIEvent> that the UI connects to in order to send UIEvent
  • An Observable<UIModel> exposed to the Activity/Fragment

The Subject<UIEvent> can just be something like this:

val uiEvents: PublishSubject<UiEvent> = PublishSubject.create<UiEvent>()

The Observable<UIModel> takes the events going through that Subject and composes it with our Transformer logic.

val uiModelObservable: Observable<MainActivityUiModel> = uiEvents.compose(transformer).replay(1).autoConnect()

You might ask: Since we subscribing and unsubscribing from the View which can be destroyed don’t we have the same problem of cancelling ongoing tasks? That’s where .replay(1).autoConnect() comes into play. You can think of .replay() as an Rx caching mechanism with a buffer size of 1. .autoConnect() converts it to an Observable.

Remember that since presenters extends ViewModel they are going to survive configuration changes of the device. When the view is recreated again, it simply starts observing the stream provided by the presenter (by subscribing to the uiModelObservable) which is going to have the state before the configuration change.

What about the Activity/Fragment?

On the view side we only have access to what the Contract interface exposes:

  • An Observable<UIModel>
  • A Subject<UIEvent>

And that’s all we need for the View side. We can now:

  1. Retrieve the presenter through the ViewModel framework (explained in Part 1).
  2. Send UIEvent to the presenter: presenter.uiEvents.onNext(UIEvent)
  3. Receive UIModel by subscribing to the observable exposed by the presenter: presenter.observeUIModel().subscribe() (don’t forget to call .dispose()).

What you do with each UIModel is completely up to you. It just needs to have all the information needed to update your view.

Conclusion

This architecture and data flow solve the “multiple sources” problem and it clearly separates the responsibility for each component. The View sends UI events and receives a representation of the current UI state. The Presenter receives the UI events, does the business logic around them and produces a UI model for the view to render. Additionally we can easily test each component: we can abstract the View and produce a series of UIEvents to consume by the Presenter (with unit tests) and we can also see how the view is rendered given different UIModels.

One thing that might be confusing is having both UIModel (our view state) and ViewModel (our presenter) but, naming issues aside, I consider this solution easy to read and robust. Certainly easier than having our own logic to handle configuration changes or adding an additional component lifecycle to handle it.

This was largely inspired by Hannes Dorfmann’s article series and Jake Wharton’s presentation on Managing State which you can watch below.

I appreciate any given feedback and any improvements to this solution. Thank you for reading.

--

--