Chapters

Hide chapters

Reactive Programming with Kotlin

Second Edition · Android 10 · Kotlin 1.3 · Android Studio 4.0

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

Section II: Operators & Best Practices

Section 2: 7 chapters
Show chapters Hide chapters

22. Building a Complete RxJava App
Written by Alex Sullivan

Throughout this book, you’ve learned about the many facets of RxJava. Reactive programming is a deep subject; its adoption often leads to architectures very different from the ones you’ve grown used to. The way you model events and data flow in RxJava is crucial for proper behavior in your apps, as well as protecting against issues in future iterations of the product.

To conclude this book, you’ll architect and code a small RxJava application. The goal is not to use Rx “at all costs,” but rather to make design decisions that lead to a clean architecture with stable, predictable and modular behavior. The application is simple by design, to clearly present ideas you can use to architect your own applications.

This chapter is as much about RxJava as it is about the importance of a well-chosen architecture that suits your needs. RxJava is a great tool that helps your application run like a well-tuned engine, but it doesn’t spare you from thinking about and designing your application architecture.

Introducing QuickTodo

Serving as the modern equivalent of the “Hello, world” program, a “To-Do” application is an ideal candidate to expose the inner structure of an Rx application.

In the previous chapters, you’ve used ViewModel, LiveData, and Room from the Jetpack suite of libraries to build your apps.

In this chapter, you’ll wrap them all together and create a modularized architecture that allows you to separate your data layer from your presentation layer.

Architecting the application

One particularly important goal of your app is to achieve a clean separation between the user interface, the business logic of your application and the services the app contains to help the business logic run. To that end, you really need a clean model where each component is clearly identified.

First, some terminology for the architecture you are going to implement:

  • View model: Defines the business logic and data used by the view to show a particular view.
  • Repository: A provider of content from some store. A repository could fetch objects from a database or from a network. Either way, it’s abstracted from the view model so that it can concentrate on view logic.
  • Model: The most basic data store in the application. View models and repositories both manipulate and exchange models.

You’ve used view models throughout the book. Repositories are a new concept and another good fit for reactive programming. Their purpose is to expose data and functionality using Observable and the other reactive types as much as possible, so as to create a global model in which components connect together as reactively as possible.

For your QuickTodo application, the requirements are relatively modest. You’ll architect it correctly nonetheless, so you have a solid foundation for future growth. It’s also an architecture you’ll be able to reuse in other applications.

The basic items you need are:

  • A TaskItem model that describes an individual task.
  • A TaskRepository repository that provides task creation, update, deletion, storage and search.
  • A storage medium; you’ll use a Room database here and, of course, its Rx adapters.

As you’ve seen in the previous chapters, the view model exposes the business logic and the model data to the activity. Just like in previous chapters you’ll use LiveData objects to emit updates to the activity. Doing this ensures that the activity is kept up to date even after a configuration change.

LiveData vs. Observables

You may be wondering why you’ve been using LiveData instead of just exposing Observables from your view model. There are a few reasons to use both utilities:

  1. LiveData has the benefit of being directly tied to Android lifecycle events. That makes it a fantastic candidate for use inside an Activity or Fragment, because it means you don’t need to worry about disposing of any subscriptions as lifecycle events fire.

  2. You don’t need to worry about when you’re subscribing or observing your lifecycles. If you were to use Observables instead of LiveData objects, you’d have to make sure that your activity or fragment is only subscribing after the UI is setup, since otherwise you may run into an exception when you try to reference a non-existent UI component. That’s not a large problem for Activities, but it can be painful in Fragments where the lifecycle is more complex.

  3. LiveData, while powerful and helpful, has nowhere near the power of an RxJava Observable. The power of RxJava should be clear at this point. While both Observable and LiveData implement the observer pattern, Observables have a huge array of operators and utilities that they can use to create complex streams. LiveData is a much simpler construct.

Task model

Now that you’ve got the basic theory down, it’s time to put these concepts into practice. Open the starter project in Android Studio. Note that the project won’t build at first; this chapter’s starter project is less fleshed out than previous chapters to give you an opportunity to go through all different sections of a reactive application.

Without further ado, you’ll start by adding in the task model. Populate TaskItem.kt as follows:

@Entity
data class TaskItem(
  @PrimaryKey(autoGenerate = true) val id: Int?,
  val text: String,
  val addedDate: Date,
  val isDone: Boolean
)

Your task model is simple and is marked as a Room entity. A task is defined as having text (the task contents), a creation date and a checked flag. You’ll use the creation date to sort tasks in the tasks list.

Task data access object

Now that you have a TaskItem that is a Room entity, you’ll need a DAO or data access object to store and fetch TaskItem model objects from the database.

Open TaskDao.kt. You’ll notice that it’s already marked as a @Dao interface, but, other than that, it’s completely empty.

For this project, your DAO will need four methods:

  1. A method to insert a single TaskItem for when the user creates a new task and saves it.
  2. A method to insert a list of TaskItems to allow the app to pre-populate the database with several default TaskItems.
  3. A method to fetch an individual TaskItem from the database by a given id.
  4. And finally a method to observe all of the TaskItems currently in the database.

Add the following to the body of the TaskDao interface, importing the io.reactivex.* reactive classes (not the io.reactivex.java3.*), and AndroidX database annotations. At the time of this writing, Room does not work with RxJava3:

// 1
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertTask(taskItem: TaskItem): Single<Long>

// 2
@Insert
fun insertTasks(tasks: List<TaskItem>): Completable

// 3
@Query("SELECT * FROM TaskItem WHERE id = :id")
fun fetchTask(id: Int): Maybe<TaskItem>

// 4
@Query("SELECT * FROM TaskItem ORDER BY addedDate")
fun taskStream(): Observable<List<TaskItem>>

Here’s a breakdown of the above DAO implementation:

  1. When inserting a single task, you’re specifying a return type of Single<Long>, where the Long represents the number of updated rows, which we’d expect to always be one.

  2. When inserting multiple tasks, you’re setting the return type to be Completable. This method is used to add default tasks to the database, so you’re less concerned with the number of updated rows.

  3. When fetching an individual TaskItem from the database, you’re setting the return type to be Maybe<TaskItem>. Since there’s no guarantee that a task exists for any given id, a Maybe makes the most sense here. See Chapter 21, “RxJava & Jetpack,” to learn how Maybes work with Room.

  4. Finally, the return type for taskStream() is naturally Observable<List<TaskItem>>. Every time a new TaskItem is inserted or updated taskStream() should emit a new List<TaskItem> representing all of the task items in the database.

Next, you’ll seed the database with some sample todo items. Open TaskRoomDatabase.kt and replace the existing TODO with the following:

val taskDatabase = database ?: return
taskDatabase.taskDao().insertTasks(
  listOf(
    TaskItem(null, "Chapter 1: Hello, RxJava!", Date(), false),
    TaskItem(null, "Chapter 2: Observables", Date(), false),
    TaskItem(null, "Chapter 3: Subjects", Date(), false),
    TaskItem(null,
      "Chapter 4: Observables and Subjects in practice", Date(),
      false),
    TaskItem(null, "Chapter 5: Filtering operators", Date(),
    false)
  )
)
.toV3Completable()
.subscribeOn(Schedulers.io())
.subscribe()

The above uses the taskDao interface on an existing Room database to insert five sample todo TaskItems.

Since these todos haven’t been added to the database yet, you’re passing null in for the ID field. Passing in null works for now, but when the rest of your app is creating TaskItems and inserting them into the database you’ll want a more elegant form of ID to pass around.

Since the Room library is still using Rxjava2, you’re using the toV3Completable extension method to transition from a v2 RxJava Completable to a v3 version. You’ll use v3 for the rest of the app, so keep that in mind when adding your imports.

You should now be able to build and run the application. You’ll be greeted with a truly unique and novel screen:

Beautiful! A true work of art. The brush strokes in particular are truly riveting.

As gorgeous as it is, it doesn’t do a whole lot right now. It’s time to build a repository to actually store your data.

Task repository

The task repository is responsible for creating, updating and fetching task items from the store. Since you’re a responsible developer, you’ll create a TaskRepository interface to hide the specifics of how tasks are accessed.

For this app, you’ll only add a single RoomTaskRepository. By hiding the specifics behind an interface you could add a NetworkTaskRepository in the future if the app starts communicating with an API.

First, create the interface. This is what you’ll expose to the users of the repository. Open TaskRepository.kt and add the interface definition:

interface TaskRepository {
  fun insertTask(taskItem: TaskItem): Single<Long>
  fun getTask(id: Int): Maybe<TaskItem>
  fun taskStream(): Observable<List<TaskItem>>
}

Make sure you’re importing the RxJava3 versions of Observable, Maybe, and Single. This is a basic interface providing the fundamental services to create, update and read query tasks. Nothing fancy here. The most important detail is that the repository exposes all data operations as reactive elements. Even the functions which create, delete and update tasks return a Single or a Maybe.

Now open RoomTaskRepository.kt and see that the RoomTaskRepository class implements the TaskRepository interface.

For now RoomTaskRepository is going to delegate most of its methods to the TaskDatabase object passed into it. Add the following to the RoomTaskRepository to make it properly implement TaskRepository:

// 1
companion object {
  const val INVALID_ID = -1
}

override fun insertTask(taskItem: TaskItem): Single<Long> {
  TODO()
}
// 2
override fun getTask(id: Int): Maybe<TaskItem> {
  return database.taskDao().fetchTask(id).toV3Maybe()
}

override fun taskStream(): Observable<List<TaskItem>> {
  // 3
  return database.taskDao().taskStream().toV3Observable()
}

There are three things to note about this implementation:

  1. You’ve introduced a new INVALID_ID constant to avoid having other classes pass in null for the TaskItems ID. You want to avoid null values as much as possible, since they’re laborious to work around and error-prone.
  2. You’re delegating getTask() and taskStream() to the TaskDatabase object passed into RoomTaskRepository
  3. You’re again using the toV3X() methods to transition from RxJava2 to RxJava3 types. One of the benefits of having this repository layer is that you can hide the fact that Room uses an old version of RxJava from the rest of your application.

All that’s left is to fill out insertTask().

insertTask() is a bit unique because it will have two distinct use cases:

  1. A user could use it to create a new TaskItem in the database.
  2. A user could use it to update an existing TaskItem.

Add the following to replace the body of insertTask():

val validIdTask =
  if (taskItem.id == RoomTaskRepository.INVALID_ID) {
    taskItem.copy(id = null)
  } else {
    taskItem
  }
return database.taskDao().insertTask(validIdTask).toV3Single()

The above code checks to see if the task items id is equal to the INVALID_ID constant you defined earlier. If it is, it creates a new copy of the task item with a null id. Otherwise, it uses the passed through ID.

If you didn’t do this check then whenever a user attempted to insert a new task item into the database you would instead overwrite whatever task was added with an id of INVALID_ID.

Todo list view

Your repository is up and running now, this means you can start working through listing all of the todos. The list of todos is going to be segmented into two sections. First, you’ll have the todos that still need to be done. Below that, you’ll have the todos that are already finished. There will be a header list item before the unfinished todos and another header list item before the finished todos to visually separate out the lists.

If you’ve worked with RecyclerView enough, you’ll know that creating lists that have different types of data can be challenging. You’re often forced to have two separate lists of items and to do frustrating math to figure out which item you should be displaying at a given time. To avoid this headache, it’s often advantageous to make a new data type specifically to work with your adapter.

Open TodoListItem.kt. Replace the existing TodoListItem class with the following:

sealed class TodoListItem(val viewType: Int) {
  object DueTasks : TodoListItem(0)
  object DoneTasks : TodoListItem(1)
  data class TaskListItem(val task: TaskItem) : TodoListItem(2)
}

TodoListItem takes in a viewType which you’ll use in a moment in an adapter. You’ve created three different types of TodoListItems:

  1. A DueTasks object which represents the first header grouping together the tasks that are yet to be done.
  2. A DoneTasks object which represents the second header grouping together the tasks that have been finished.
  3. And a TaskListItem data class that represents one of the tasks in either the done or due sections of the list.

By creating a common data type abstraction on top of all the different visual treatments, you’ll want, in the list you’re allowing, the adapter to still only operate on one list of items. Instead of operating on a List<TaskItem> it’ll instead work on a List<TodoListItem>.

Using a ListAdapter

Open TodoAdapter.kt and look at the class header:

class TodoAdapter : ListAdapter<TodoListItem,
  RecyclerView.ViewHolder>(TodoDiffUtil())

There are two interesting pieces, here:

  1. TodoAdapter extends ListAdapter rather than RecyclerView.Adapter. ListAdapter is an extremely handy class in the RecyclerView library that will compute a diff between the current list and a new list you provide. It will then dispatch Adapter.notifyItem calls depending on the differences between the two lists. Using this smart diffing tool allows you to focus on submitting new lists rather than considering which specific items have changed.
  2. You’re supplying a TodoDiffUtil object to the ListAdapter superclass. ListAdapter isn’t magical. It still needs a way to tell that two list items aren’t the same item. It uses the DiffUtil.ItemCallback class to differentiate between the two lists.

You’ll need to update the TodoDiffUtil class to properly dispatch updates to the adapter.

Open TodoDiffUtil.kt. There are two methods that you’ll need to implement to get proper diffing:

  1. areItemsTheSame(), which checks to see if two items represent the same item.
  2. areContentsTheSame(), which checks to see if two items have the same contents.

The distinction may seem strange, but it makes sense after some thought. You can have an item in two lists that represent the same item but have different contents. One could have been before a user marked the task as done and one could have been after. The items still represent the same item but their contents are different, because the user took some action on the item.

Add the following to replace the body of areItemsTheSame():

return when (oldItem) {
  // 1
  TodoListItem.DueTasks -> newItem is TodoListItem.DueTasks
  TodoListItem.DoneTasks -> newItem is TodoListItem.DoneTasks
  // 2
  is TodoListItem.TaskListItem -> {
    if (newItem !is TodoListItem.TaskListItem) return false
    oldItem.task.id == newItem.task.id
  }
}

Here’s a breakdown of the above:

  1. Two DueTasks items are always equal since they’re represented as objects. The same is true for two DoneTasks objects.
  2. Two TaskListItems are the same if they have the same id. Even if they have different contents they represent the same item.

The areContentsTheSame method is much simpler. Add the following to replace the body of areContentsTheSame():

return oldItem == newItem

Two items have the same contents if they’re equal to each other. Data classes keeps this short.

Navigate back to TodoAdapter. Now that the TodoDiffUtil has been fleshed out you can finish the rest of the adapter.

First, replace the body of getItemViewType() with the following:

return getItem(position).viewType

ListAdapter exposes getItem() to fetch an item from its list of items. When using ListAdapter you don’t manage the list of items yourself, which is why getItem() is necessary.

Since TodoListItem has a viewType field, getting the viewType for a given position is trivial.

Now, add the following to the body of onBindViewHolder():

val item = getItem(position)
val resources = holder.itemView.context.resources
when (item) {
  TodoListItem.DueTasks -> {
    holder.itemView.section_title.text =
        resources.getString(R.string.due_tasks)
  }
  TodoListItem.DoneTasks -> {
    holder.itemView.section_title.text =
        resources.getString(R.string.done_tasks)
  }
  is TodoListItem.TaskListItem -> {
    holder.itemView.task_title.text = item.task.text
    holder.itemView.task_done.isChecked = item.task.isDone
  }
}

The above code uses the Kotlin Android Extensions to reference views on the TodoSectionViewHolder and sets them according to what type of item getItem() returned.

Note again that by using a sealed class to represent the items in the list applying different visual treatments to different items is trivial. No messy logic indexing into multiple lists!

Reactive programming is made much easier by having components and widgets that are receptive to having their state reset at any time. If ListAdapter didn’t exist then you would instead be forced to carry around state in your ViewModel to differentiate between the two lists. It would also require an expanded API contract between your View and ViewModel, since the ViewModel would need to convey a lot more information about which list items should be created, updated, moved, or deleted.

Setting up the list view model

You’ve got your repository ready to go. You’ve got an adapter up and running. It’s time to build out the list view model to start seeing some todos.

Open TodoListActivity.kt. At the bottom of onCreate() you’ll notice the following block of code:

val viewModel = buildViewModel {
  TodoListViewModel()
}

buildViewModel() is a convenience function to abstract away the boilerplate of instantiating a ViewModel.

TodoListViewModel will be in charge of querying for todos so it will require a few dependencies. Replace the body of the lambda provided to buildViewModel() with the following, knowing that you’ll have a compiler error until you get to work on TodoListViewModel:

val repository =
    RoomTaskRepository(TaskRoomDatabase.fetchDatabase(this))
TodoListViewModel(repository, Schedulers.io())

TodoListViewModel will take in two dependencies: a TaskRepository and a background scheduler. You’re passing in the background scheduler so that you can control what scheduler your Rx operators run on, which will make unit testing the view model much easier.

Open TodoListViewModel.kt and update the class header to accept the new dependencies:

class TodoListViewModel(
  repository: TaskRepository,
  backgroundScheduler: Scheduler
) : ViewModel()

As mentioned earlier, you’ll expose LiveData objects for the activity to consume. Add the following instance variable to TodoListViewModel:

val listItemsLiveData = MutableLiveData<List<TodoListItem>>()

Now that everything’s in place, you can finally query your TaskRepository for some TaskItems!

Create an init block in TodoListViewModel and add the following to it:

repository
  // 1
  .taskStream()
  // 2
  .map { tasks -> tasks.map { TodoListItem.TaskListItem(it) } }
  // 3
  .map { listItems ->
    val finishedTasks = listItems.filter { it.task.isDone }
    val todoTasks = listItems - finishedTasks
    listOf(
      TodoListItem.DueTasks,
      *todoTasks.toTypedArray(),
      TodoListItem.DoneTasks,
      *finishedTasks.toTypedArray()
    )
  }
  // 4
  .subscribeOn(backgroundScheduler)
  .subscribe(listItemsLiveData::postValue)
  .addTo(disposables)

That’s a beefy chunk of code, so here’s a breakdown:

  1. You’re calling taskStream() on TaskRepository. taskStream() should return an Observable<List<TaskItem>> that emits a new List<TaskItem> every time the database is updated.
  2. You’re then using map() to transform that List<TaskItem> into a List<TodoListItem>. Don’t be confused by the map() within a map() here - the second map() is being called on the List<TaskItem> and is a method exposed on Lists by the Kotlin standard library.
  3. You’re then taking the List<TodoListItem> returned by the previous map() and adding in the two section header list items. Before you do that you need to separate out the tasks that have been finished and the tasks that haven’t. To that end you’re using filter(), again in the Kotlin standard library.
  4. Finally, you’re subscribing on a background scheduler and forwarding the results onto the listItemsLiveData object. You’re using a method reference to avoid some boilerplate.

The last step before you can run the app and see some progress is to observe the listItemsLiveData in the TodoListActivity. Add the following below the viewModel declaration in TodoListActivity.kt:

viewModel.listItemsLiveData
  .observe(this, Observer(adapter::submitList))

Note: Make sure to import androidx.lifecycle.Observer and not its Rx equivalent when adding this line!

Again, you’re using a method reference to avoid some boilerplate. You’re using submitList() to update the list of items in your adapter. submitList() is a method exposed by ListAdapter that takes care of doing all of the diffing logic between the old list and the new one.

Run the app. You should see a screen that looks like this:

However, toggling the individual tasks does nothing. You’ll change that next.

Replacing callbacks with observables

Since the individual list items each have a switch on them, you’ll need to communicate with the TodoAdapter whenever the user toggles a switch. Typically, you’d do that using a callback. However, you can always rework a callback into an Observable to preserve the reactive chain.

Open TodoAdapter.kt and add the following instance variables to the top of the class:

private val taskClickSubject = PublishSubject.create<TaskItem>()
private val taskToggledSubject =
  PublishSubject.create<Pair<TaskItem, Boolean>>()
val taskClickStream = taskClickSubject.hide()
val taskToggledStream = taskToggledSubject.hide()

The user is going to be able to take two separate actions on a list item:

  1. They can toggle an individual task to mark it as completed.
  2. They can click a task and edit some of the details.

To capture those two different actions, you’ve created two private PublishSubjects which you’ll use shortly. You’re also exposing corresponding Observables. It’s important to hide the details of your subjects from outside consumers so they don’t have the opportunity to push unexpected objects into your stream.

Scroll down to the bottom of onBindViewHolder() and add the following in the TodoListItem.TaskListItem block of the when statement:

holder.itemView.task_done.setOnClickListener {
  taskToggledSubject.onNext(
     item.task to holder.itemView.task_done.isChecked)
}
holder.itemView.setOnClickListener {
  taskClickSubject.onNext(item.task)
}

Whenever someone clicks the task_done Switch you’re calling onNext() on the taskToggleSubject with a pair of objects. The first object is the TaskItem the user took an action on. The second object is a Boolean indicating that the task has been marked as finished or not.

Additionally, whenever a user clicks on anything in the adapter row you’re calling onNext() on the taskClickSubject, passing through the TaskItem that was selected.

Utilizing Subjects and Observables is a common approach to reworking a callback based API into a reactive one. Don’t be afraid to use this strategy liberally.

Updating the TodoListViewModel

Now you need to notify your view model when the above Observables fire. Ideally you’d be able to pass the newly created Observables into your TodoListViewModel. Unfortunately, if you were to do that, when the user rotated the screen your view model would stop receiving callbacks, since the adapter would create new PublishSubjects which your view model would not know about.

Instead, you’re going to subscribe to the Observables in your TodoListActivity and forward the information through to the TodoListViewModel, just like you did in previous chapters.

Start off by adding two new PublishSubject values to TodoListViewModel:

private val taskClicks = PublishSubject.create<TaskItem>()
private val taskDoneToggles =
 PublishSubject.create<Pair<TaskItem, Boolean>>()

taskClicks represents a user clicking on a task in the list, while taskDoneToggles represents toggling a task on and off.

Next up add two methods to forward events into your two new PublishSubjects:

fun taskClicked(taskItem: TaskItem) =
  taskClicks.onNext(taskItem)

fun taskDoneToggled(taskItem: TaskItem, on: Boolean) =
  taskDoneToggles.onNext(Pair(taskItem, on))

These use onNext() to notify each PublishSubject of the event.

Next, add a CompositeDisposable to the top of TodoListActivity:

private val disposables = CompositeDisposable()

This will allow you to responsibly dispose of your observable chains.

Last but not least, add the following below the call building the view model:

adapter.taskClickStream.subscribe {
  viewModel.taskClicked(it)
}.addTo(disposables)

adapter.taskToggledStream.subscribe {
  viewModel.taskDoneToggled(it.first, it.second)
}.addTo(disposables)

You’re subscribing to both the taskClickStream and taskToggledStream Observables you defined in your adapter and forwarding the result into the TodoListViewModel.

When the user toggles a task as done, you want to call into the TaskRepository to update the state of the task item that was toggled. That will then trigger the taskStream that you subscribed to at the top of the init block, which will keep your UI up to date.

Add the following to the bottom of the TodoListViewModel‘s’ init block:

// 1
taskDoneToggles
  // 2
  .flatMapSingle { newItemPair ->
    // 3
    repository
      .insertTask(
          newItemPair.first.copy(isDone = newItemPair.second))
      .subscribeOn(backgroundScheduler)
    }
  .subscribe()
  .addTo(disposables)

Here’s a section by section break down of the above:

  1. You’re using the taskDoneToggles Observable you added into the view model earlier to listen for a user tapping the switch one any of the task items.
  2. You’re then using flatMapSingle() to transform this stream from an Observable<Pair<TaskItem, Boolean>> into a Single<Long>. You need to use flatMapSingle() because flatMap() expects the lambda you pass it to produce an Observable, but TaskRepository.insertTask() produces a Single.
  3. You’re using the aforementioned insertTask() to save the updated version of the TaskItem the user toggled. The emitted Pair contains both the TaskItem to update and whether that item has been marked as completed or not, which you’re using to create a new TaskItem to save off in the database.

Run the app and toggle a few tasks back and forth. You should see them move fluently between the done and due sections.

Editing tasks

When a user clicks on one of the task items the app should take them to another screen where they can edit the details of that task.

Open TodoListViewModel.kt and add another LiveData object:

val showEditTaskLiveData = MutableLiveData<Int>()

The activity will observe showEditTaskLiveData to be informed when it should open an activity to edit a task. The Int passed into the activity will represent the id of the task item to be edited.

Note: You could make your TaskItem implement Parcelable and then pass it as an extra in an Intent. However, it’s generally considered best practice to pass around the smallest piece of data you can between activities so you don’t end up exceeding the maximum amount of information an Intent can carry. In this scenario, you can easily fetch a TaskItem from its id.

Add the following to the bottom of the init block:

// 1
taskClicks
  // 2
  .throttleFirst(1, TimeUnit.SECONDS)
  // 3
  .subscribe {
    val id = it.id ?: RoomTaskRepository.INVALID_ID
    showEditTaskLiveData.postValue(id)
  }
  .addTo(disposables)

From top to bottom, the above code:

  1. Subscribes to the taskClicks Observable you passed in earlier. Remember that taskClicks emits every time a user taps one of the rows in the list of tasks.
  2. Uses throttleFirst() to ensure that only one tap goes through. throttleFirst() is a new operator that works similarly to debounce(). Instead of delaying the mission of the Observable until the time unit has passed, throttleFirst() immediately emits an item and then skips any new items that come within the designated time period. By using throttleFirst, you can ensure that multiple activities aren’t started by quickly tapping the task.
  3. Fetches the id from the task, defaulting to the INVALID_ID if the id on the task item is null. Finally, you’re posting the id to showEditTaskLiveData, indicating that the activity should launch the edit task activity.

The above flow looks beautiful, but if you were to add unit tests for it you’d run into an ugly surprise: you have to wait a full second every time you want to emulate a task being clicked!

To control that timing information, it’s best practice to pass in a dedicated Scheduler to use for timing tasks, that way you can advance time manually using a TestScheduler in your unit tests.

Update the TodoListViewModel class header to accept another Scheduler as a parameter:

class TodoListViewModel(
  repository: TaskRepository,
  backgroundScheduler: Scheduler,
  computationScheduler: Scheduler
) : ViewModel()

And update TodoListActivity to pass a Scheduler in:

TodoListViewModel(
  repository,
  Schedulers.io(),
  Schedulers.computation()
)

Back in TodoListViewModel, update the call to throttleFirst():

throttleFirst(1, TimeUnit.SECONDS, computationScheduler)

Now you can easily control time in your unit tests. Far out, man.

Head back to TodoListActivity.kt and add code to observe the showEditTaskLiveData at the bottom of onCreate():

viewModel.showEditTaskLiveData.observe(this, Observer {
  EditTaskActivity.launch(this, it)
})

Now, run the app and tap one of the tasks. You should be presented with a blank edit screen that looks like this:

Saving an edited task

On this edit page you’ll want to achieve several tasks:

  1. You want to pre-populate the EditText at the top of the screen with the title of the TaskItem being edited. If there is no TaskItem being edited, then you’ll leave it blank.
  2. You then want to listen for taps on the done FAB in the bottom right, and save an updated TaskItem that contains the new title.
  3. Finally, you want to finish this new activity and return to the task list after the user taps the done button.

Open EditTaskActivity.kt and replace the existing EditTaskViewModel being build in onCreate() with the following. Again, it won’t compile until you edit the view model too:

val repository =
  RoomTaskRepository(TaskRoomDatabase.fetchDatabase(this))
val taskIdKey =
  intent.getIntExtra(TASK_ID_KEY, RoomTaskRepository.INVALID_ID)
EditTaskViewModel(
  // 1
  repository,
  // 2
  Schedulers.io(),
  // 3
  taskIdKey
)

You’re supplying three dependencies to the ViewModel for the EditTask View:

  1. A TaskRepository instance, which you’ll use to fetch and save TaskItems.
  2. A background Scheduler.
  3. The id of the TaskItem you’re editing, which was fetched out of the Intent.

Open EditTaskViewModel.kt and change the class header to accept the new dependencies:

class EditTaskViewModel(
  taskRepository: TaskRepository,
  backgroundScheduler: Scheduler,
  taskId: Int
) : ViewModel()

There’s two different pieces of user input you’ll need to react to:

  1. A user clicking the done floating action button.
  2. A user inputting the name of a task.

Just like before, you’ll need to expose methods and subjects in your view model to handle those actions. Add the following to the top of EditTaskViewModel:

private val finishedClicks = PublishSubject.create<Unit>()
private val taskTitleTextChanges =
  BehaviorSubject.create<CharSequence>()

Then, add two new methods to pipe values into the two subjects:

fun onFinishClicked() = finishedClicks.onNext(Unit)

fun onTextChanged(text: CharSequence) =
  taskTitleTextChanges.onNext(text)

Your view model is now ready to accept user input.

Next up, navigate to EditTaskActivity and add the following to the top of the class:

private val disposables = CompositeDisposable()

Last but not least, add the following below the viewModel declaration:

done.clicks()
  .subscribe { viewModel.onFinishClicked() }
  .addTo(disposables)
title_input.textChanges()
  .subscribe { viewModel.onTextChanged(it) }
  .addTo(disposables)

You’re using the RxBindings clicks() and textChanges() methods to listen for user input events and forwarding them to your view model. You’re now all setup to start reacting to user input.

There are two dynamic pieces to the edit task UI:

  1. Displaying the title of the TaskItem being edited in the EditText at the top of the page.
  2. Finishing the activity after the user taps on the done FAB.

Therefore you’ll need two LiveData objects exposed in the EditTaskViewModel. Add the following instance variables in EditTaskViewModel below the disposables variable:

val finishLiveData = MutableLiveData<Unit>()
val textLiveData = MutableLiveData<String>()

You can think of LiveData objects as having a one-to-one relationship with any dynamic pieces of your UI. Any static component, like a TextView with text that doesn’t change, doesn’t need a corresponding LiveData.

Interacting with the TaskRepository

The first thing you’ll need to do in the EditTaskViewModel is retrieve whatever TaskItem is being edited, if there is one.]

Add an init block to EditTaskViewModel below the variable declarations:

init {
    val existingTask = taskRepository.getTask(taskId).cache()
}

You’re using getTask() on taskRepository along with the taskId passed into the view model to get a Maybe<TaskItem> representing whatever TaskItem is being edited. If there is no TaskItem that corresponds to the passed in id, the Maybe will emit nothing and complete.

You’re also using cache() so you can utilize existingTask in multiple places without remaking the call every time, since that could be expensive.

Now add the following Rx block after existingTasks declaration:

existingTask
  .subscribeOn(backgroundScheduler)
  .subscribe { textLiveData.postValue(it.text) }
  .addTo(disposables)

You’re subscribing to the existingTask Maybe you fetched earlier on the backgroundScheduler and then posting the resulting TaskItems text in the textLiveData.

Open EditTaskActivity.kt again, and subscribe to the textLiveData in the bottom of onCreate() (again making sure to import androidx.lifecycle.Observer and not its Rx equivalent):

viewModel.textLiveData
  .observe(this, Observer(title_input::append))

Run the app again and tap on a task. You should see the title of that task pre-populated in the EditText:

Saving an updated task

The next feature for the Edit Task screen is to save the updated task when the user taps the done button.

You have access to two crucial Observables in the EditTaskViewModel that will help you implement this feature: If you combine the finishedClicks Observable with the taskTitleTextChanges Observable, you’ll have the latest text whenever the done button is tapped.

Open EditTaskViewModel.kt and start off another Rx chain at the bottom of the init block:

Observables.combineLatest(finishedClicks, taskTitleTextChanges)
  .map { it.second }

combineLatest() will combine whatever the latest element is in the finishedClicks and taskTitleTextChanges Observables into a Pair<Unit, CharSequence>. The Unit portion of that Pair is the data type passed in from the finishedClicks Observable. All you care about is that that Observable triggers the combined Observable, so you can use map() to transform the resulting Observable from a Observable<Pair<Unit, CharSequence>> into an Observable<CharSequence>.

Now append the following to the bottom of the Rx chain:

// 1
.flatMapSingle { title ->
  existingTask
    // 2
    .defaultIfEmpty(
        TaskItem(null, title.toString(), Date(), false))
    // 3
    .flatMap {
      val taskItem =
          TaskItem(it.id, title.toString(), Date(), it.isDone)
      taskRepository.insertTask(taskItem)
    }
    // 4
    .subscribeOn(backgroundScheduler)
}

Here’s a breakdown of that short but dense block of code:

  1. You’re using flatMapSingle() to convert this Observable into a Single. You’ll find that whenever you’re executing a network or database call that returns a Single after some user interaction, you’ll want to use flatMapSingle(). Converting from an Observable to a Single can make the intent of your Rx chain clear to other developers.

  2. flatMapSingle() expects the lambda passed into it to return (shocker!) a Single. However, the existingTask variable you declared earlier is a Maybe. If there’s no TaskItem associated with the taskId passed into this view model, you want to save a new TaskItem instead of modifying an existing one. Enter defaultIfEmpty(). defaultIfEmpty() takes a Maybe and converts it into a Single by supplying a default item that the Maybe will use if it’s empty.

    That way you can always guarantee that your Maybe will return an item, and it now satisfies the requirements of being a Single.

  3. You’re then using the flatMap() operator to take the TaskItem and save it in the database using insertTask(), which returns a Single<Long>.

  4. You’re doing all of the above work on the backgroundScheduler because you’re a good Android citizen, and you don’t want to freeze the UI!

That was a powerful batch of code. Congratulations for working your way through it! Finish off the new Rx chain by subscribing to it and making sure it’s properly disposed of. Make sure this goes outside of the flatMapSingle():

.subscribe { finishLiveData.postValue(Unit) }
.addTo(disposables)

Once you’re done saving off the TaskItem you can signal to the activity to call finish via the finishLiveData variable.

To finish off your editing feature, open EditTaskActivity.kt and add code to observe the finishLiveData at the bottom of onCreate():

viewModel.finishLiveData.observe(this, Observer { finish() })

Now run the app and tap one of the tasks. Edit the title for the task, then tap the done FAB. You’ll see that the updated task appears in the list, and it moves to the bottom of whatever section that task is in since you updated the date for that task.

Creating a new task

There’s only one thing missing from your app: The user has no way to create a new task. Luckily, you can lean on the work you finished in the edit section to complete this.

First, open TodoListViewModel.kt and add one last PublishSubject at the top of the class:

private val addClicks = PublishSubject.create<Unit>()

Next up, add a corresponding method to push events through your new subject:

fun addClicked() = addClicks.onNext(Unit)

Then add another Rx chain to the bottom of the init block:

addClicks
  .throttleFirst(1, TimeUnit.SECONDS, computationScheduler)
  .subscribe {
    showEditTaskLiveData
      .postValue(RoomTaskRepository.INVALID_ID)
  }
  .addTo(disposables)

The addClicks stream represents taps on the add FAB. You’re using throttleFirst() again to make sure only the first tap is acted upon. When the user does tap, you’re reusing the showEditTaskLiveData, but this time purposefully passing an INVALID_ID so a new TaskItem is created and saved into the database.

Last but not least, open TodoListActivity.kt and pipe add click events through to your view model by adding this to onCreate():

add_button.clicks()
  .subscribe { viewModel.addClicked() }
  .addTo(disposables)

Now the run the app and add a new task item. You should see the new task inserted at the end of the due tasks list!

Challenges

Challenge 1: Support item deletion

You’ve probably noticed that it isn’t possible to delete items. You’ll need to make changes to both TodoListActivity and TodoListViewModel to add this functionality. Once you complete the challenge, the users will be able to swipe away a task to delete it.

The project includes a helper file named SwipeToRemoveHelper.kt, which facilitates the swipe to remove process. Start off by uncommenting the code in onSwiped() and getMovementFlags(). You’ll also need to add a new method to the TodoAdapter.kt file to allow your SwipeToRemoveHelper class to access files:

fun getListItem(position: Int): TodoListItem {
  return getItem(position)
}

You can add the following code in TodoListActivity’s onCreate() to hook it up to your RecyclerView:

val swipeHelper = SwipeToRemoveHelper(adapter)
ItemTouchHelper(swipeHelper).attachToRecyclerView(todo_list)

Now you can get to the core of the challenge: handling the actual deletion. The solution to this challenge involves:

  • Creating deleteTask() on the TaskRepository, RoomTaskRepository and TaskDao classes. For the TaskDao method, you can use the @Delete annotation to instruct Room that you’re deleting an item.
  • Using swipeStream variable exposed by SwipeToRemoveHelper with your TodoListViewModel to listen for remove events.
  • Subscribing to the swipeStream and calling repository.deleteTask() with the swiped away task.

Challenge 2: Add live statistics

To make the UI more interesting, you want to display the number of due and done items in your list. A text view is reserved for this purpose at the bottom of the TodoListActivity view; it’s called statistics. For this challenge, start from either your solution to the previous challenge, or from the chapter’s final project.

First off, set the statistics view to be visible in onCreate of TodoListActivity:

statistics.visibility = View.VISIBLE

Next up you’ll need to create a new LiveData object to carry the statistics information from the TodoListViewModel to the activity.

You’ll then need to subscribe to that LiveData in the TodoListActivity and update the statistics text view.

To get the actual statistics, you’ll want to work off of the taskStream exposed by the repository. You’re already subscribing to the taskStream Observable, so consider using cache() to do multiple subscribes!

Where to go from here?

This concludes the final chapter of this book! We hope you loved it as much as we did. You now have a solid foundation of programming with RxJava, RxKotlin, and RxAndroid to build on as you continue your learning. Good luck!

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.