Introduction to Kotlin coroutines

Foreword

1. What is Kotlin coroutine?

2. Specific introduction to Kotlin coroutines

Advantages of 2.0 Kotlin Coroutines

2.1 Add dependencies

2.2 Start coroutine

2.3 CoroutineScope

2.4 Job (job)

2.5 CoroutineDispatcher (scheduler)

2.6 suspend keyword

3. Jetpack library supports coroutines

4. Retrofit’s support for coroutines

Reference articles


Foreword

Before introducing the Kotlin coroutine, let’s take a look at what a coroutine in a broad sense is.

  • A solution for handling concurrent tasks in a program
  • It is more lightweight than threads. The context switching of threads requires the participation of the kernel (the core of the operating system), and The context switching of the coroutine is completely controlled by the user, which avoids a large number of interruptions and reduces the resources consumed by thread context switching and scheduling. Thread context switching is as follows:

Suspend a thread, store the thread’s state (context) in the CPU somewhere in memory;
Resume a thread, retrieve the next thread’s context in memory and store it in the CPU’s Resume in register;
Jump to the location pointed to by the program counter (that is, jump to the line of code when the thread was interrupted) to resume the thread.

Thread context: thread Id + thread state + stack + register state, etc.

1. What is Kotlin coroutine?

  • Kotlin’s coroutine and generalized coroutine are not the same thing. Kotlin’s coroutine is a thread framework, which is essentially just a package based on the native Java Thread API, similar to Android’s Handler Series API.
  • It is mentioned in the official Android document that coroutine is a concurrency design pattern, and Kotlin coroutine can be used on the Android platform to simplify asynchronously executed code.

2. Specific introduction to Kotlin coroutines

Advantages of 2.0 Kotlin Coroutines

  • The biggest advantage of the coroutine is that it canTo help us automatically cut threads, write asynchronous code in a way that looks synchronous, without writing callbacks.
  • It is very difficult to use threads to initiate two parallel requests. Generally, you may choose to request sequentially. If it is a coroutine, it is easy to use two async() functions to start two coroutine synchronization requests.
  • Using coroutine scope to manage coroutines uniformly can reduce memory leaks.
  • Many Jetpack libraries include extensions that provide full coroutine support.

2.1 Add dependencies

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'

2.2 Start Coroutine

  • launch()

It needs to be started in CoroutineScope, launch returns a “Job”, which is used for coroutine supervision and cancellation, and is used in scenarios with no return value.

GlobalScope. launch {
    
}
  • async()

It needs to be started in CoroutineScope, async returns a Job subclass “Deferred”, and the return value when completed can be obtained through await().

val res = GlobalScope. async {

}
  • runBlocking()

The runBlocking function will create a coroutine that blocks the current thread.

runBlocking {

}

2.3 CoroutineScope

Used to manage the coroutine life cycle.

  • GlobalScope

Defined as a singleton object, there is only one object instance in the entire JVM virtual, and the life cycle runs through the entire JVM, so you need to be alert to memory leaks when using it! ! !

  • CoroutineScope

Custom coroutine scope, you can specify the context parameters of the coroutine scope.

val scope = CoroutineScope(Dispatchers.IO + CoroutineName("MyCoroutineScope"))
scope. launch {

}
scope. cancel()
  • MainScope()

Open a scope to create a coroutine based on the main thread

open class BaseActivity: AppCompatActivity(), CoroutineScope by MainScope(){ //You can build a BaseActivity that implements the CoroutineScope interface to conveniently call launch to open coroutines
    override fun onDestroy() {
        super. onDestroy()
        cancel()
    }
}
  • coroutineScope()

will inherit the outer coroutine scope and create a subscope. Blocks the current coroutine until all code and child coroutines in the scope run, but does not affect other coroutines and threads. It can be used to open coroutines in the suspend function.

2.4 Job (job)

  • The Job of a coroutine is part of the context and can be retrieved in context using the coroutineContext[Job] expression.
  • Calling the launch function will return a Job object, which represents a task of a coroutine, which can be used to view the status of the coroutine and control the coroutine.
//View coroutine status
isActive: Boolean //whether alive
isCancelled: Boolean //Cancel
isCompleted: Boolean //Whether it is completed
children: Sequence // all child jobs

// control coroutine
cancel() // cancel the coroutine
join() // Block the current thread until the execution of the coroutine is completed
cancelAndJoin() // Combine the two, cancel and wait for the coroutine to complete
cancelChildren() // Cancel all child coroutines, you can pass in CancellationException as the reason for cancellation
attachChild(child: ChildJob) // attach a child coroutine to the current coroutine

2.5 CoroutineDispatcher (scheduler)

The coroutine scheduler is part of the coroutine context, which determines which thread or threads the associated coroutine is executed on. The coroutine scheduler can limit coroutine execution to a specific thread, dispatch it to a thread pool, or let it run unrestricted.

  • There are four schedulers for Kotlin coroutines

  • withContext

Unlike launch, async, and runBlocking, withContext does not create new coroutines, and is often used to switch the running thread of code execution.
It is also a suspending method until the end returns a result. Multiple withContexts are executed serially, so it is very suitable for the situation where a task depends on the result returned by the previous task.

2.6 suspend keyword

  • suspend means being suspended, and the so-called being suspended means cutting a thread; when running to the suspend function, the coroutine will be in a suspended state (will not block the current thread, break away from the current thread, and open a new thread), after the result is returned, automatically switch back to the current thread (the coroutine will help me post a Runnable, Let the rest of my code continue to return to the previous thread to execute.) When the coroutine is suspended (waiting), the current thread will return to the thread pool. When the wait is over, the coroutine will start from an idle thread in the thread pool recover.
fun main() {
    println("start ${Thread. currentThread(). name}")
    GlobalScope. launch(Dispatchers. Main) {
        delay(1000)// Suspend the function, open another thread to execute, will not block the current thread
        println("launch ${Thread. currentThread(). name}")
    }
    println("end ${Thread. currentThread(). name}")}
  • The suspend function runs in a coroutine or another suspend function. The coroutine has context information, so that after the suspend function cuts the thread, it can jump to the original thread. The context environment in Kotlin coroutines → CoroutineContext, which stores various elements in the form of key-value pairs:

CoroutineContext

Job (coroutine unique identifier) ​​+ CoroutineDispatcher (scheduler) + ContinuationInterceptor (interceptor) + CoroutineName (coroutine name, generally set during debugging)

  • suspend is used for reminder and mark, which means that it is a time-consuming function and needs to run in the background, and the caller does not need to judge whether the code is time-consuming.
  • Create a suspend function. In order to make it contain the real suspend logic, it is necessary to directly or indirectly call the suspend function that comes with Kotlin inside it. This suspend is meaningful.

3. Jetpack library support for coroutines

  • ViewModelScope

For ViewModelScope, use androidx.lifecycle:lifecycle-viewmodel-ktx:2.1.0-beta01 or higher.

Defines a ViewModelScope for each ViewModel in the application. If the ViewModel is cleared, any coroutines started within this scope are automatically canceled. Coroutines are useful when you have work that needs to be done only while the ViewModel is active. For example, if you want to calculate some data for the layout, you should scope your work to the ViewModel, so that after the ViewModel is cleared, the system will automatically cancel the work to avoid consuming resources.
The ViewModel’s CoroutineScope can be accessed through the ViewModel’s viewModelScope property, as shown in the following example:

class MyViewModel: ViewModel() {
        init {
            viewModelScope. launch {
                // The coroutine will be automatically canceled when the ViewModel is cleared
            }
        }
    }
  • LifecycleScope

For LifecycleScope, use androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-alpha01 or higher.

A LifecycleScope is defined for each Lifecycle object. Coroutines started within this scope are canceled when the Lifecycle is destroyed. You can access the Lifecycle’s CoroutineScope through the lifecycle.coroutineScope or lifecycleOwner.lifecycleScope properties.
The following example demonstrates how to use lifecycleOwner.lifecycleScope to create precomputed text asynchronously:

class MyFragment: Fragment() {
        override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
            super.onViewCreated(view, savedInstanceState)
            viewLifecycleOwner. lifecycleScope. launch {val params = TextViewCompat. getTextMetricsParams(textView)
                val precomputedText = withContext(Dispatchers. Default) {
                    PrecomputedTextCompat. create(longTextContent, params)
                }
                TextViewCompat.setPrecomputedText(textView, precomputedText)
            }
        }
    }
  • Using coroutines with LiveData

For liveData, use androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha01 or higher.

When using LiveData, you may need to compute values ​​asynchronously. For example, you might need to retrieve the user’s preferences and pass them to the interface. In these cases, you can use the liveData builder function to call the suspend function and pass the result as a LiveData object, which can be used in the Repository to generate livedata data and return it to the ViewModel.
In the following example, loadUser() is a suspend function declared elsewhere. Use the liveData builder function to call loadUser() asynchronously, then use emit() to emit the result:

val user: LiveData = liveData {
        val data = database.loadUser() // loadUser needs to be declared as a suspend function, otherwise liveData will create an execution on the main thread
        emit(data)
    }

When LiveData becomes active, the code block starts executing; when LiveData becomes inactive, the code block is automatically canceled after a configurable timeout. If the code block is canceled before completing, it will restart after LiveData becomes active again; if it completed successfully in the previous run, it will not restart. Note that code blocks will only restart if they are automatically canceled. If the code block is canceled for any other reason (for example, throwing a CancellationException), it will not be restarted.
It is also possible to emit multiple values ​​from a code block. Each emit() call suspends execution of the code block until the LiveData value is set on the main thread.

val user: LiveData = liveData {
        emit(Result.loading())
        try {
            emit(Result. success(fetchUser()))
        } catch(ioException: Exception) {
            emit(Result. error(ioException))
        }
    }

It is also possible to use liveData with Transformations, as shown in the following example:

class MyViewModel: ViewModel() {
        private val userId: LiveData = MutableLiveData()
        val user = userId. switchMap { id ->
            liveData(context = viewModelScope. coroutineContext + Dispatchers.IO) {
                emit(database. loadUserById(id))
            }
        }
    }

4. Retrofit’s support for coroutines

Retrofit version 2.6.0 has built-in support for Kotlin Coroutines, which further simplifies the process of using Retrofit and coroutines for network requests.

Examples are as follows:
In ApiService, use the suspend keyword to declare that the function needs to be executed in the background, and the function can directly return the object we need instead of returning the Deferred object.

interface ApiService {
    @GET("article/list/{page}/json")
    suspend fun getHotArticle(@Path("page")page: Int): ArticleResponse
} 
object ServiceCreator {
    val retrofit: Retrofit = Retrofit. Builder()
    .baseUrl("https://www.wanandroid.com/")
    .addConverterFactory(GsonConverterFactory.create())
    .build()
    
    inline fun  create() = retrofit.create(T::class.java)
} 
object ApiNetwork {
    private val apiService = ServiceCreator.create() // Create a dynamic proxy object
    
    suspend fun getArticle(page: Int) = apiService.getHotArticle(page) // call the suspend function, which also needs to be declared as a suspend function
} 

Get data in Repository

object HotRepository {
    fun getArticle(page: Int) = liveData(Dispatchers.IO){ // The last parameter of the livedata() function is a lambda expression in which the suspend function can be called

        val res = try {
            val articleResponse = ApiNetwork. getArticle(page)
            if (articleResponse. errorCode == 0) {
                Result.success(articleResponse) // The Result class can encapsulate the result and return more intuitive information
            } else {
                Result. failure(RuntimeException("errorCode is ${articleResponse. errorCode}"))
            }
        } catch (e: Exception) {
            Result. failure(e)
        }
        emit(res)
    }
}


Reference Article

Boring Kotlin Coroutine Trilogy (Part 2)—Application Practice

Kotlin’s coroutines take a hard look – can’t learn coroutines? Very likelyBecause the tutorials you have read are all wrong

Leave a Reply

Your email address will not be published. Required fields are marked *