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
TaskItemmodel that describes an individual task. - A
TaskRepositoryrepository 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:
-
LiveDatahas the benefit of being directly tied to Android lifecycle events. That makes it a fantastic candidate for use inside anActivityorFragment, because it means you don’t need to worry about disposing of any subscriptions as lifecycle events fire. -
You don’t need to worry about when you’re subscribing or observing your lifecycles. If you were to use
Observables instead ofLiveDataobjects, 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. -
LiveData, while powerful and helpful, has nowhere near the power of an RxJavaObservable. The power of RxJava should be clear at this point. While bothObservableandLiveDataimplement the observer pattern,Observables have a huge array of operators and utilities that they can use to create complex streams.LiveDatais 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:
- A method to insert a single
TaskItemfor when the user creates a new task and saves it. - A method to insert a list of
TaskItems to allow the app to pre-populate the database with several defaultTaskItems. - A method to fetch an individual
TaskItemfrom the database by a givenid. - 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:
-
When inserting a single task, you’re specifying a return type of
Single<Long>, where theLongrepresents the number of updated rows, which we’d expect to always be one. -
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. -
When fetching an individual
TaskItemfrom the database, you’re setting the return type to beMaybe<TaskItem>. Since there’s no guarantee that a task exists for any givenid, aMaybemakes the most sense here. See Chapter 21, “RxJava & Jetpack,” to learn howMaybes work with Room. -
Finally, the return type for
taskStream()is naturallyObservable<List<TaskItem>>. Every time a newTaskItemis inserted or updatedtaskStream()should emit a newList<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:
- You’ve introduced a new
INVALID_IDconstant to avoid having other classes pass innullfor theTaskItems ID. You want to avoidnullvalues as much as possible, since they’re laborious to work around and error-prone. - You’re delegating
getTask()andtaskStream()to theTaskDatabaseobject passed intoRoomTaskRepository - 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:
- A user could use it to create a new
TaskItemin the database. - 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:
- A
DueTasksobject which represents the first header grouping together the tasks that are yet to be done. - A
DoneTasksobject which represents the second header grouping together the tasks that have been finished. - And a
TaskListItemdata 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:
-
TodoAdapterextendsListAdapterrather thanRecyclerView.Adapter.ListAdapteris an extremely handy class in theRecyclerViewlibrary that will compute a diff between the current list and a new list you provide. It will then dispatchAdapter.notifyItemcalls 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. - You’re supplying a
TodoDiffUtilobject to theListAdaptersuperclass.ListAdapterisn’t magical. It still needs a way to tell that two list items aren’t the same item. It uses theDiffUtil.ItemCallbackclass 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:
-
areItemsTheSame(), which checks to see if two items represent the same item. -
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:
- Two
DueTasksitems are always equal since they’re represented asobjects. The same is true for twoDoneTasksobjects. - 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:
- You’re calling
taskStream()onTaskRepository.taskStream()should return anObservable<List<TaskItem>>that emits a newList<TaskItem>every time the database is updated. - You’re then using
map()to transform thatList<TaskItem>into aList<TodoListItem>. Don’t be confused by themap()within amap()here - the secondmap()is being called on theList<TaskItem>and is a method exposed onLists by the Kotlin standard library. - You’re then taking the
List<TodoListItem>returned by the previousmap()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 usingfilter(), again in the Kotlin standard library. - Finally, you’re subscribing on a background scheduler and forwarding the results onto the
listItemsLiveDataobject. 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.Observerand 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:
- They can toggle an individual task to mark it as completed.
- 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:
- You’re using the
taskDoneTogglesObservable you added into the view model earlier to listen for a user tapping the switch one any of the task items. - You’re then using
flatMapSingle()to transform this stream from anObservable<Pair<TaskItem, Boolean>>into aSingle<Long>. You need to useflatMapSingle()becauseflatMap()expects the lambda you pass it to produce anObservable, butTaskRepository.insertTask()produces aSingle. - You’re using the aforementioned
insertTask()to save the updated version of theTaskItemthe user toggled. The emittedPaircontains both theTaskItemto update and whether that item has been marked as completed or not, which you’re using to create a newTaskItemto 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
TaskItemimplementParcelableand then pass it as an extra in anIntent. 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 anIntentcan carry. In this scenario, you can easily fetch aTaskItemfrom 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:
- Subscribes to the
taskClicksObservable you passed in earlier. Remember thattaskClicksemits every time a user taps one of the rows in the list of tasks. - Uses
throttleFirst()to ensure that only one tap goes through.throttleFirst()is a new operator that works similarly todebounce(). 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 usingthrottleFirst, you can ensure that multiple activities aren’t started by quickly tapping the task. - Fetches the
idfrom the task, defaulting to theINVALID_IDif theidon the task item is null. Finally, you’re posting theidtoshowEditTaskLiveData, 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:
- You want to pre-populate the
EditTextat the top of the screen with the title of theTaskItembeing edited. If there is noTaskItembeing edited, then you’ll leave it blank. - You then want to listen for taps on the done FAB in the bottom right, and save an updated
TaskItemthat contains the new title. - 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:
- A
TaskRepositoryinstance, which you’ll use to fetch and saveTaskItems. - A background
Scheduler. - The id of the
TaskItemyou’re editing, which was fetched out of theIntent.
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:
- A user clicking the done floating action button.
- 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:
- Displaying the title of the
TaskItembeing edited in theEditTextat the top of the page. - Finishing the activity after the user taps on the
doneFAB.
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:
-
You’re using
flatMapSingle()to convert thisObservableinto aSingle. You’ll find that whenever you’re executing a network or database call that returns aSingleafter some user interaction, you’ll want to useflatMapSingle(). Converting from anObservableto aSinglecan make the intent of your Rx chain clear to other developers. -
flatMapSingle()expects the lambda passed into it to return (shocker!) aSingle. However, theexistingTaskvariable you declared earlier is aMaybe. If there’s noTaskItemassociated with thetaskIdpassed into this view model, you want to save a newTaskIteminstead of modifying an existing one. EnterdefaultIfEmpty().defaultIfEmpty()takes aMaybeand converts it into aSingleby supplying a default item that theMaybewill use if it’s empty.That way you can always guarantee that your
Maybewill return an item, and it now satisfies the requirements of being aSingle. -
You’re then using the
flatMap()operator to take theTaskItemand save it in the database usinginsertTask(), which returns aSingle<Long>. -
You’re doing all of the above work on the
backgroundSchedulerbecause 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 theTaskRepository,RoomTaskRepositoryandTaskDaoclasses. For theTaskDaomethod, you can use the@Deleteannotation to instruct Room that you’re deleting an item. - Using
swipeStreamvariable exposed bySwipeToRemoveHelperwith yourTodoListViewModelto listen for remove events. - Subscribing to the
swipeStreamand callingrepository.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!