Coroutines are a concurrent design pattern that you can use on Android to simplify asynchronous execution of code. Kotlin version 1.3 adds Coroutines and is based on established concepts in other languages.
On Android, coroutines help solve two main problems:
- Manage long-running tasks, otherwise it may block the main thread and cause the application to freeze.
- Provides master security, or safely invokes network or disk operations from the main thread.
This topic describes how to solve these problems using Kotlin coroutines, allowing you to write clearer and cleaner application code.
Manage long-running tasks
On Android, each application has a main thread to handle the user interface and manage user interactions. If your application has assigned too much work to the main thread, the application may be significantly stuttering or running slowly. Network requests, JSON parsing, reading or writing from a database, or even just iterating over large lists can cause the application to run slowly, resulting in visible slow or frozen UIs to respond slowly to touch events. These long-running operations should run outside the main thread.
The following example shows a simple coroutine implementation of a hypothetical long-running task:
suspend fun fetchDocs() { // val result = get("") // for `get` show(result) // } suspend fun get(url: String) = withContext() { /* ... */ }
The coroutine builds regular functionality by adding two operations to handle long-running tasks. In addition to invoke (or call) and return, the coroutine also adds suspend and resume:
- suspend pauses the execution of the current coroutine and saves all local variables.
- resume resumes the suspended coroutine from the suspended collaboration.
You can only call the suspend function from other suspend functions, or use a coroutine builder such as startup to start a new coroutine.
In the example above, get() is still running on the main thread, but it hangs the coroutine before starting the network request. When a network request is completed, get resumes the suspended coroutine instead of using a callback to notify the main thread.
Kotlin uses a stack framework to manage functions that run with any local variables. When the coroutine is suspended, the current stack frame is copied and saved for later use. When recovering, the stack frame will be copied back from the save location and the function will start running again. Even if the code looks like a normal sequential blocking requests, coroutines ensure that network requests avoid blocking the main thread.
Use coroutines for main-safety
Kotlin coroutines use schedulers to determine which threads are used for coroutine execution. To run the code outside the main thread, you can tell the Kotlin coroutine to perform work on the Default or IO scheduler. In Kotlin, all coroutines must be run in the scheduler, even if they run on the main thread. Coroutines can be paused and schedulers are responsible for restoring them.
To specify where the coroutine should run, Kotlin provides three schedulers that can be used:
- - Use this scheduler to run coroutines on the main Android thread. This should only be used to interact with the UI and perform fast work. Examples include calling a suspend function, running Android UI framework operations, and updating LiveData objects.
- - This scheduler has been optimized to perform disk or network I/O outside the main thread. Examples include using Room components, reading or writing files, and running any network operations.
- - This scheduler has been optimized to perform CPU-intensive work outside the main thread. Example use cases include sorting lists and parsing JSON.
Continuing with the previous example, you can use the scheduler to redefine the get function. Inside the body of the get, call withContext() to create a block running on the IO thread pool. Any code placed in that block is always executed through the IO scheduler. Since withContext itself is a suspend function, the function get is also a suspend function.
Using coroutines, you can schedule threads with fine-grained control. Because withContext() allows you to control the thread pool of any line of code without introducing a callback, you can apply it to very small functions such as reading from a database or executing network requests. A good practice is to use withContext() to ensure that each function is master-safe, which means you can call the function from the main thread. This way, the caller never needs to consider which thread should be used to execute the function.
In the previous example, fetchDocs() is executed on the main thread; however, it can safely call get, which performs network requests in the background. Because coroutines support suspend and recovery, as long as the withContext block is completed, coroutines on the main thread will be restored with get results.
Important Note: Using suspend does not tell Kotlin to run functions on background threads. It is normal for the pause function to run on the main thread. It is also common to start coroutines on the main thread. When you need master security, such as when reading or writing to disk, performing network operations, or running CPU-intensive operations, you should always use withContext() inside the suspend function.
withContext() does not add additional overhead compared to the equivalent callback-based implementation. Furthermore, in some cases, the withContext() call can be optimized instead of the equivalent callback-based implementation. For example, if a function makes ten calls to the network, you can tell Kotlin to switch threads only once by using external withContext(). Then, even if the network library uses withContext() multiple times, it still stays on the same scheduler and avoids switching threads. In addition, Kotlin optimizes switching between and to avoid thread switching as much as possible.
Important: Using a scheduler using or equal thread pool does not guarantee that the block will be executed on the same thread from top to bottom. In some cases, the Kotlin coroutine may move the execution to another thread after being paused and resumed. This means that thread local variables may not point to the same value of the entire withContext() block.
Specify CoroutineScope
When defining a coroutine, it must also specify its CoroutineScope. CoroutineScope manages one or more coroutines. You can also use CoroutineScope to start a new coroutine within that scope. However, unlike schedulers, CoroutineScope does not run coroutines.
An important feature of CoroutineScope is to stop coroutine execution when the user leaves the content area in the application. With CoroutineScope, you can make sure that any running operations are stopped correctly.
Use CoroutineScope with Android architecture components
On Android, you can associate a CoroutineScope implementation with the component lifecycle. This avoids memory leaks or performs additional work for activities or fragments that are no longer relevant to the user. With Jetpack components, they naturally fit the ViewModel. Since the ViewModel is not destroyed during configuration changes (such as screen rotation), you don't have to worry about the coroutine being cancelled or restarted.
Range knows every coroutine they start. This means that you can unselect everything you launch in scope at any time. The scope propagates itself, so if one coroutine starts another coroutine, the two coroutines have the same scope. This means that even if other libraries start coroutines from your scope, you can cancel them at any time. This is especially important if you run coroutines in ViewModel. If the ViewModel is destroyed because the user leaves the screen, all the asynchronous work it is performing must be stopped. Otherwise, you will waste resources and potentially leak memory. If you should continue to work asynchronously after destroying the ViewModel, it should be done in the lower layer of the application architecture.
Warning: Collaboration is cancelled by throwing a CancellationException. The exception handler that catches an exception or Throwable is triggered during coroutine cancellation.
Using the KTX library components for Android architecture, you can also use the extended property viewModelScope to create a coroutine that can run until the ViewModel is destroyed.
Start a coroutine
You can start the coroutine in one of two ways:
- Launch starts a new coroutine and does not return the result to the caller. Any work that is considered "launched and forgotten" can be started using launch.
- async starts a new coroutine and allows you to return the result using a suspend function called await.
Usually, you should start a new coroutine from a regular function because the regular function cannot be called to wait. Asynchronous is used only when performing parallel decomposition inside another coroutine or inside a suspended function.
Based on the previous example, here is a coroutine with the viewModelScope KTX extension attribute, which uses launch to switch from regular functions to coroutine:
fun onDocsNeeded() { { // fetchDocs() // (suspend function call) } }
Warning: The way exceptions are started and processed asynchronously is different. Since async expects to finally call await at some point, it keeps the exceptions and re-throws them in the await call. This means that if you use await to start a new coroutine from a regular function, the exception may be removed silently. These discarded exceptions will not appear in the crash metrics, nor in logcat.
Parallel decomposition
When the function returns, all coroutines started by the suspended function must be stopped, so you may need to ensure that these coroutines complete before returning. With structured concurrency in Kotlin, you can define a coroutineScope that starts one or more coroutines. Then, using await() (for a single coroutine) or awaitAll() (for multiple coroutines), it is guaranteed that these coroutines will complete before returning from the function.
For example, let's define a coroutineScope that gets two documents asynchronously. By calling await() on each delayed reference, we guarantee that both asynchronous operations are completed before returning the value:
suspend fun fetchTwoDocs() = coroutineScope { val deferredOne = async { fetchDoc(1) } val deferredTwo = async { fetchDoc(2) } () () }
Even if fetchTwoDocs() starts a new coroutine asynchronously, the function uses awaitAll() to wait for those startup coroutines to complete before returning. But please note that even if we do not call awaitAll(), the coroutineScope builder does not resume coroutines calling fetchTwoDocs until all new coroutines complete.
Additionally, coroutineScope catches any exceptions thrown by the coroutine and routes them back to the caller.
For more information about parallel decomposition, see Writing suspended functions.
Architectural components with built-in support
Some architectural components, including ViewModel and Lifecycle, contain built-in support for coroutines through their own CoroutineScope members.
For example, a ViewModel contains a built-in viewModelScope. This provides a standard way to start coroutines within the ViewModel scope, as shown in the following example:
class MyViewModel : ViewModel() { fun launchDataLoad() { { sortList() // Modify UI } } /** * Heavy operation that cannot be done in the Main Thread */ suspend fun sortList() = withContext() { // Heavy work } } LiveDataAlso used withliveDataBlock collaborative program: liveData { // runs in its own LiveData-specific scope }
original
The above is all the content of this article. I hope it will be helpful to everyone's study and I hope everyone will support me more.