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

21. RxJava & Jetpack
Written by Alex Sullivan

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

Android Jetpack is a suite of libraries provided by the Android team to make developing Android apps a breeze (well, maybe not quite a breeze…). You’ve already been working with two of the libraries provided as part of Jetpack throughout the book: LiveData and ViewModel. In this chapter, you’re going to explore two more libraries that every Android developer should know about, and how they interact with RxJava.

The first library you’re going to utilize is the Room database library. Interacting with a database has typically been a painful process when writing an Android app. In the beginning, a developer would usually use a custom instance of the SQLiteOpenHelper class to manually create tables and run updates using SQL.

This approach worked, but came with a lot of downsides. It was cumbersome to keep all of the SQL statements you were writing in code and it was very easy to have the objects you were trying to store in the database and the tables representing those objects get out of sync. To top it all off, you needed a lot of boilerplate to turn those objects into ContentValues to then be inserted into the database. Luckily, Room provides an easy to use abstraction on top of SQLiteOpenHlper that makes storing data a much simpler task.

The second library you’re going to explore is the Paging library. Another common task for app developers is to implement a kind of infinitely scrollable list, like Instagram or Facebook has. The Paging library provides simple hooks for you to use to load new data as a user scrolls down in a list. It even ties together with Room to give you an easy way to pipe data from your database into your app.

Best of all, both Room and the Paging library come with first-class RxJava integrations!

In this chapter, you’ll explore both libraries by creating a Lord of the Rings-based book collector app, which allows a user to fetch a list of books from the Open Library API, scroll through the books, favorite some of them and mark others as read.

Getting started

Open the starter project in Android Studio and run the app. You should see the following screen:

The BookCollector app displays a list of books fetched from the Open Library API. A user can then either favorite the book by clicking the star icon, or mark it as a book they’ve already read by clicking the envelope icon.

There are three pages in the app. The first page is the screen you see in the screenshot above and the starting screen for the app, which displays the entire list of books. The second page is a favorites page, which displays the books the user has favorited. The third page displays all of the books the user has marked as read.

Each page is controlled by a different Fragment in a ViewPager. Each fragment is backed by the same view model, which is called MainViewModel. Open the MainViewModel class now and take a look around.

The first thing you’ll notice is that this view model follows a familiar pattern: There are three LiveData objects that govern what’s shown on an individual page. Then, in the init block, the view model queries the Open Library API and uses the cache operator to cache the result. Then, the view model subscribes to the resulting Observable three times, once for each live data object, filtering and mapping the results according to what that live data should emit.

There are also two stubbed out methods:

fun favoriteClicked(book: Book) {
  TODO()
}

fun readClicked(book: Book) {
  TODO()
}

You’ll update these two methods governing what happens when a user clicks the favorite icon and the read icon later on in the chapter. Before you go any further, it’s a good idea to get a quick refresher on how Room works.

There are three core components to Room:

  1. The Entity: An Entity is a model object annotated with the @Entity annotation, and represents the data that will reside in the database. Room will typically create a table under the hood for each class marked with the @Entity annotation.
  2. The Dao: A Dao is an interface marked with the @Dao annotation. This interface typically exposes high-level methods to insert and query items from the database. You can think of the Dao as being akin to a Retrofit interface.
  3. The Database: The Database class is a class that you create that extends the RoomDatabase object, and it is annotated with an @Database annotation, wherein you list all of your Entities and expose the version of the database.

Open Book.kt to see an example of an Entity:

@Entity
data class Book(@PrimaryKey val title: String,
                val authorName: String,
                val publisher: String,
                val subject: String,
                val isFavorited: Boolean = false,
                val isAlreadyRead: Boolean = false)

The Book class is marked with the @Entity annotation to signify that it can be inserted and retrieved from a Room database.

Each Entity needs an instance variable marked with the @PrimaryKey annotation. The @PrimaryKey annotation signifies to Room that this instance variable can determine uniqueness for an object. That means that, in the example above, you could never have two Books with the same title in a Room database, since that would violate a primary key constraint on uniqueness.

Now, open BookDao.kt to see an example of a Dao:

@Dao
interface BookDao

As you can see, the BookDao class is empty. For now. :]

Last but not least, open BookDatabase.kt to see an example Database:

@Database(entities = [Book::class], version = 1)
abstract class BookDatabase : RoomDatabase() {
  abstract fun bookDao(): BookDao
}

It outlines the entities that will live in the database and the version of the database. For this app, you’ll only have one object residing in the database: the Book class. Your only exposed dao will be the BookDao.

RxJava and Room

Now that you’re familiar with Room, it’s time to sprinkle some Rx goodness on top of it.

implementation "androidx.room:room-rxjava2:$room_version"

Database philosophy

Before you start getting your hands dirty, take a minute to discuss what the strategy is going to be moving forward for dealing with the network and the database.

@Insert(onConflict = OnConflictStrategy.IGNORE)
fun insertBooks(books: List<Book>): Completable
@Query("SELECT * from book ORDER BY title")
fun bookStream(): Observable<List<Book>>

Inserting an item

If you’re inserting an item into the database, you can use the following return types:

Updating and deleting an item

If you’re updating or deleting an item in the database, you can use the same return types as inserting but with a slightly different meaning:

Querying

Querying is where all the magic happens, and you can use the full suite of reactive types, other than Completable, which doesn’t make much sense in a querying context:

Reacting to database changes

Now that you’ve exposed methods to insert and retrieve books from the database, it’s time to update the app to utilize those methods.

// 1
val observable = OpenLibraryApi.searchBooks("Lord of the Rings")
  .subscribeOn(Schedulers.io())
  // 2
  .flatMapCompletable {
    database.bookDao().insertBooks(it).toV3Completable()
  }
  // 3
  .andThen(database.bookDao().bookStream().toV3Observable())
  .share()
observable
  .subscribeBy(
    onNext = { item -> allBooksLiveData.postValue(item) },
    onError = { print("Error: $it") }
  )
.addTo(disposables)

Updating individual items

The next thing you need to do to get the BookCollector app up and running is to fill out the details of the favoriteClicked and readClicked methods in the MainViewModel. However, before you do that, you’ll need a way to insert a single updated book into the database.

@Update(onConflict = OnConflictStrategy.REPLACE)
fun updateBook(book: Book): Single<Int>
fun favoriteClicked(book: Book) {
  database.bookDao()
    .updateBook(book.copy(isFavorited = !book.isFavorited))
    .toV3Single()
    .subscribeOn(Schedulers.io())
    .subscribe()
    .addTo(disposables)
}
fun readClicked(book: Book) {
  database.bookDao()
    .updateBook(book.copy(isAlreadyRead = !book.isAlreadyRead))
    .toV3Single()
    .subscribeOn(Schedulers.io())
    .subscribe()
    .addTo(disposables)
}

Starting the app with cached data

The app is working great, but it’s not fully utilizing the fact that it’s using a database. Specifically, when the app starts, it’s immediately making a network request and not showing any information until that request finishes. That’s a bummer since you’ve got the data at your fingertips!

val observable = OpenLibraryApi.searchBooks("Lord of the Rings")
  // 1
  .retryWhen { it.delay(5, TimeUnit.SECONDS) }
  .subscribeOn(Schedulers.io())
  .flatMapCompletable {
      database.bookDao().insertBooks(it).toV3Completable()
   }
  .andThen(database.bookDao().bookStream().toV3Observable())
  // 2
  .startWith(database.bookDao().bookStream().toV3Observable()
    .take(1))
  .share()

Paging data in

Now that you’ve explored the Room libraries Rx integration, it’s time to implement infinite paging using the paging library.

@GET("search.json")
fun searchBooks(
  @Query("q") searchTerm: String,
  @Query("page") page: Int
): Single<OpenLibraryResponse>
fun searchBooks(searchTerm: String, page: Int = 1): Single<List<Book>> {
  return service.searchBooks(searchTerm, page)
  ...
}
PagedListAdapter<Book, BookViewHolder>(getDiffUtil())
val book = getItem(position) ?: return
@Query("SELECT * from book ORDER BY title")
fun bookStream(): DataSource.Factory<Int, Book>
implementation "androidx.paging:paging-rxjava2-ktx:$paging_version"
val config = PagedList.Config.Builder()
  .setEnablePlaceholders(false)
  .setPageSize(20)
  .build()
RxPagedListBuilder<Int, Book>(
    database.bookDao().bookStream(), config)
  .buildObservable()
  .toV3Observable()  
  .subscribe(allBooksLiveData::postValue)
  .addTo(disposables)
val allBooksLiveData = MutableLiveData<PagedList<Book>>()
val favoriteBooksLiveData = MutableLiveData<PagedList<Book>>()
val alreadyReadBooksLiveData =
  MutableLiveData<PagedList<Book>>()
@Query("SELECT * from book WHERE isFavorited = 1 ORDER BY title")
fun favoritesStream(): DataSource.Factory<Int, Book>

@Query("SELECT * from book WHERE isAlreadyRead = 1 ORDER BY title")
fun alreadyReadStream(): DataSource.Factory<Int, Book>
RxPagedListBuilder<Int, Book>(
    database.bookDao().favoritesStream(), config)
  .buildObservable()
  .toV3Observable()
  .subscribe(favoriteBooksLiveData::postValue)
  .addTo(disposables)

RxPagedListBuilder<Int, Book>(
    database.bookDao().alreadyReadStream(), config)
  .buildObservable()
  .toV3Observable()
  .subscribe(alreadyReadBooksLiveData::postValue)
  .addTo(disposables)

Paging in from the network

This app is looking beautiful, but there’s one problem: it’s not pulling anything from the server! Right now the app is only serving up cached data from the database; it’s never actually fetching anything new. If you were to uninstall and reinstall the app, you wouldn’t see any content because nothing would actually be downloaded.

private fun loadItems(requestType: PagingRequestHelper.RequestType) {
  // 1
  helper.runIfNotRunning(requestType) { callback ->
    // 2
    OpenLibraryApi.searchBooks(searchTerm, currentPage)
      // 3
      .flatMapCompletable {
        db.bookDao().insertBooks(it).toV3Completable()
      }
      .subscribeOn(Schedulers.io())
      // 4
      .subscribe {
        currentPage++
        callback.recordSuccess()
      }
  }
}
override fun onZeroItemsLoaded() {
  loadItems(PagingRequestHelper.RequestType.INITIAL)
}

override fun onItemAtEndLoaded(itemAtEnd: Book) {
  loadItems(PagingRequestHelper.RequestType.AFTER)
}
.setBoundaryCallback(
    BookBoundaryCallback("The lord of the rings", database))

Key points

  • Room allows you to specify reactive types in your Dao objects.
  • You can use Completable or Single or Maybe when inserting, updating or deleting items from the database.
  • You can use Observable or Flowable when querying items from the database.
  • Your query Observable will keep emitting as data changes in the database!
  • The Paging library comes with an RxJava extension that allows you to stream PagedList objects.
  • Room and the Paging library make for a fantastic reactive combination!

Where to go from here?

The Room and Paging libraries are great examples of how a library can effectively integrate Rx into its API. Given these libraries are written by Google, its nice to know that you’re getting first party support for Rx from these libraries.

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 accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now