Many of us in the Android community have been using RxJava to perform complex asynchronous tasks but have often fallen short of using it to create a truly reactive application.
In this post, we’ll walk through implementing a feature of an Android TODO app in a reactive manner, touching on some of the places where people typically diverge from a true reactive flow.
What is reactive programming?
Before we get started, let’s take a moment to outline what reactive programming
is. At its core, reactive programming is a style of programming wherein data is
represented as streams of information that a program can easily react to.
Manifestations of reactive programming often rely heavily on the observer
pattern; wherein some object has a list of observers that it notifies about
changes in its internal state. For RxJava, the observers in question are the
objects calling subscribe
on an Observable
.
Components outline
Before we dive into the code, it’s worth touching on the architecture the app uses. Most of the pieces won’t be anything new to the average Android developer, but the way they interact with each other may be.
There’s a ViewModel, which drives UI logic. It typically observes a
Repository which makes network or database calls and handles business logic
that is less directly related to the UI. And there is of course a View,
usually represented by an Android Activity
or Fragment
.
Diving in
To demonstrate building a feature in a reactive manner, we’ll focus in on a theoretical feature of our TODO app that allows a user to view their “top” TODO for the day.
You’ll need an object to model a todo:
data class Todo(val title: String, val dueDate: Date, val isComplete: Boolean)
You’ll need some data layer component that can fetch the top todo and save an updated todo:
interface TopTodoService {
fun topTodo(): Single<Todo>
fun saveTodo(todo: Todo): Completable
}
The data layer could be saving to a database or fetching from a network; it’s not particularly important. All that matters is that it implements the above interface.
At this point it’s worth taking a minute to recognize that just because the
TopTodoService
exposes RxJava types for its methods does not mean it’s
exposing a reactive API. A reactive
API should allow a consumer to observe or listen for some change whenever a
change is made to the internal state of whatever the API is exposing.
Here’s a (contrived) example of a non reactive repository that wraps the
TopTodoService
:
class NonReactiveRepository(private val service: TopTodoService) {
fun fetchTopTodo(): Single<Todo> {
return service.topTodo()
}
fun saveTopTodo(todo: Todo): Completable {
return service.saveTodo(todo)
}
}
Calling fetchTopTodo
on this repository should theoretically give you the top
TODO for the day. However, if you’re calling fetchTopTodo
from your
ViewModel you’ll need to make sure to re-query it whenever the top TODO is
updated. That means you need to keep track of where in your code you’re updating
that TODO, and you need to make sure you’re always remembering to update the UI
whenever those changes are made.
In a truly reactive system, you’re observing streams of data, so you know you’re always operating on the most up to date information the system can provide.
You know this isn’t a truly reactive API because fetchTopTodo
returns a
Single
. You can’t model a stream of information with a Single
, so that’s a
good hint that you’ll need to do a bit more work to expose a genuinely reactive
API.
Creating a reactive API
Instead, here’s a truly reactive wrapper around TopTodoService
:
class TopTodoRepository(private val service: TopTodoService) {
private val disposables = CompositeDisposable()
private val topTodoSubject = BehaviorSubject.create<Todo>()
val topTodoObservable = topTodoSubject.hide()
init {
fetchTopTodo()
}
private fun fetchTopTodo() {
service
.topTodo()
.subscribeOn(Schedulers.io())
.subscribe(topTodoSubject::onNext)
.addTo(disposables)
}
fun completeTodo(todo: Todo) {
val completedTodo = todo.copy(isComplete = true)
service.saveTodo(completedTodo)
.subscribeOn(Schedulers.io())
.subscribe { topTodoSubject.onNext(completedTodo) }
.addTo(disposables)
}
}
The above repository works such that whenever completeTodo
is called, a new
value is piped through the topTodoSubject
. It will also immediately send a new
Todo
through topTodoSubject
as soon as TopTodoRepository
is created. As a
result, any class that uses the TopTodoRepository
can subscribe to the
topTodoObservable
and will receive a steady stream of whatever the top Todo
is, even as the top Todo
changes!
By exposing all of the repositories state in a single observable, your
ViewModel becomes entirely a function of the TopTodoRepository
‘s
observable:
class TopTodoViewModel(val repo: TopTodoRepository): ViewModel() {
private val disposables = CompositeDisposable()
val dueDateLiveData = MutableLiveData<Date>()
val titleLiveData = MutableLiveData<String>()
val isCompleteLiveData = MutableLiveData<Boolean>()
init {
val todoObservable = repo.topTodoObservable
.subscribeOn(Schedulers.io())
.share()
todoObservable
.map { it.copy(dueDate = Date()) }
.map { it.dueDate }
.subscribe(dueDateLiveData::postValue)
.addTo(disposables)
todoObservable
.map { it.title }
.subscribe(titleLiveData::postValue)
.addTo(disposables)
todoObservable
.map { it.isComplete }
.subscribe(isCompleteLiveData::postValue)
.addTo(disposables)
}
}
Critically, the above will keep the UI up to date as the internal state of the repo changes.
Responding to user actions
Consuming information is one thing, but the above code doesn’t deal with
changing the state of the world at all. Imagine we have some view on the screen
that when clicked should mark a Todo
as completed.
Ultimately it’s the View that will invoke this action, so we need some way for the View to communicate that action with the ViewModel.
At first pass the answer seems obvious. Simply add a public function to the ViewModel that calls into the repo:
fun todoCompleted() {
repo.completeTodo(???)
}
However, at this point you don’t know what the Todo
is that should be updated.
You have a few options:
- You could send the initial
Todo
over to the View when it’s first loaded so the View can then bubble it back up to the ViewModel when the completion action is taken. - You can subscribe to the
todoObservable
in the ViewModel and save the currently displayedTodo
and use that whentodoCompleted
is called - You could flip the table around and expose whatever click listener calls
todoCompleted
as an observable and pass that into the view model.
The first option violates the single responsibility principle. Ideally the View has no insight into the business objects.
The second option, keeping a local copy of the Todo
, solves the single
responsibility violation introduced in the first option, but it has the downside
of introducing mutable state to your view model. You’d have to make sure that
your local copy of the Todo
object is always up to date and is never updated
incorrectly.
The third option, treating a view like an observable, is absolutely a viable approach to take. However, it too comes with several downsides:
- If you’re going to make an observable out of a click listener, you need to be aware of the threading limitations that come with interacting with views. Specifically, you need to make sure that the piece adding the click listener does so on the main thread.
- Since most Android views can only have one associated click listener, you’ll
only be able to subscribe to the observable once before you start overwriting
other subscriptions. You can avoid this by using the
share
operator, but it’s a hidden trap to fall into.
There is also a surprise fourth option: stop treating click listeners any different than the other dynamic properties you’re updating!
Just like you’d send a String
to be displayed in a TextView
via a LiveData
stream, you can send a ClickListener
function to the View via a LiveData
object.
val finishedTaskClickListenerLiveData = MutableLiveData<() -> Unit>()
todoObservable
.subscribe { todo ->
finishedTaskClickListenerLiveData.postValue {
repo.completeTodo(todo)
}
}
.addTo(disposables)
Now the finishedTaskClickListenerLiveData
will deliver a function that calls
repo.completeTodo
with the correct Todo
. There’s no special logic to handle
click listeners; it’s just another mutable property of a view that you deliver
via LiveData
.
The View
Building out the View is now straightforward. All it involves is subscribing
to the LiveData
s that the ViewModel exposes:
class TopTodoActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val repo = TopTodoRepository(FakeService())
val viewModel = TopTodoViewModel(repo)
viewModel.dueDateLiveData.observe(this, Observer {
dueDate.text = it?.toString()
})
viewModel.titleLiveData.observe(this, Observer {
titleText.text = it
})
viewModel.isCompleteLiveData.observe(this, Observer {
isFinished.text = if (it!!) { "Done!" } else { "Not Done!" }
})
viewModel.finishedTaskClickListenerLiveData.observe(this, Observer { click ->
finishedTaskButton.setOnClickListener { click?.invoke() }
})
}
}
The only novel piece of code here is the last observing block:
viewModel.finishedTaskClickListenerLiveData.observe(this, Observer { click ->
finishedTaskButton.setOnClickListener { click?.invoke() }
})
The finishedTaskClickListenerLiveData
delivers a LiveData<() -> Unit>
, which
the View then observes. Since setOnClickListener
expects a function of
type (View) -> Unit
, you’ll have to create a new OnClickListener
and
manually invoke the delivered function. This can be easily cleaned up with a
simple extension method, but that’s out of scope for this blog post.
Wrapping up
Utilizing a reactive architecture can simplify your application’s logic and guarantee that your users are seeing the most up to date information. But it take’s some effort to ensure that you’re getting the most out of your app’s RxJava and LiveData usage. In general, make sure to:
- Verify that any data that can change is represented as an
Observable<Data>
rather than aSingle
. - Aim to have your business logic components expose observables that represent their state rather than passing those observables back up through to the view or viewmodel.
- Try and construct your view models such that they could theoretically be
written entirely in the
init
block. That’s not to say that you should just put everything in theinit
block, but if you’re finding that you don’t have the information you need to model the inputs and outputs of your view model in theinit
block you may be missing a reactive piece in your system.