Chapters

Hide chapters

Kotlin Coroutines by Tutorials

Third Edition · Android 12 · Kotlin 1.6 · Android Studio Bumblebee

Section I: Introduction to Coroutines

Section 1: 9 chapters
Show chapters Hide chapters

17. Persistence & Coroutines
Written by Luka Kordić

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

Most Android apps are data-consuming apps. This means that most of the time, these apps request data from another source, usually a web service. Another common requirement for apps is to have data available offline, so users can see it even when they don’t have a network connection. A common way to make data available offline is to store it in a database.

Regarding database persistence in Android, Room has been the de facto standard for quite some time. Room has supported coroutines since version 2.1. That means you can mark your DAO methods as suspending functions to ensure they’re not executed on the main thread. You can also observe changes on the database tables with Kotlin flows. In this chapter, you’ll see how Room and coroutines fit nicely together and how that makes it easy to persist and manage your data.

Note: This chapter assumes you know how to implement Room database into your project because you’ll focus solely on using Room with coroutines.

Getting Started

Download the starter project for this chapter and open it in Android Studio. Before you start working on the code, take some time to familiarize yourself with it. The files you’ll use in this chapter are:

  1. DisneyDatabase.kt in the data/database package: This file contains DisneyDatabase abstract class, which is the Room database definition. It has the characterDao abstract method, which you’ll use to interact with the database.
  2. CharacterDao.kt in the data/database package contains the CharacterDao interface annotated with @Dao. This interface contains method definitions that Room will use to generate concrete implementations.
  3. DisneyRepository.kt in the repository package is a bridge between the UI layer in your app and the data layer.

To use Room with coroutines in your project, you must declare a Gradle dependency, which looks like this:

implementation "androidx.room:room-ktx:2.4.2"

This is done for you in this project, but keep that in mind for the future.

Until now, all the data in your app came from the network. In this chapter, you’ll add a new data source — a database. Generally, when you have two or more data sources, you want to have an abstraction for getting data from lower layers of an app to the UI layer. When you want to show characters on the screen, there’s no reason for the UI layer to know whether the data comes from the Internet or the local database. That’s the job of DisneyRepository, in your case.

Accessing Database on the Main Thread

For the first example, you’ll make a simple database query on the main thread. The code for the example has been prepared for you. Open CharacterDao.kt and check out the definition of getCharacters.

@Query("SELECT * FROM character")
fun getCharacters(): List<DisneyCharacter>
@Override
  public List<DisneyCharacter> getCharacters() {
    final String _sql = "SELECT * FROM character";
    final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 0);
    __db.assertNotSuspendingTransaction();
    final Cursor _cursor = DBUtil.query(__db, _statement, false, null);
    try {…} finally {
      _cursor.close();
      _statement.release();
    }
  }
private fun fetchDisneyCharacters() {
  val result = characterDao.getCharacters()
  showResults(result)
}
E/AndroidRuntime: FATAL EXCEPTION: main
  Process: com.raywenderlich.android.disneyexplorer, PID: 15323
  java.lang.IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long period of time.

Suspending Database Calls

Fixing the crash from the previous example is rather simple because Room and coroutines work seamlessly together. In CharacterDao.kt, mark getCharacters with the suspend modifier. Your method definition should look like this:

@Query("SELECT * FROM character")
suspend fun getCharacters(): List<DisneyCharacter>
private fun fetchDisneyCharacters() {
  lifecycleScope.launch {
    val result = characterDao.getCharacters()
    showResults(result)
  }
}

@OptIn(DelicateCoroutinesApi::class)
private fun populateDatabase() {
  val characterDao = DependencyHolder.characterDao
  val characters = listOf(
    DisneyCharacter(
      0,
      "Mickey Mouse",
      "https://toppng.com/uploads/preview/mickey-mouse-vector-free-download-11574217307wizdbrc6rj.png"
    ),
    DisneyCharacter(
      1,
      "Simba",
      "https://toppng.com/uploads/preview/disneys-simba-logo-vector-free-11574130611twlahawi9n.png"
    )
  )
  GlobalScope.launch {
    characterDao.saveCharacters(characters)
  }
}

Under the Hood

Adding the suspend modifier doesn’t usually change a thread of execution. So why did it happen here? To figure that out, look at the code Room generated for suspend fun getCharacters:

  @Override
  public Object getCharacters(final Continuation<? super List<DisneyCharacter>> continuation) {
    final String _sql = "SELECT * FROM character";
    final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 0);
    final CancellationSignal _cancellationSignal = DBUtil.createCancellationSignal();
    return CoroutinesRoom.execute(__db, false, _cancellationSignal, new Callable<List<DisneyCharacter>>() {
      @Override
      public List<DisneyCharacter> call() throws Exception {
        final Cursor _cursor = DBUtil.query(__db, _statement, false, null);
        try {…} finally {
          _cursor.close();
          _statement.release();
        }
      }
    }, continuation);
  }
@OptIn(DelicateCoroutinesApi::class)
@JvmStatic
public suspend fun <R> execute(
    db: RoomDatabase,
    inTransaction: Boolean,
    cancellationSignal: CancellationSignal,
    callable: Callable<R>
): R {
    // 1
    if (db.isOpen && db.inTransaction()) {
        return callable.call()
    }
    // 2
    // Use the transaction dispatcher if we are on a transaction coroutine, otherwise
    // use the database dispatchers.
     val context = coroutineContext[TransactionElement]?.transactionDispatcher
         ?: if (inTransaction) db.transactionDispatcher else db.getQueryDispatcher()
    // 3
     return suspendCancellableCoroutine<R> { continuation ->
         val job = GlobalScope.launch(context) {
             try {
                 val result = callable.call()
                 continuation.resume(result)
             } catch (exception: Throwable) {
                 continuation.resumeWithException(exception)
             }
         }
         continuation.invokeOnCancellation {
             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
                 SupportSQLiteCompat.Api16Impl.cancel(cancellationSignal)
             }
             job.cancel()
       }
    }
}

Observing Database Changes

Often, you’ll have both a database and a back-end service as data sources. In such cases, it’s usually wise to have a single source of truth for your data. For example, if your app needs to support offline mode, you should make the database your source of truth. That means the data you show on the screen will always come from the database. When you need new data from your back end, you fetch and insert that data into the database, then update your UI with the new data. When doing this kind of work, you don’t want to manually query the database every time new data comes in. Instead, you want to observe the changes and react to them by displaying them to the user.

@Query("SELECT * FROM character")
fun getCharacters(): Flow<List<DisneyCharacter>>
fun getDisneyCharacters() = characterDao.getCharacters()
suspend fun getFreshData() {
  apiService.getCharacters().onSuccess { characterDao.saveCharacters(it.data) }
}
private fun fetchDisneyCharacters() {
  lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
      disneyRepository.getDisneyCharacters().collect {
        showResults(it)
      }
    }
  }
}
private fun getFreshData() {
  lifecycleScope.launch {
    disneyRepository.getFreshData()
  }
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
  if (item.itemId == R.id.refresh) {
    getFreshData()
  }
  return false
}

Suspending Transactions

If you close and reopen the app now, you see that you always have 52 items in the list. That’s because you fetched and stored the characters in the last example. Now, you’ll use populateDatabase in the App class to delete the entries at every startup and insert just Mickey and Simba.

@Transaction
suspend fun deleteAllAndUpdate(characters: List<DisneyCharacter>) {
  deleteAll()
  saveCharacters(characters)
}

Key Points

  • When communicating with the database, make sure you’re doing it on a background thread.
  • Mark your DAO methods with suspend for easy threading.
  • Use Kotlin Flows as return types for your DAO methods if you want to observe changes in your database.
  • Room automatically executes transactions on a background thread when your DAO methods are suspending or return Flow.
  • When you have multiples sources of data, use a repository class as a bridge between the data layer and the UI layer.

Where to Go From Here?

Congratulations, you’ve successfully established communication with Room database by using coroutines. In the final chapter, you’ll add a ViewModel to your project and use it as a mediator between the repository and the activity. You’re also going to write some unit tests for the code using coroutines. Finally, you’ll see an example of using coroutines in Jetpack Compose code.

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