TIQETS.ENGINEERING

From Presenter to View on Android: LiveData vs Rx

How the Tiqets app shares presentation logic among fragments

Geert Jan Alsem
Tiqets Engineering

--

The Tiqets app connects travelers with instant, last-minute, mobile tickets to top museums and attractions all over the world.

Sharing presenters between views

In Android apps, a screen is usually implemented using an Activity or Fragment. In classic architecture patterns like Model-View-Presenter, the Activity or Fragment is considered the view. The presenter contains the view’s logic, and provides the view with the data it needs to update itself.

The way a presenter passes this data to a view can be implemented in several ways. For the Tiqets Android app, initially, we simply used a PresenterView interface for this. The view implements this interface, and the presenter keeps a reference to the PresenterView. Whenever the presenter has new data ready, it can pass it to the view directly. This works as long as every view has its own presenter.

But what if you want to share a single presenter between an Activity containing multiple Fragments? The Activity and each child Fragment would be a view, so the presenter would have to keep track of multiple views, somehow.

Enter the observer pattern. Instead of the presenter keeping track of the different views itself, we make the presenter unaware of views entirely! The presenter wraps its data in an observable, and the observable will keep track of which observer needs to be notified of data updates. Each view can subscribe to the observable when needed.

Trying out LiveData

To implement the presenter’s observable in the Tiqets app, we chose to try out LiveData, an Android library provided by Google for this purpose. LiveData was created especially for Android, so it has these advantages:

  • Automatic lifecycle handling
    When a view subscribes to LiveData, it automatically makes sure updates are only given while the view is in “started” state, and the view is unsubscribed when it’s destroyed.
  • Callbacks when at least one observer is started
    A LiveData object has the handy callbacks onActive() and onInactive(), which are called when at least one observer is subscribed, or when the last observer unsubscribes.
  • Internal data versioning
    When a view reaches it onStart(), it will start receiving data from the LiveData. However, if the view has been in a started state before, and has already received the latest version of the data, then the view will not receive the data again. LiveData automatically keeps track of this for you.

Replacing LiveData with RxJava

We chose to try out LiveData because of the advantages above. But we were already using RxJava in other parts of our codebase, which means that now we had two different observable implementations in our app. So, naturally, the question arose: can’t we use RxJava for this presenter-view observable too?

Here’s how we did it. We started by replacing the LiveData object with an RxJava BehaviorSubject:

A BehaviorSubject shares some behavior with LiveData: it remembers the last item, and automatically emits it to new subscribers. But what about the three advantages of LiveData we mentioned above?

Automatic lifecycle handling

This can be achieved with a simple Kotlin extension function on Observable:

This function is pretty straightforward; it subscribes and unsubscribes from the Observable when the lifecycle owner starts or stops.

Callbacks when at least one observer is started

This can be handled with some RxJava magic. First, let’s add some callbacks to our Observable:

This doesn’t behave like LiveData yet. Here, onActive() will be called for every subscriber. We only want to know when there is at least one subscriber. Here is the full Observable implementation that makes this happen:

RxJava’s refCount() implements reference counting, meaning it keeps track of the number of subscribers. It will ensure that the upstream observable is only subscribed to when the first downstream subscription happens. Also, when the last downstream subscriber unsubscribes, it will unsubscribe from the upstream observable. This means that now onActive() and onInactive() are called exactly when we want them.

BehaviorSubject remembers its last item, but since we are using refCount(), the item will only be emitted when the first downstream subscriber subscribes. We have to remember the last item for subsequent subscribers too. To achieve this, we use replay(1), which keeps a buffer of the last 1 item.

Internal data versioning

This is now the last thing that’s missing compared to LiveData. And frankly, we don’t need it! The downside of not having this, is that when a view subscribes to the observable in onStart(), it will always receive a callback with the latest item, even if it has received this item before.

In our case, this means the view will receive the data object, and will update itself based on it. But updating the view should be fast anyway! Any small change in the data will result in an update, so one more update in onStart() is not going to make a difference, performance-wise.

Conclusion

In the end, both LiveData and RxJava can do the job. They are just tools to help you, having a clear architecture is up to you. You could even argue you don’t need a framework for this: if all you need is a simple observable, and you don’t want to depend on a library, it’s entirely feasible to implement yourself.

For our project, RxJava was the best fit for the presenter observable. It allows us to further integrate the presenter into our existing RxJava flow. For example, if the presenter observes a repository class for data, we could implement the presenter observable without using a BehaviorSubject:

The biggest downside of RxJava is that it can look like black magic to developers not experienced with it. But if your team commits to it, it becomes a very powerful tool.

Author

Geert Jan Alsem is an Android Developer at Tiqets. Besides being into software, unexpectedly he’s also a bit of a geek, and loves to watch movies and play (video and board) games.

--

--