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

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

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.

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:

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.

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

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.

// 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>>
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()

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.

interface TaskRepository {
  fun insertTask(taskItem: TaskItem): Single<Long>
  fun getTask(id: Int): Maybe<TaskItem>
  fun taskStream(): Observable<List<TaskItem>>
}
// 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()
}
val validIdTask =
  if (taskItem.id == RoomTaskRepository.INVALID_ID) {
    taskItem.copy(id = null)
  } else {
    taskItem
  }
return database.taskDao().insertTask(validIdTask).toV3Single()

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.

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

Using a ListAdapter

Open TodoAdapter.kt and look at the class header:

class TodoAdapter : ListAdapter<TodoListItem,
  RecyclerView.ViewHolder>(TodoDiffUtil())
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
  }
}
return oldItem == newItem
return getItem(position).viewType
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
  }
}

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.

val viewModel = buildViewModel {
  TodoListViewModel()
}
val repository =
    RoomTaskRepository(TaskRoomDatabase.fetchDatabase(this))
TodoListViewModel(repository, Schedulers.io())
class TodoListViewModel(
  repository: TaskRepository,
  backgroundScheduler: Scheduler
) : ViewModel()
val listItemsLiveData = MutableLiveData<List<TodoListItem>>()
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)
viewModel.listItemsLiveData
  .observe(this, Observer(adapter::submitList))

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.

private val taskClickSubject = PublishSubject.create<TaskItem>()
private val taskToggledSubject =
  PublishSubject.create<Pair<TaskItem, Boolean>>()
val taskClickStream = taskClickSubject.hide()
val taskToggledStream = taskToggledSubject.hide()
holder.itemView.task_done.setOnClickListener {
  taskToggledSubject.onNext(
     item.task to holder.itemView.task_done.isChecked)
}
holder.itemView.setOnClickListener {
  taskClickSubject.onNext(item.task)
}

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.

private val taskClicks = PublishSubject.create<TaskItem>()
private val taskDoneToggles =
 PublishSubject.create<Pair<TaskItem, Boolean>>()
fun taskClicked(taskItem: TaskItem) =
  taskClicks.onNext(taskItem)

fun taskDoneToggled(taskItem: TaskItem, on: Boolean) =
  taskDoneToggles.onNext(Pair(taskItem, on))
private val disposables = CompositeDisposable()
adapter.taskClickStream.subscribe {
  viewModel.taskClicked(it)
}.addTo(disposables)

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

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.

val showEditTaskLiveData = MutableLiveData<Int>()
// 1
taskClicks
  // 2
  .throttleFirst(1, TimeUnit.SECONDS)
  // 3
  .subscribe {
    val id = it.id ?: RoomTaskRepository.INVALID_ID
    showEditTaskLiveData.postValue(id)
  }
  .addTo(disposables)
class TodoListViewModel(
  repository: TaskRepository,
  backgroundScheduler: Scheduler,
  computationScheduler: Scheduler
) : ViewModel()
TodoListViewModel(
  repository,
  Schedulers.io(),
  Schedulers.computation()
)
throttleFirst(1, TimeUnit.SECONDS, computationScheduler)
viewModel.showEditTaskLiveData.observe(this, Observer {
  EditTaskActivity.launch(this, it)
})

Saving an edited task

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

val repository =
  RoomTaskRepository(TaskRoomDatabase.fetchDatabase(this))
val taskIdKey =
  intent.getIntExtra(TASK_ID_KEY, RoomTaskRepository.INVALID_ID)
EditTaskViewModel(
  // 1
  repository,
  // 2
  Schedulers.io(),
  // 3
  taskIdKey
)
class EditTaskViewModel(
  taskRepository: TaskRepository,
  backgroundScheduler: Scheduler,
  taskId: Int
) : ViewModel()
private val finishedClicks = PublishSubject.create<Unit>()
private val taskTitleTextChanges =
  BehaviorSubject.create<CharSequence>()
fun onFinishClicked() = finishedClicks.onNext(Unit)

fun onTextChanged(text: CharSequence) =
  taskTitleTextChanges.onNext(text)
private val disposables = CompositeDisposable()
done.clicks()
  .subscribe { viewModel.onFinishClicked() }
  .addTo(disposables)
title_input.textChanges()
  .subscribe { viewModel.onTextChanged(it) }
  .addTo(disposables)
val finishLiveData = MutableLiveData<Unit>()
val textLiveData = MutableLiveData<String>()

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.]

init {
    val existingTask = taskRepository.getTask(taskId).cache()
}
existingTask
  .subscribeOn(backgroundScheduler)
  .subscribe { textLiveData.postValue(it.text) }
  .addTo(disposables)
viewModel.textLiveData
  .observe(this, Observer(title_input::append))

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.

Observables.combineLatest(finishedClicks, taskTitleTextChanges)
  .map { it.second }
// 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)
}
.subscribe { finishLiveData.postValue(Unit) }
.addTo(disposables)
viewModel.finishLiveData.observe(this, Observer { finish() })

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.

private val addClicks = PublishSubject.create<Unit>()
fun addClicked() = addClicks.onNext(Unit)
addClicks
  .throttleFirst(1, TimeUnit.SECONDS, computationScheduler)
  .subscribe {
    showEditTaskLiveData
      .postValue(RoomTaskRepository.INVALID_ID)
  }
  .addTo(disposables)
add_button.clicks()
  .subscribe { viewModel.addClicked() }
  .addTo(disposables)

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.

fun getListItem(position: Int): TodoListItem {
  return getItem(position)
}
val swipeHelper = SwipeToRemoveHelper(adapter)
ItemTouchHelper(swipeHelper).attachToRecyclerView(todo_list)

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.

statistics.visibility = View.VISIBLE

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.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now