Using coroutines in Android development | Background introduction

Using coroutines in Android development | Background introduction

This article is the first part of a series on Android coroutines. It will mainly introduce how coroutines work and what problems they mainly solve.

[[321378]]

What problems do coroutines solve?

Coroutines in Kotlin provide a new way to handle concurrency, and you can use it on the Android platform to simplify asynchronous execution of code. Coroutines were introduced in Kotlin 1.3, but the concept has been around since the dawn of the programming world, with the earliest programming language using coroutines dating back to Simula in 1967.

In the past few years, the concept of coroutines has gained momentum and has been adopted by many mainstream programming languages, such as Javascript, C#, Python, Ruby, and Go. Kotlin's coroutines are based on established concepts from other languages.

  • Coroutines: https://kotlinlang.org/docs/reference/coroutines-overview.html
  • Simula: https://en.wikipedia.org/wiki/Simula
  • Javascript: https://javascript.info/async-await
  • C#: https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/
  • Python: https://docs.python.org/3/library/asyncio-task.html
  • Ruby: https://ruby-doc.org/core-2.1.1/Fiber.html
  • Go: https://tour.golang.org/concurrency/1

On the Android platform, coroutines are mainly used to solve two problems:

  • Handle long running tasks, which often block the main thread;
  • Main-safety means that any suspend function can be safely called from the main thread.

Let’s take a deeper look at the above questions and see how we can apply coroutines to our code.

Handling time-consuming tasks

Getting web content or interacting with a remote API involves sending network requests, getting data from a database or reading image resources from disk involves file read operations. Usually we classify such operations as time-consuming tasks - the application will stop and wait for them to complete, which will take a lot of time.

Today’s phones process code much faster than they process network requests. For example, on the Pixel 2, a single CPU cycle takes less than 0.0000000004 seconds, a number that is difficult to express in human language, but if you express network requests in “blinks of an eye,” which is about 400 milliseconds (0.4 seconds), it’s easier to understand how fast the CPU runs. In just the blink of an eye, or the time it takes for a slower network request to complete, the CPU has completed more than 1 billion clock cycles.

Each application in Android runs a main thread, which is mainly used to process the UI (such as drawing the interface) and coordinate user interaction. If there are too many tasks to be processed on the main thread, the application will run slower and look like it is "stuck", which greatly affects the user experience. Therefore, if you want the application to run smoothly, the animation can run smoothly, or respond quickly to user click events, you must ensure that those time-consuming tasks do not block the main thread.

To process network requests without blocking the main thread, a common approach is to use callbacks. A callback is to execute your callback code at a later time. Using this method, the code for requesting data from the developer.android.google.cn website will be similar to the following:

  1. class ViewModel: ViewModel() {
  2. fun fetchDocs() {
  3. get("developer.android.google.cn") { result - >  
  4. show(result)
  5. }
  6. }
  7. }

In the above example, even though get is called in the main thread, it uses another thread to perform the network request. Once the network request returns a result, the callback code will be called by the main thread when the result is available. This is a good way to handle time-consuming tasks. Libraries like Retrofit use this method to help you handle network requests without blocking the main thread.

Retrofi: https://square.github.io/retrofit/

Use coroutines to handle coroutine tasks

Using coroutines can simplify your code to handle time-consuming tasks like fetchDocs. Let's rewrite the above code using coroutines to explain how coroutines handle time-consuming tasks, making the code clearer and more concise.

  1. // Dispatchers.Main
  2. suspend fun fetchDocs() {
  3. // Dispatchers.Main
  4. val result = get ("developer.android.google.cn")
  5. // Dispatchers.Main
  6. show(result)
  7. }
  8. // See this code in the next section
  9. suspend fun get(url: String) = withContext(Dispatchers.IO){/*...*/}

In the above example, you may have many questions. Doesn't it block the main thread? How does the get method return the result without waiting for the network request and thread blocking? In fact, it is the coroutine in Kotlin that provides this method of executing code without blocking the main thread.

Coroutines add two new operations to regular functions. In addition to invoke (or call) and return, coroutines add suspend and resume:

  • suspend — also known as suspend or pause, used to suspend the execution of the current coroutine and save all local variables;
  • resume — Used to resume a suspended coroutine from where it was suspended.

Kotlin implements the above functions by adding the suspend keyword. You can only call other suspend functions in a suspend function, or start a new coroutine through a coroutine constructor (such as launch).

  • launch: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html

(1) Use suspend and resume together to replace callbacks

In the example above, get still runs on the main thread, but it suspends the coroutine before starting the network request. When the network request is complete, get resumes the suspended coroutine instead of using a callback to notify the main thread.

The above animation shows how Kotlin uses suspend and resume instead of callbacks.

Observing the execution of fetchDocs in the figure above, you can understand how suspend works. Kotlin uses stack frames to manage which function to run and all local variables. When a coroutine is suspended, the current stack frame is copied and saved for later use. When a coroutine is resumed, the stack frame is copied back from where it was saved, and the function starts running again. In the animation above, when all coroutines under the main thread are suspended, the main thread has no pressure to handle screen drawing and click events. So using the above suspend and resume operations instead of callbacks looks very refreshing.

(2) When all coroutines under the main thread are suspended, the main thread will have no pressure to handle other events

Even though the code may look like a normal sequential blocking request, coroutines ensure that network requests avoid blocking the main thread.

Next, let’s look at how coroutines ensure main-safety and discuss the scheduler.

Using coroutines to ensure main thread safety

In Kotlin coroutines, it is usually safe for the main thread to call well-written suspend functions. No matter what those suspend functions do, they should allow any thread to call them.

However, there are many things in our Android applications that are too slow to process and should not be done on the main thread, such as network requests, parsing JSON data, reading and writing from the database, and even traversing large arrays. These operations that will take a long time to execute and make users feel "stuck" should not be executed on the main thread.

Using suspend does not mean telling Kotlin to execute a function on a background thread. What needs to be emphasized here is that the coroutine will run on the main thread. In fact, when you want to respond to a UI event and start a coroutine, using Dispatchers.Main.immediate is a very good choice, so that even if the time-consuming task that requires the main thread to be safe is not executed in the end, the user can be provided with available execution results in the next frame.

Dispatchers.Main.immediateh:

ttps://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-main-coroutine-dispatcher/immediate.html

(1) The coroutine will run in the main thread. Suspend does not mean background execution.

If you need to process a function that is too time-consuming to execute on the main thread, but you want to make it main-safe, you can let Kotlin coroutines perform work on the Default or IO scheduler. In Kotlin, all coroutines must run in a scheduler, even if they run on the main thread. Coroutines can suspend themselves, and the scheduler is responsible for resuming them.

Kotlin provides three dispatchers that you can use to specify where a coroutine should be run:

If you use suspend functions, RxJava, or LiveData in Room, Room will automatically ensure main thread safety.

Networking libraries like Retrofit and Volley manage their own threads, so you don't need to worry about main-thread safety when calling code from these libraries from Kotlin coroutines.

  • Dispatcher: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-dispatcher/
  • suspend: https://medium.com/androiddevelopers/room-coroutines-422b786dc4c5
  • RxJava: https://medium.com/androiddevelopers/room-rxjava-acb0cd4f3757
  • LiveData: https://developer.android.google.cn/topic/libraries/architecture/livedata#use_livedata_with_room
  • Room: https://developer.android.google.cn/topic/libraries/architecture/room
  • Retrofit: https://square.github.io/retrofit/
  • Volley: https://developer.android.google.cn/training/volley

Continuing with the previous example, you can redefine the get function using a dispatcher. In the body of get, call withContext(Dispatchers.IO) to create a block that runs in the IO thread pool. Any code you put inside that block is always executed through the IO dispatcher. Since withContext itself is a suspend function, it uses coroutines to ensure main thread safety.

  1. // Dispatchers.Main
  2. suspend fun fetchDocs() {
  3. // Dispatchers.Main
  4. val result = get ("developer.android.google.cn")
  5. // Dispatchers.Main
  6. show(result)
  7. }
  8. // Dispatchers.Main
  9. suspend fun get(url: String) =
  10. // Dispatchers.Main
  11. withContext(Dispatchers.IO) {
  12. // Dispatchers.IO
  13. }
  14. // Dispatchers.Main

Coroutines allow you to schedule threads with fine-grained control. Since withContext lets you control the thread pool for any line of code without introducing callbacks, you can apply it to very small functions, such as reading data from a database or performing a network request. A good practice is to use withContext to ensure that every function is main-safe, which means that you can call each function from the main thread. This way, the caller does not need to worry about which thread should be used to execute the function.

In this example, fetchDocs will execute on the main thread, however, it can safely call get to perform the network request in the background. Because coroutines support suspend and resume, the coroutine on the main thread will resume execution once the withContext block completes.

(2) It is usually safe for the main thread to call a well-written suspend function.

It is useful to ensure that each suspend function is main-thread safe. If a task needs to contact the disk, the network, or even just takes up too much CPU, you should use withContext to ensure that it can be called safely from the main thread. This is also the principle followed by code libraries such as Retrofit and Room. If you follow this in the process of writing code, your code will become very simple and will not mix thread issues with application logic. At the same time, under this principle, coroutines can also be called freely from the main thread, network requests or database operation codes become very concise, and ensure that users will not feel "stuck" when using the application.

Performance of withContext

withContext performs similarly to callbacks or main-safe RxJava. In some cases, withContext calls can even be optimized to outperform equivalent callback-based implementations. If a function needs to make 10 calls to a database, you can use an external withContext to have Kotlin switch threads only once. This way, even if the database codebase keeps calling withContext, it stays on the same dispatcher and follows the fast path to ensure performance. In addition, switching between Dispatchers.Default and Dispatchers.IO has also been optimized to minimize the performance loss caused by thread switching.

Next step

This article introduces what kind of problems can be solved using coroutines. Coroutines are an old concept in the field of computer programming languages, but they have become popular again because they can make the code of network requests more concise.

On Android, you can use coroutines to handle two common problems:

  • Simplify time-consuming tasks such as network requests, disk reads, and even large JSON data parsing;
  • Ensure the safety of the main thread, so that the execution of the main thread will not be blocked without increasing the complexity of the code and ensuring the readability of the code.

Next article: "Using coroutines in Android development | Getting started guide"

[This article is an original article from the 51CTO column "Google Developers". Please contact the original author (WeChat public account: Google_Developers) for reprinting.]

Click here to read more articles by this author

<<:  Is the new iPhone SE your next iPhone?

>>:  What technologies does Alibaba have in its Flutter system construction?

Recommend

How does event planning create a hit?

Organizing activities is a good choice. It can in...

Alcoholics spend their lives in a bottle, not knowing it is a disease

Alcohol addiction has always been considered a mo...

Operational tips: 5 ways to quickly increase community activity

I often hear people say that they joined a commun...

49 popular tools that new media operators must know!

Tools 1. Data Index 1. Baidu Index : a very usefu...

Humanoid robots will run a half marathon with humans

On March 4, at a press conference held by the Bei...

What do scientists do at night when they are not publishing papers?

This article is a review article published in Gen...

Morty. Index investment series course, start systematic learning, fund investment

Morty. Index investment series, start systematic l...

Analysis of Didi Qingju Bike User Growth System!

The “last mile” of public transportation is the m...

50 million alliance: 0 fans bring over 10,000 GMV to the community

Community introduction: How to quickly get more t...

How to distinguish between Douyin information flow delivery and Douyin+?

According to official statistics, the official da...