background
Started guide

Use coroutines to solve real coding problems

The first two articles focused on using coroutines to simplify code, keep the main thread safe on Android, and avoid task leaks. With this in mind, we think using coroutines is a great way to handle background tasks and simplify Android callback code.

While we have so far focused on what coroutines are and how to manage them, in this article we will show you how to use coroutines to accomplish some practical tasks. Coroutines, like functions, are a common feature in programming language features that you can use to implement anything you can with functions and objects. In real programming, however, there are always two types of tasks that are well suited to using coroutines:

  1. One shot requests are one-shot requests that are called only once, and then terminated when the request gets a result.
  2. The streaming request listens for changes and returns to the caller after the request is made, and does not end after the first result is obtained.

Coroutines are an excellent solution for handling these tasks. In this article, we’ll take a closer look at one-time requests and explore how to implement them using coroutines in Android.

One-time request

A one-time request is invoked only once, and the execution ends when the result is obtained. This pattern is much like calling a regular function — it is called once, executed, and then returned. Because it is similar to a function call, it is easier to understand than a streaming request.

A one-time request is invoked only once, and the execution ends when the result is obtained.

For example, you can think of it as a browser loading page. When you click on the link to this article, the browser sends a web request to the server, which then loads the page. Once the page data has been transferred to the browser, the browser has all the data it needs and stops talking to the back-end service. If the server subsequently modifies the content of the article, the new changes will not appear in the browser unless you refresh the browser page voluntarily.

Although this approach lacks the real-time push feature of streaming requests, it is still very useful. In Android applications you can use this approach to solve many problems, such as querying, storing, or updating data, and it is also useful for sorting lists.

Problem: Show an ordered list

Let’s explore how to build a one-time request with an example showing an ordered list. To make the example a little more concrete, let’s build an inventory application for store employees that can find items based on when they were last purchased, in ascending and descending order. Because there are so many items stored in this warehouse, sorting them takes nearly a second, so we need to use coroutines to avoid blocking the main thread.

In the application, all data is stored in the Room database. Since network requests are not involved, we do not need to make network requests and focus on programming patterns such as one-time requests. This example is simple because there is no network request, but it still shows what pattern to use to implement one-time requests.

To implement this with coroutines, you need to introduce ViewModel, Repository, and Dao into the coroutine. Let’s walk through them one by one and see how they can be integrated with coroutines.


class ProductsViewModel(val productsRepository: ProductsRepository): ViewModel() { private val _sortedProducts = MutableLiveData<List<ProductListing>>() val sortedProducts: LiveData<List<ProductListing>> = _sortedProducts /** * */ fun onSortAscending() = sortPricesBy(ascending =true)
   fun onSortDescending() = sortPricesBy(ascending = false)
 
   private fun sortPricesBy(ascending: Boolean) {
       viewModelScope.launch {
      // suspendAnd resume make the database request mainline safe, So the ViewModel don't need to care about thread safety question _sortedProducts. Value = productsRepository. LoadSortedProducts (ascending)}}}Copy the code

The ProductsViewModel is responsible for receiving events from the UI layer and requesting updated data from repository. It uses LiveData to store the currently sorted list data for display by the UI. When a new event occurs, sortProductsBy will start a new coroutine to sort the list and update the LiveData when the sort is complete. In this architecture, it is common to start coroutines using the ViewModel because doing so can be onCleared. These tasks do not need to continue after the user leaves the interface.

If you haven’t used LiveData before, you can check out this article by @ceruleanotter that explains how LiveData saves data for the UI — ViewModels: A Simple Example.

@CeruleanOtter :

Twitter.com/CeruleanOtt…

ViewModels: A Simple Example:

Medium.com/androiddeve…

This is a common pattern for using coroutines on Android. Since the Android Framework does not actively invoke suspend functions, you need to use coroutines in conjunction with UI events. The easiest way to do this is to start a new coroutine with an event, and the best place to handle this is the ViewModel.

Starting coroutines in the ViewModel is a very common pattern.

ViewModel actually uses ProductsRepository to fetch data, as shown in the following code:

Class ProductsRepository(Val productsDao: productsDao) {/** This is a normal suspended function, meaning that the caller must be in a coroutine. Repository is not responsible for starting or stopping coroutines because it has no control over the coroutine life cycle. This might be called in dispatchers. Main, and again it's mainline safe, because Room keeps the mainline safe for us. * /suspend fun loadSortedProducts(ascending: Boolean): List<ProductListing> {
       return if (ascending) {
           productsDao.loadProductsByDateStockedAscending()
       } else {
           productsDao.loadProductsByDateStockedDescending()
       }
   }
}
Copy the code

ProductsRepository provides a reasonable interface to interact with commodity data. In this application, everything is stored in the local Room database. It provides @DAO with two interfaces with different functions for different sorts.

Repository is an optional component of the Android architecture, and if you already integrate itor another module with similar functionality in your application, it should prefer to use suspended functions. Because Repository does not have a life cycle, it is just an object, so it cannot handle resource cleaning, so by default all coroutines started in Repository are likely to leak.

In addition to avoiding leaks, using suspended functions can also reuse Repository in different contexts. Anyone who knows how to create coroutines can call loadSortedProducts, such as the background tasks managed by WorkManager.

Repository should use hang functions to secure the main thread.

Note: Some of the data-saving operations in the background may continue to work after the user leaves the interface, in which case it doesn’t make sense to run outside of the application life cycle, so viewModelScope is a good choice in most cases.

Take a look at ProductsDao again, with the following example code:

@dao Interface ProductsDao {// Because this method is marked assuspend, Room will use its own scheduler to run the @query ("select * from ProductListing ORDER BY dateStocked ASC")
   suspendFun loadProductsByDateStockedAscending () : a List < ProductListing > / / because this method is marked in ordersuspend, Room will use its own scheduler to run the @query ("select * from ProductListing ORDER BY dateStocked DESC")
   suspend fun loadProductsByDateStockedDescending(): List<ProductListing>
}
Copy the code

ProductsDao is a room@DAO that provides two suspended functions. Because of the suspend modifier, Room guarantees that they are mainline safe, which means you can call them directly from dispatchers.main.

If you haven’t used Coroutines in Room, you can start by looking at this article by @fmuntenescu: Room 🔗 Coroutines

@FMuntenescu

twitter.com/FMuntenescu

Room 🔗 Coroutines

Medium.com/androiddeve…

Note, however, that the coroutine calling it will be executed on the main thread. So, if you do something time-consuming with the result, such as converting the contents of the list, you want to make sure it doesn’t block the main thread.

Note: Room uses its own scheduler to perform query operations on background threads. You should no longer use withContext(dispatchers.io) to call Room’s suspend query, which only complicates your code and slows down the query.

Room’s suspend functions are mainline safe and run in a custom scheduler.

One-time request mode

This is a complete pattern of one-time requests using coroutines in Android architectural components, which we added to ViewModel, Repository, and Room, each with a different division of responsibilities.

  1. The ViewModel starts the coroutine on the main thread and finishes execution as soon as it has a result;
  2. Repository provides a hang function that keeps the main thread safe;
  3. The database and network layers provide suspend functions that keep the main thread safe.

The ViewModel is responsible for starting coroutines and ensuring that they are cancelled when the user leaves the corresponding interface. It does not do time-consuming operations on its own, but relies on other hierarchies to do so. Once you have the results, you use LiveData to send the data to the UI layer. Because the ViewModel doesn’t do time-consuming operations, it starts the coroutine on the main thread so that it can respond to user events more quickly.

Repository provides suspended functions to access data, and typically does not start coroutines with long life cycles because they cannot be cancelled once started. Whenever Repository wants to do something time-consuming, such as converting the contents of a list, it should use withContext to provide a mainline-safe interface.

The data layer (network or database) will always provide suspend functions. When using Kotlin coroutines, make sure these suspend functions are mainline safe. Room and Retrofit follow this.

In a one-time request, the data layer only provides the suspend function, which the caller can only call again if he wants to get the latest value, just like the refresh button in the browser.

It’s worth taking the time to educate you about the one-time request pattern, which is the more common pattern in Android coroutines, and you’ll use it all the time.

The first bug is here

After testing, you deploy to production and feel fine for a few weeks until you receive a very strange bug report:

Title: 🐞 – Sort wrong!

Error reporting: When I clicked the sort button very quickly, the sorting result was occasionally wrong, which is not always repeated 🙃.

You study it and ask yourself what went wrong? The logic is simple:

  1. Start performing the sort operation requested by the user;
  2. Start sorting in the Room scheduler;
  3. Show the sorting results.

You think the bug doesn’t exist and you’re ready to shut it down because the solution is simple, “Don’t click the button so fast,” but you’re still worried that something is wrong. After adding some logging to your code and running a bunch of test cases, you finally know what the problem is!

It looks like the sorting results shown in the app are not real “sorting results”, but the last time the sorting was done. When the user clicks the button quickly, multiple sort operations are triggered simultaneously, which can end in any order.

When starting a new coroutine in response to UI events, consider what might happen if the user starts a new task before the previous one has completed.

This is a concurrency problem that has nothing to do with whether coroutines are used or not. If you use callbacks, Rx, or ExecutorService, you may still run into this bug.

There are many solutions to this problem, both in ViewModel and Repository. Let’s see how we can make one-time requests return results in the order we expect them to.

Best solution: Disable button

The core problem is that we sort it twice, and to fix it we can only sort it once. The simplest solution is to disable the button from emitting new events.

It seems simple, and it’s actually a good idea. The code implemented is simple and easy to test, as long as it can reflect the state of the button in the UI, it will solve the problem.

To disable the button, simply tell the UI if there is a sort request being processed in sortPricesBy, as shown in the following example:

Class ProductsViewModel(val productsRepository: productsRepository):ViewModel() {
   private val _sortedProducts = MutableLiveData<List<ProductListing>>()
   val sortedProducts: LiveData<List<ProductListing>> = _sortedProducts

   private val _sortButtonsEnabled = MutableLiveData<Boolean>()
   val sortButtonsEnabled: LiveData<Boolean> = _sortButtonsEnabled

   init {
       _sortButtonsEnabled.value = true} /** When the user clicks the sort button, call */ fun onSortAscending() = sortPricesBy(ascending =)true)
   fun onSortDescending() = sortPricesBy(ascending = false) private fun sortPricesBy(Ascending: Boolean) {viewModelScope.launch {// Disable the _sortButtonSenabled.value = as long as there is a sort going onfalseTry {_sortedProducts. Value = productsRepository. LoadSortedProducts (ascending)} finally {/ / after the sorting, Enable button _sortButtonSenabled.value =true}}}}Copy the code

Use _sortButtonsEnabled in sortPricesBy to disable buttons when sorting

Ok, this looks fine, just disable the button inside sortPricesBy when calling Repository.

In most cases, this is the best solution, but what if we want to fix the bug while keeping the button usable? This is a bit difficult, and I’ll look at how to do it in the rest of this article.

Note: This code shows the great advantage of booting from the main thread, as the button becomes unclickable immediately after being clicked. But if you switch to another scheduler, it’s still possible to send multi-click events when someone with a fast hand is working on a slower phone.

Concurrent mode

The next few chapters cover more advanced topics. If you’re new to coroutines, you don’t need to understand this part. Using the Disable button is the best solution to most of these problems.

In the rest of the article we’ll explore ensuring that a one-time request works without disabling the button. We can avoid the concurrency problems we just encountered by controlling when we let coroutines run (or not).

There are three basic patterns that allow us to ensure that only one request is made at a time:

  1. Cancel previous tasks before starting more coroutines;
  2. Queue up the next task to wait for the previous task to complete;
  3. If there is a task in progress, return to that task instead of starting a new one.

Once you’ve introduced these three scenarios, you may find that their implementation is quite complex. To focus on design patterns rather than implementation details, I created a GIST to provide implementations of these three patterns as reusable abstractions.

Option 1: Cancel the previous task

In the sorting case, fetching a new event means that the previous sorting task can be cancelled. After all, the user has already indicated that they do not want the last sorting result, so there is no point in continuing the last sorting operation.

To cancel the last request, we first have to trace it in some way. The cancelPreviousThenRun function in GIST does just that.

Let’s see how you can use it to fix this bug:

// For sorting and filtering, new requests come in and cancel the previous one, which is a good solution. class ProductsRepository(val productsDao: ProductsDao, val productsApi: ProductsService) { var controlledRunner = ControlledRunner<List<ProductListing>>()suspendFun loadSortedProducts(Ascending: Boolean): List<ProductListing> {// Cancel the previous sort task before starting the new sortreturn controlledRunner.cancelPreviousThenRun {
           if (ascending) {
               productsDao.loadProductsByDateStockedAscending()
           } else {
               productsDao.loadProductsByDateStockedDescending()
           }
       }
   }
}
Copy the code

Use cancelPreviousThenRun to ensure that only one sort task is running at a time

Look at the code implementation of cancelPreviousThenRun in GIST, and you can learn how to keep track of the task at work.

/ / see the complete implementation in the at / / https://gist.github.com/objcode/7ab4e7b1df8acd88696cb0ccecad16f7 to check the complete implementationsuspend fun cancelPreviousThenRun(block: suspend() -> T): T {// If this is an activeTask, cancel it because its result no longer requires activeTask? .cancelAndJoin() // ...Copy the code

In short, it keeps track of the current sort through the member variable activeTask. Whenever a new sort is started, the cancelAndJoin operation is immediately performed on all tasks in the current activeTask. This will cancel the sorting task in progress before starting a new sort.

Using an abstract implementation like ControlledRunner to encapsulate the logic is a better way to do it than simply mixing concurrency and application logic.

Choose to use abstractions to encapsulate code logic and avoid intermingling concurrent and application logic code.

Note: This pattern is not suitable for global singletons because unrelated callers should not cancel each other out.

Option 2: Queue the next task

Here is a solution that always works for concurrency problems.

Queue tasks so that only one task can be processed at a time. Just like waiting in line at a mall, requests will be processed in the order they are queued up.

For this particular sort problem, option 1 is actually a better option than this one, but it’s worth mentioning because it’s always effective at solving concurrency problems.

Note: this method is not a good solution for sorting or filtering, but it is very suitable for dealing with the concurrency problems caused by network requests. class ProductsRepository(val productsDao: ProductsDao, val productsApi: ProductsService) { val singleRunner = SingleRunner()suspendFun loadSortedProducts(Ascending: Boolean): List<ProductListing> {// Wait for the previous sorting task to complete before starting a new taskreturn singleRunner.afterPrevious {
           if (ascending) {
               productsDao.loadProductsByDateStockedAscending()
           } else {
               productsDao.loadProductsByDateStockedDescending()
           }
       }
   }
}
Copy the code

Whenever a new sort is performed, a SingleRunner instance is used to ensure that only one sort task is in progress at the same time.

It uses Mutex, which can be thought of as a one-way ticket (or lock) that the coroutine must acquire in order to enter the block. If a coroutine is running and another coroutine tries to enter that block of code, it must suspend itself until all coroutines holding Mutex complete their task and release the Mutex.

Mutex guarantees that only one coroutine will run at a time, and that it will end in the order in which it was started.

Scenario 3: Reuse the previous task

The third option to consider is to reuse the previous task, that is, the new request can reuse the existing task, for example, the previous task is half completed in a new request, so that the request directly reuse the half-completed task, a lot of trouble.

This doesn’t make much sense for sorting, but it does for network data requests.

For our inventory application, users needed a way to get the latest inventory data from the server. We provide a simple operation like a refresh button that allows users to initiate a new web request with a single click.

The Disable button simply solves the problem while the request is in progress. But if we don’t want to, or can’t, reuse existing requests in this way.

Check out the following sample code from GIST that uses joinPreviousOrRun:


class ProductsRepository(val productsDao: ProductsDao, val productsApi: ProductsService) {
   var controlledRunner = ControlledRunner<List<ProductListing>>()

   suspendFun fetchProductsFromBackend(): List<ProductListing> {// If there is already a running request, return it. If not, start a new request.return controlledRunner.joinPreviousOrRun {
           val result = productsApi.getProducts()
           productsDao.insertAll(result)
           result
       }
   }
}
Copy the code

The above code acts the opposite of cancelPreviousAndRun, which uses the previous request directly and abandons the new one, whereas cancelPreviousAndRun abandons the previous request and creates a new one. If there is already a running request, it waits for the request to complete and returns the result directly. A new request is created to execute the code block only if there are no running requests.

You can see how joinPreviousOrRun works when it starts and return it directly if there are any working tasks in activeTask.

/ / https://gist.github.com/objcode/7ab4e7b1df8acd88696cb0ccecad16f7#file-concurrencyhelpers- kt-l124

suspend fun joinPreviousOrRun(block: suspend() -> T): T {// If activeTask exists, return its result directly and do not execute the code block activeTask? .let {return it.await()
    }
    // ...
Copy the code

This pattern is suitable for requests that query commodity data by ID. You can use a map to establish an ID to Deferred mapping, and then use the same logic to track prior request data for the same product.

Direct reuse of previous tasks can effectively avoid repeated network requests.

The next step

In this article, we explored how to use Kotlin coroutines to implement one-time requests. We implemented how to start coroutines in the ViewModel and then provide open suspend Function in Repository and Room DAOs, thus forming a complete programming paradigm.

For most tasks, using the Kotlin coroutine on Android will suffice. These methods, like sorting above, can be used in many scenarios, and you can use them to solve problems such as querying, saving, and updating network data.

Then we looked at possible bugs and suggested solutions. The simplest (and often best) solution is to make the change directly from the UI and disable the button directly at sort runtime.

Finally, we explored some advanced concurrency patterns and showed you how to implement them in Kotlin coroutines. Although the code is a bit complex, it is a good introduction to some high-level coroutine topics.

In the next article, we’ll take a look at streaming requests and explore how to use the liveData constructor, so stay tuned for updates for those interested.

Use Kotlin coroutines to Improve application performance