Getting Started with Android Kotlin Coroutine

Article directory

  • Preface
  • 1. Coroutine Basics
    • The first coroutine
      • Structured Concurrency
    • Suspend function suspend
    • Scope builder
    • Scope builder and concurrency
    • job
    • Lightweight
  • Second, Kotlin coroutine on Android
    • Features
    • Example of Kotlin coroutine + Retrofit on Android for network request
      • li>
        • Some code preparation
        • Kotlin is simple to use Retrofit
        • Kotlin android is simple to use coroutines
        • Kotlin android is simple to use Coroutine + retrofit
    • Reference

    Foreword

    ps: The article will be very long

    Coroutines are really easy to use, so let’s learn! This article will step by step by interpreting kotlin official documents and android development documentsTo know Kotlin coroutines, those who do not have a Kotlin foundation are advised to learn Kotlin first.


    1. Coroutine basics

    This section introduces the basic concepts of coroutines. The following are official kotlin examples, very classic.

    Dependencies and Permissions

    //Dependency
     implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9")
          //retrofit
        api 'com.squareup.retrofit2:retrofit:2.6.0'
        api 'com.squareup.retrofit2:converter-gson:2.6.0'
        api 'com.squareup.retrofit2:adapter-rxjava2:2.6.0'
        api 'com.trello.rxlifecycle2:rxlifecycle-components:2.6.1'
        api 'com.facebook.stetho:stetho:1.5.0'
        api 'com.facebook.stetho:stetho-okhttp3:1.5.0'
    
     //permission
     
        
    

    The first coroutine

    A coroutine is an instance of suspendable computation. It’s conceptually similar to a thread in that it requires a block of code to run that does work concurrently with the rest of the code. However, coroutines are not bound to any particular thread. It can suspend execution in one thread and resume in another.
    Coroutines can be thought of as lightweight threads ,But there are a number of important differences that make their real-life use very different from threads.
    This code example below will be our first coroutine

    import kotlinx.coroutines.*
    
    fun main() = runBlocking { // this: CoroutineScope
    // launch a new coroutine and continue start a new coroutine and connect
        launch {
        // non-blocking delay for 1 second (default time unit is ms) delay for one second
            delay(1000L)
            // print after delay print after delay
            println("World!")
        }
        // "Hello" in the main thread continues to execute, while "World!" is printed after a delay
        println("Hello")
    }
    

    Unsurprisingly, we get this result:

    Hello
    World!

    Judging from this result, this is indeed very similar to a thread, isn’t it? Let’s analyze the code above

    launch
    is a coroutine builder. It starts a new coroutine at the same time as the rest of the code, which continues to work independently. That’s why Hello is printed first.
    delay
    is a special pause function. It suspends the coroutine at a specific time. Suspends a coroutine without blocking the underlying thread, allowing other coroutines to run and use the underlying thread for their code. It can be mutually verified with sleep() in the thread for memory (not to say that it can be equivalent)
    runBlocking
    is also a coroutine builder that connects the regular fun main()’s non-coroutine with the coroutine code inside runBlocking{…}. In the IDE, there is a hint after the curly braces at the beginning of runBlocking: CoroutineScope.

    Note: If you delete or forget runBlocking in this code, you will get the following error in the start call, because start is only declared in CoroutineScope:

    Unresolved reference: launch
    

    The name of runBlocking means that the thread running it (in this case the main thread) is blocked during the call, until all coroutines in runBlocking{…} are blocked to complete their code logic. You’ll often see runBlocking used like this in top-level applications, but rarely in real code because threads are expensive resources and blocking them is inefficient and often unnecessary.

    Structured Concurrency

    Coroutines follow the principle of structured concurrency, which means that new coroutines can only be started within a specific coroutine scope that limits the lifetime of the coroutine. The above example shows that runBlocking establishes the corresponding scope, which is why the previous example waits until World! Print after a one-second delay, then exit.

    In a real application, many coroutines will be launched. Structured concurrency ensures that they are not lost, nor leaked. An outer scope cannot complete until all of its child coroutines have completed. Structured concurrency also ensures that any bugs in your code are properly reported and never lost.

    Suspend function suspend

    Appeal is just a basic coroutine, we can extract the code block in launch{…} into a separate function. When you perform an “extract function” refactoring on this code (that’s alt+enter), you will get a new function (suspend function) with the suspend modifier. This is your first pause function. Suspend functions can be used in coroutines like regular functions, or they can be used in turn to suspend the execution of coroutines using other suspend functions (such as defer in this example).

    import kotlinx.coroutines.*
    
    fun main() = runBlocking { // this: CoroutineScope
        launch { doWorld() }
        println("Hello")
    }
    
    // this is your first suspending function
    // first suspend function
    suspend fun doWorld() {
        delay(1000L)
        println("World!")
    }
    

    The same results are as follows

    Hello
    World!

    Scope builder Scope builder

    In addition to the coroutine scopes provided by the different builders, it is also possible to declare your own scopes using the coroutine scope builder. It creates a coroutine scope that won’t complete until all launched children have completed.

    The builders for runBlocking and coroutineScope may look similar, since they both wait for the completion of their own body and all children. The main difference is that the runBlocking method blocks the current thread to wait, while coroutineScope just suspends and releases the underlying thread for other use. Because of this difference, runBlocking is a regular function, while coroutineScope is a suspending function.

    We can use coroutineScope from any suspending function. For example, the concurrent printing of Hello and World can be moved to suspend fun doWorIn the ld() function:

    import kotlinx.coroutines.*
    
    fun main() = runBlocking {
        doWorld()
    }
    
    suspend fun doWorld() = coroutineScope { // this: CoroutineScope
        launch {
            delay(1000L)
            println("World!")
        }
        println("Hello")
    }
    

    Same result:

    Hello
    World!

    Scope builder and concurrency

    Scope constructors can be used in any suspend function to perform multiple concurrent operations. Let’s start two concurrent coroutines in the doWorld suspending function:

    import kotlinx.coroutines.*
    
    // Sequentially executes doWorld followed by "Done"
    //sequential execution
    fun main() = runBlocking {
        doWorld()
        println("Done")
    }
    
    // Concurrently executes both sections
    // Execute both parts at the same time
    suspend fun doWorld() = coroutineScope { // this: CoroutineScope
        launch {
            delay(2000L)
            println("World 2")}
        launch {
            delay(1000L)
            println("World 1")
        }
        println("Hello")
    }
    

    The two code blocks in launch {…} are executed at the same time, printing World 1 first after one second from the start, and printing World 2 after two seconds from the start. A coroutineScope in doWorld can’t complete until both are complete, so doWorld returns and allows to print the Done string only when completed:

    Hello
    World 1
    World 2
    Done

    job

    The launch coroutine constructor returns a job object, which is a handle to the launch coroutine, which can be used to explicitly wait for it to complete. For example, you could wait for the child coroutine to complete, then print the “Done” string:

    import kotlinx.coroutines.*
    
    fun main() = runBlocking {
        val job = launch {
        // launch a new coroutine and keep a reference to its Job
        // start a new coroutine and keep a reference to its work
            delay(1000L)
            println("World!")
        }
        println("Hello")
        // wait until child coroutine completes
        //Wait for the child coroutine to complete
        job. join()
        println("Done")
    }
    

    The results are as follows:

    Hello
    World!
    Done

    Lightweight

    We have the following sample code:

    import kotlinx.coroutines.*
    
    //sampleStart
    fun main() = runBlocking {
        repeat(100_000) { // launch a lot of coroutines
            launch {
                delay(5000L)
                print(".")
            }
        }
    }
    //sampleEnd
    

    It starts 100,000 coroutines, and after 5 seconds, each coroutine prints a dot.
    However, if we try it with threads (delete runBlocking, replace launch with thread, replace delay with thread.sleep). what happens? (The code will most likely generate some kind of out of memory error)

    2. Kotlin coroutines on Android

    The Android development documentation introduces coroutines as follows:
    Coroutines are a concurrency design pattern that you can use on the Android platform to simplify asynchronously executed code. Coroutines were added to Kotlin in version 1.3 and are based on established concepts from other languages.

    On Android, coroutines help manage long-running tasks that, if not managed properly, can block the main thread and render your app unresponsive. More than 50% of professional developers who use coroutines report increased productivity using coroutines. This topic describes how to use Kotlin coroutines to solve the following problems, allowing you to write cleaner and more concise application code.

    Features

    Coroutines are our recommended solution for asynchronous programming on Android. Noteworthy features include:

    • Lightweight: You can run multiple coroutines on a single thread, since coroutines support suspension without blocking the thread that is running the coroutine. Suspending saves memory compared to blocking and supports multiple parallel operations.
    • Fewer memory leaks: Use structured concurrency to perform multiple operations in one scope.
    • Built-in cancellation support: Cancellation is automatically propagated throughout the running coroutine hierarchy.
    • Jetpack integration: Many Jetpack libraries include extensions that provide full coroutine support. Some libraries also provide their own coroutine scopes that you can use for structured concurrency.

    Example of Kotlin coroutine + Retrofit for network request on Android

    For the convenience of comparison, we first use a separate Retrofit. If you are not familiar with Retrofit, you can refer to this article of the author

    For the java version, the Retrofit version that supports coroutines must be at least greater than 2.6. This article uses the 2.6 version of Retrofit
    Android retrofit from ignorance to entry (the latest and most complete)

    Some code preparation

    API

    import retrofit2.Call
    import retrofit2.Response
    import retrofit2.http.GET
    
    interface ISystemRemoteService {
        @get:GET("/system/alive")
        val systemStatus: Call>
    }
    

    RetrofitService

    import android.util.Log
    import retrofit2.Retrofit
    import okhttp3.OkHttpClient
    import com.zyf.kotlindemo.HttpsUtils
    import kotlin.Throws
    import com.zyf.kotlindemo.RetrofitService
    import java.util.concurrent.TimeUnit
    
    class RetrofitService private constructor() {
        private val retrofit: Retrofit
        fun  create(clazz: Class?): T {
            return retrofit. create(clazz)
        }
    
        companion object {
            private val client = OkHttpClient. Builder()
                .connectTimeout(30, TimeUnit. SECONDS)
                .readTimeout(30, TimeUnit. SECONDS)
                .sslSocketFactory(HttpsUtils.getSslSocketFactory(null, null, null))
                .hostnameVerifier { hostname, session -> true }
                .writeTimeout(30, TimeUnit.SECONDS).addInterceptor { chain ->
                    val builder = chain.request()
                        .newBuilder()
                    val request = builder. build()
                    chain.proceed(request)
                }.build()
    
            private var i: RetrofitService? = null
            fun i(): RetrofitService? {
                if (i == null) {
                    i = RetrofitService()
                }
                return i
            }
    
            fun rebuild() {
                i = RetrofitService()
            }
        }
    
        init {
            val serverUrl = "https://blog.csdn.net/shop_and_sleep"
            retrofit = Retrofit. Builder()
                .baseUrl(serverUrl) //Set timeout, intercept
                .client(client)
                .build()
            Log.i("ServerUrl:------", serverUrl)
        }
    }
    

    Kotlin simply uses Retrofit

    Only use Retrofit without coroutines

     val api = RetrofitService.i()?.create(ISystemRemoteService::class.java)
            val call= api?.systemStatus
            //Here because I am in the main thread, so it can only be asynchronous
            val result = call?.enqueue(object : Callback<Response>{
                override fun onResponse(call: Call<Response>, response: Response<Response>) {
                    // successful callback
                }
    
                override fun onFailure(call: Call<Response>, t: Throwable) {
                    // failure callback
                }
    
            })
    

    Kotlin android simple use of coroutines

    In the first coroutine, we know that each coroutine must have a scope constructor, so the first coroutine in Android can become

     CoroutineScope(Dispatchers.Main).launch {
                // non-blocking delay for 1 second (default time unit is ms) delay for one second
                delay(1000L)
                // print after delay print after delay
                println("World!")
            }
            println("Hello")
    

    Dispatchers.Main
    means to start this coroutine in the main thread, Dispatchers is a scheduler. In addition to Main, we can also see others in its source code, which is explained in the following table:

    group function
    Default The default scheduler (this is the default if not written)
    Main The scheduler of the main thread (this is the default if not written )
    Unconfined A coroutine scheduler that is not confined to any particular thread
    IO As the name suggests, a scheduler designed to offload blocking IO tasks to a shared thread pool)

    Kotlin android simply uses coroutines + retrofit

    After understanding the scheduler Dispatchers, the coroutine constructor, and the return job of the coroutine constructor, a simple network request appears naturally:

     CoroutineScope(Dispatchers.Main).launch {
    
                /*Because the first coroutine is still the main thread,
                Android stipulates that network requests must be in sub-threads,
                So here we get the request result through withContext,
                Switch to the IO thread through the scheduler Dispatcher,
                This operation will suspend the current coroutine, but will not block the current thread*/
                val result = withContext(Dispatchers.IO) {
                    /*This is already in the child thread,
                So use execute() to synchronously request
              withContext will return the return value of the last line of code in the closureSo the above result is the Response type */
                    RetrofitService.i()?.create(ISystemRemoteService::class.java)?.
                    systemStatus?.
                    execute()?.
                    body()
                }
                Log.d("onCreate", "result: "+result. toString())
                if (result?.isSuccessful == true){
                    Toast.makeText(mContext, "The request was successful", Toast.LENGTH_SHORT).show()
                }
            }
    

    Where withContext invokes the specified suspending block with the given coroutine context, suspends until complete, and then returns, except for launch
    , withContext, and async , due to space reasons More details, if you are interested, you can understand by yourself;

    It is worth mentioning that if the call to ***withContext*** is canceled when its scheduler starts executing code, it will discard the result of ‘withContext’ and throw a [CancellationException].
    In order to solve this problem, we have to mention the built-in cancellation support of coroutines, so our complete code becomes as follows:

    import android.support.v7.app.AppCompatActivity
    import android.os.Bundle
    import android.util.Log
    import android.view.Gravity
    import android.widget.Button
    import android.widget.Toast
    import kotlinx.android.synthetic.main.activity_main.*
    import kotlinx.coroutines.*
    import retrofit2. Call
    import retrofit2. Callback
    import retrofit2.Response
    import kotlin.coroutines.CoroutineContext
    
    
    class MainActivity : AppCompatActivity() , CoroutineScope {
        private val mContext by lazy { this } //(The delegate is used here, which means that the code will be executed only when mContext is used)
        //job is used to control the coroutine, the coroutine started by launch{} later, the returned job is the job object
        private lateinit var job: Job
        // Inheriting CoroutineScope must initialize the coroutineContext variable
        // This is a standard way of writing, + is actually the front of the plus method, which means job, which is used to control the coroutine, followed by Dispatchers, which specifies the thread to start
        override val coroutineContext: CoroutineContext
            get() = job + Dispatchers. Main
    
    
       override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            job = Job()setContentView(R. layout. activity_main)
            CoroutineScope(Dispatchers.Main).launch {
    
          
                 RetrofitService.i()?.create(ISystemRemoteService::class.java)?.
                    systemStatus?.
                    execute()?.
                    body()
                }
                Log.d("onCreate", "result: "+result. toString())
                if (result?.isSuccessful == true){
                    Toast.makeText(mContext, "The request was successful", Toast.LENGTH_SHORT).show()
                }
            }
        }
        
        override fun onDestroy() {
        //When the acitivity ends, we don't need to request the network anymore, end the current coroutine
            job. cancel()
            super. onDestroy()
    
        }
    }
    

    This is the end of this simple example, but the actual project generally uses network requests in the viewmodel instead of in the activity, which has to mention another advantage of the coroutine Jetpack integration, if it is in the viewmodel, You can use viewModelScope to bind the life cycle of viewModel, otherwise you have to manually call the job.cancel() method as in the example


    Reference

    kotlin official document
    Android official document
    Coroutine cooperates with retrofit to call network request
    Kotlin coroutine (Coroutines) and Retrofit for network requests (1)

Leave a Reply

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