TIQETS.ENGINEERING
From Presenter to View on Android: LiveData vs Rx
How the Tiqets app shares presentation logic among fragments
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
ALiveData
object has the handy callbacksonActive()
andonInactive()
, which are called when at least one observer is subscribed, or when the last observer unsubscribes. - Internal data versioning
When a view reaches itonStart()
, 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.