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ć

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>

It’s a simple query method to fetch all characters from the database. As you can see from the return type, it fetches them as a list. Look at the implementation that Room generates for this method. Select Make Project from the Build menu in Android Studio to build the project. Open build/generated/source/kapt/debug/com/raywenderlich/android/disneyexplorer/data/database/CharacterDao_Impl.java and locate getCharacters():

@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();
    }
  }

try{…} block is omitted for brevity and because it’s not important for this example. You’ll compare this code with the code generated for a suspending method that you’ll implement in a moment. Pay attention to the following aspects in the code above:

  • The method has zero parameters.
  • It prepares the _statement with the string you put in @Query.
  • It runs the query by executing DBUtil.query

Now, open DisneyActivity.kt and check out the implementation of fetchDisneyCharacters, which looks like this:

private fun fetchDisneyCharacters() {
  val result = characterDao.getCharacters()
  showResults(result)
}

You make a call to the database, store the result in a value and pass it to showResults for rendering. Build and run the app. On the intro screen, click Networking, Persistence, Jetpack. The app should crash with the following error:

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.

The app crashed because Room doesn’t let you do any database operations on the main thread. You’re going to fix this in a moment, using Room’s built-in coroutine support.

Note: For now, you’re calling DAO methods directly, ignoring the repository for simplicity’s sake. You’ll connect the repository soon.

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>

That’s all you have to do to make the problem go away. Of course, because this now is a suspending function, you’ll have to launch the coroutine to be able to run it. In DisneyActivity.kt, change the implementation of fetchDisneyCharacters to the following:

private fun fetchDisneyCharacters() {
  lifecycleScope.launch {
    val result = characterDao.getCharacters()
    showResults(result)
  }
}

Build and run the app. You should see Mickey and Simba in your list, like in the image below:

You might wonder where the two items came from. A simple method in App.kt inserts the two items into the database upon app startup. It looks like this:

@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)
  }
}

This method creates two DisneyCharacters, launches a new coroutine in GlobalScope and calls the saveCharacters suspending function to insert the items. Because GlobalScope is a delicate API, you need to put @OptIn(DelicateCoroutinesApi::class) on top of the method. It’s safe to use GlobalScope here because it will get canceled when the app process finishes and your work isn’t tied to any particular screen.

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);
  }

Compare this code with the generated code from the previous example.

  • Even though you didn’t define your method to accept any parameters, Room added one called continuation. That allows this method to suspend execution of the coroutine it’s running in and to know how to resume when needed.

  • It prepares _statement in the same way as for non-suspending function.

  • Creates a CancellationSignal so it cooperates with cancellation.

  • Calls CoroutinesRoom.execute, which is the part responsible for offloading the actual database communication to a background thread. Notice that the same logic from the synchronous method is wrapped in a Callable here.

To better understand how this works, check out the implementation of CoroutinesRoom.execute as well:

@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()
       }
    }
}

Here’s a breakdown of this code:

  1. If the database is opened and a transaction is currently in progress, it executes the callable immediately.
  2. Room uses different Dispatchers for transactions and queries by calling db.transactionDispatcher and db.getQueryDispatcher(). These are derived from the executors you can provide while building your database. Room exposes two methods for this: setTransactionExecutor and setQueryExecutor. If you don’t define this yourself, it will use the IO executor from Architecture Components — the same executor used by LiveData.
  3. It wraps Callable.call with suspendCancellableCoroutine and makes the requested database operation.

You’ve just gone through a bunch of code that Room generates for you. Although it’s good to understand how everything works behind the scenes, you don’t need to memorize any of this. It’s important to remember that if you mark your method with suspend, threading will be handled for you automatically.

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.

To demonstrate this, you’ll observe changes in your DisneyCharacter database using Kotlin Flows. You’ll make an API call to fetch the characters, put the result in the database and then collect that data from the flow.

To start with the implementation, navigate to CharacterDao.kt and change getCharacters to the following:

@Query("SELECT * FROM character")
fun getCharacters(): Flow<List<DisneyCharacter>>

There are two significant differences compared with this method’s previous definition:

  1. You changed the return type from List<DisneyCharacter> to Flow<List<DisneyCharacter>>.
  2. You don’t need the suspend modifier anymore because this method returns immediately and doesn’t block anything. Flow is activated only when a terminal operator, such as collect, is invoked.

Note: When you have a DAO method defined with flow as a return type, Room automatically moves the execution to a background thread. This is also what happens when you define DAO with suspendable functions.

Next, open DisneyRepository.kt and add two new methods:

  1. getDisneyCharacters, which looks like this:
fun getDisneyCharacters() = characterDao.getCharacters()

This method forwards the call to the database. You do it this way, using the repository, because you want to hide that the data is coming from the database.

  1. getFreshData, which looks like this:
suspend fun getFreshData() {
  apiService.getCharacters().onSuccess { characterDao.saveCharacters(it.data) }
}

getFreshData fetches the characters from the API and saves them into the database if the request has been successful. Notice you’re not returning any data from the database at this point. You’re simply inserting the new entries.

As the final step, go to your DisneyActivity.kt and replace the old fetchDisneyCharacters implementation with this one:

private fun fetchDisneyCharacters() {
  lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
      disneyRepository.getDisneyCharacters().collect {
        showResults(it)
      }
    }
  }
}

This code should look familiar to you from Chapter 15, “Coroutines in the UI Layer”. You launch a coroutine in lifecycleScope and then use repeatOnLifecycle to avoid leaking the flow. At the end, you collect the flow and send it to showResults.

Finally, add a new method below fetchDisneyCharacters. It should look like this:

private fun getFreshData() {
  lifecycleScope.launch {
    disneyRepository.getFreshData()
  }
}

This method triggers the API call to fetch new characters when users click the refresh action.

Call this method from onOptionsItemSelected. Replace the line // TODO Get fresh data from here with getFreshData(). It should look like this:

override fun onOptionsItemSelected(item: MenuItem): Boolean {
  if (item.itemId == R.id.refresh) {
    getFreshData()
  }
  return false
}

Run the app and you should see Mickey and Simba again. Click the refresh button in the top right corner to obtain new characters from the API. You’ll see that the list updates itself as soon as the new data has been inserted into the database.

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.

Room allows you to make your transaction methods suspending, and they can also call other suspending DAO methods. To see this in action, open CharacterDao.kt and add the following method:

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

It’s a simple @Transaction method that deletes all the existing entries and saves the characters passed in as arguments. Go to App.kt, and find and replace characterDao.saveCharacters(characters) in populateDatabase with characterDao.deleteAllAndUpdate(characters).

Build and run the app. Refresh the data, then kill the app and launch it again. After relaunching, you should see only two items in the list:

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