craigrussell

Technical blog from Craig Russell.

 

Should Repositories expose suspend functions?

When using coroutines in Android, you have to choose which functions will be marked as suspend. How and where you do this across your app is an architectural decision. This post discusses whether the Repository layer should expose suspend functions or not.

What is a Repository in this context?

A common layer in Android app architectures is a Repository layer. Exactly where it sits in your layers, and who its immediate neighbours are, might vary depending on your architecture flavour, but a Repository will typically sit somewhere below your view layer and above your data source layer. Its purpose, in part, is to let you decouple and encapsulate the exact mechanism of data storage from the layers above.

high level architecture diagram showing repository below UI layer and above data source

Image Source

Your Activities, Fragments, ViewModels et al. should not need to know precisely how you are storing data; that is an implementation detail that can be forgotten about at those higher view layers. The view layer can talk to a Repository to get and save data, all without the need to understand Room DB, DataStore, SharedPreferences, Files etc…

This post focuses on suspend functions, but more generally covers whether to have coroutine-specific features exposed by your Repository APIs, and so extends to whether to return Flow or not.

Data access needs to consider threading

In an ideal world, reading and writing data would be so quick to perform that we could do it from the main thread, but that’s not the case. Often, data access involves making network calls and even when all operations are done on local storage only, you still can’t be guaranteed they’ll happen quickly enough to be allowable on the main thread (data access speeds are so variable on Android, IO disk resource contention might slow it down etc…).

Whether making remote network calls or reading/writing from local storage, we need to assume that it can take a few moments to complete and avoid using the main thread.

Who chooses the thread?

If we consider the scenario where an Activity talks to a ViewModel which talks to a Repository which talks to a Room DAO, who is responsible for ensuring the data access happens off of the main thread?

Specifically at the Repository level, should the Repository internally handle the threading or should that be left up to the caller of the Repository to get it right?

Let’s say we have this Repository, which uses a Room DAO to persist data.

class BookmarksRepository(private val bookmarksDao: BookmarksDao) {
    
    fun insert(bookmark: Bookmark) {
        bookmarksDao.insert(bookmark)
    }
    
}

The insert function in this example is not marked as suspend. Should it be?

Arguments against exposing suspend functions

Forces callers to use coroutines

An argument against exposing suspend functions from the Repository is that it forces all callers to be coroutine-aware.

Calling code should own threading decisions

Another argument is that the calling code can (and should) make decisions about the threading

Arguments for exposing suspend functions

The best decisions on threading requires knowledge of internal workings

The caller can’t always make the best threading decisions without knowing the internal workings of the Repository (which we want to avoid).

For example, it might seem reasonable for the calling code to always ensure it executes the Repository function on a background thread (e.g, Dispatchers.IO), but that might create inefficiency. If using Room for example, it is wasteful to switch dispatcher at a higher level because Room handles this internally.

your code should not use withContext(Dispatchers.IO) to call suspending room queries. It will complicate the code and make your queries run slower. (Source)

In other cases, you might need to perform some complex operations after retrieving the data from the data source, but before returning it from the Repository.

Forces correctness on calling code

Another argument for exposing suspend functions is that it’s hard for callers to get threading right by themselves and the use of suspend can force correctness upon them.

Use of suspend alone could still allow a caller to call it from a coroutine executing on the main thread (e.g., Dispatchers.Main). So how does the suspend function offer a way to reduce errors here?

*Main-safety

The ability to call Repository functions in a main-safe way isn’t enforced by the compiler; you as developer of the Repository still have to do that. It’s trivial to do with withContext, but you still have to do it. But it’s easier to get that right once when writing the Repository function than having to get it right from every caller that calls it in the future.

Interestingly, Google go so far as to state that all suspend functions should be main-safe, but that’s a different debate for a different day. 😅

Suspend functions should be main-safe, meaning they’re safe to call from the main thread. If a class is doing long-running blocking operations in a coroutine, it’s in charge of moving the execution off the main thread using withContext. This applies to all classes in your app, regardless of the part of the architecture the class is in.

You are made to think

An immediate effect of trying to call a suspend function is that you get a big hint the function might not complete quickly, and that this requires you to think.

Don’t keep me in suspense; what’s the recommendation?

While I get the sentiments behind some counter-arguments, my recommendation is to expose suspend functions from the Repository layer.

It is also a recommended practice by Google, in their Coroutines Best Practices guide: https://developer.android.com/kotlin/coroutines/coroutines-best-practices

Home