A brief introduction to Android coroutines

1. The birth of the coroutine

As we all know, Android cannot perform any time-consuming operations on the main thread for the safety of the main thread. When developers perform time-consuming operations, they need to start sub-threads and run them in sub-threads, which will generate a lot of thread management codes in the process. The birth of the coroutine is to optimize this operation. The coroutine is a concurrent design pattern that can be used on the Android platform to simplify the code that needs to be executed asynchronously.

Features of coroutines

Coroutines are Google's recommended solution for asynchronous programming on Android. It has the following characteristics:

  • Lightweight: You can run multiple coroutines on a single thread, since coroutines support suspension, which does not block 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 execute multiple operations within a single scope.
  • Built-in cancellation support: Cancellation is automatically propagated through 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.

2. The use of coroutines

Execute in background thread

If a network request is made on the main thread, the main thread waits or blocks until a response is received. Because the thread is blocked, the OS cannot call onDraw(), which causes the app to freeze and potentially result in an Application Not Responding (ANR) dialog. In order to solve this problem, we usually perform time-consuming operations such as network requests on background threads during development.

Let's take a simple login request as an example to see how to use the coroutine operation.

First, let's take a look at how the Repository class makes requests in Google's recommended architecture:

//网络请求响应结果实体封装类
sealed class Result<out R> {
    
    
    //带范型的返回数据类
    data class Success<out T>(val data: T) : Result<T>()
    //网络请求错误结果数据类
    data class Error(val exception: Exception) : Result<Nothing>()
}

class LoginRepository(private val responseParser: LoginResponseParser) {
    
    
    //具体的请求地址
    private const val loginUrl = "https://example.com/login"

    // Function that makes the network request, blocking the current thread
    //具体的网络请求函数,会阻塞当前线程,直到结果返回。
    fun makeLoginRequest(
        jsonBody: String
    ): Result<LoginResponse> {
    
    
        val url = URL(loginUrl)
        (url.openConnection() as? HttpURLConnection)?.run {
    
    
            requestMethod = "POST"
            setRequestProperty("Content-Type", "application/json; utf-8")
            setRequestProperty("Accept", "application/json")
            doOutput = true
            outputStream.write(jsonBody.toByteArray())
            return Result.Success(responseParser.parse(inputStream))
        }
        return Result.Error(Exception("Cannot open HttpURLConnection"))
    }
}

In the above code, in order to process the response data of the network request, we created our own Result class. Among them, makeLoginRequest is a synchronous execution function, which will block the calling thread.

The ViewModel triggers a network request when the user interacts with the interface (for example, clicks the login button):

class LoginViewModel(
    private val loginRepository: LoginRepository
): ViewModel() {
    
    

    fun login(username: String, token: String) {
    
    
        val jsonBody = "{ username: \"$username\", token: \"$token\"}"
        loginRepository.makeLoginRequest(jsonBody)
    }
}

If we use the above code directly, LoginViewModel will block the interface thread when the network request is sent. If we need to move the execution operation out of the main thread, our previous method is to start a new thread to execute:

class LoginViewModel(
    private val loginRepository: LoginRepository
): ViewModel() {
    
    

    fun login(username: String, token: String) {
    
    
        //创建线程并启动,执行登录请求。
        Thread{
    
    
            Runnable {
    
    
                loginRepository.makeLoginRequest(jsonBody)
            }
        }.start()
    }
}



The above approach will allow us to create a thread every time we execute a network login request, and after the request is completed, we need to use the callback and handler to re-pass the request result to the main thread for processing. The emergence of coroutines allows us to have an easier method, which is to create a new coroutine, and then execute network requests on the I/O thread:

class LoginViewModel(
    private val loginRepository: LoginRepository
): ViewModel() {
    
    

    fun login(username: String, token: String) {
    
    
        // Create a new coroutine to move the execution off the UI thread
        //创建一个新的协程,使其移出UI线程执行, Dispatchers.IO: I/O 操作预留的线程
        viewModelScope.launch(Dispatchers.IO) {
    
    
            val jsonBody = "{ username: \"$username\", token: \"$token\"}"
            //执行网络请求操作,该请求会在I/O 操作预留的线程上执行
            loginRepository.makeLoginRequest(jsonBody)
        }
    }
}

Let's take a closer look at the coroutine code in the login function:

  • viewModelScope is a predefined CoroutineScope included in the ViewModel KTX extension. Note that all coroutines must run within a scope. A CoroutineScope manages one or more related coroutines.
  • launch is a function that creates a coroutine and dispatches the execution of its function body to the appropriate dispatcher, (Dispatchers.IO) is an optional parameter.
  • Dispatchers.IO indicates that this coroutine should execute on a thread reserved for I/O operations.

The login function executes as follows:

  • The application calls the login function (on click of the login button) from the View layer on the main thread.
  • launch creates a new coroutine, and network requests are issued independently on threads reserved for I/O operations.
  • While the coroutine is running, the login function will continue to execute and may return before the network request is completed (the request will not block the main thread for subsequent operations).

Since this coroutine is launched via viewModelScope, it executes within the scope of the ViewModel. If the ViewModel is destroyed because the user leaves the screen, the viewModelScope is automatically canceled and all running coroutines are also canceled.

The two problems with the previous example are that anything that calls makeLoginRequest needs to remember to explicitly move execution off the main thread by passing in the (Dispatchers.IO) parameter after the launch function. The second is that the result of the login request is not processed. Let's take a look at how to modify the Repository to solve this problem.

Use coroutines to ensure main thread safety

We consider a function to be main thread safe if it does not block UI updates on the main thread. The makeLoginRequest function is not main thread safe because calling makeLoginRequest from the main thread does block the UI. In the above code example, we can start the coroutine in the ViewModel and assign the corresponding scheduler, but this approach requires us to go to the tail scheduler every time we call makeLoginRequest. To solve this problem we can use the withContext() function in the coroutine library to move the execution of the coroutine to other threads:

class LoginRepository(...) {
    
    
    private const val loginUrl = "https://example.com/login"
    //suspend 关键字表示改方法会阻塞线程,Kotlin 利用此关键字强制从协程内调用函数。
    suspend fun makeLoginRequest(
        jsonBody: String
    ): Result<LoginResponse> {
    
    
        // Move the execution of the coroutine to the I/O dispatcher
        //表示协程的后续执行会被放在IO线程中
        return withContext(Dispatchers.IO) {
    
    
            val url = URL(loginUrl)
            (url.openConnection() as? HttpURLConnection)?.run {
    
    
                requestMethod = "POST"
                setRequestProperty("Content-Type", "application/json; utf-8")
                setRequestProperty("Accept", "application/json")
                doOutput = true
                outputStream.write(jsonBody.toByteArray())
                return Result.Success(responseParser.parse(inputStream))
            }
            return Result.Error(Exception("Cannot open HttpURLConnection"))
            
        }
    }
}

withContext(Dispatchers.IO) moves the execution of the coroutine to an I/O thread, so that our calling function is main thread safe and supports updating the interface as needed.

makeLoginRequest is also marked with the suspend keyword. Kotlin makes use of this keyword to force calling a function from within a coroutine.

Next we are in the ViewModel, since makeLoginRequest moves execution out of the main thread, the coroutine in the login function can now be executed in the main thread:

class LoginViewModel(
    private val loginRepository: LoginRepository
): ViewModel() {
    
    

    fun login(username: String, token: String) {
    
    

        // Create a new coroutine on the UI thread
        //直接在UI主线程中启动一个协程
        viewModelScope.launch {
    
    
            val jsonBody = "{ username: \"$username\", token: \"$token\"}"

            // Make the network call and suspend execution until it finishes
            //执行网络操作,并且等待被suspend标记的函数执行完成。
            //该等待并不会阻塞主线程,因为被suspend标记的函数会被分配到IO线程执行
            val result = loginRepository.makeLoginRequest(jsonBody)

            // Display result of the network request to the user
            //当收到请求结果后,向用户现实请求结果,并更行对应界面
            when (result) {
    
    
                is Result.Success<LoginResponse> -> // 登录成功,跳转主页。。。
                else -> // 登录失败,提示用户错误信息
            }
        }
    }
}

Note that a coroutine is still required here, because makeLoginRequest is a suspend function, and all suspend functions must be executed in a coroutine.

This code differs from the previous login example in the following ways:

  • launch does not accept a (Dispatchers.IO) argument. By default all coroutines launched from viewModelScope will run in the main thread.
  • The system now handles the results of network requests to display a success or failure interface.

The login function now executes as follows:

  • The application calls the login() function from the View layer on the main thread.
  • launch creates a new coroutine on the main thread, and the coroutine starts executing.
  • Within a coroutine, calling loginRepository.makeLoginRequest() now suspends further execution of the coroutine until the withContext block in makeLoginRequest() finishes running.
  • After the withContext block finishes running, the coroutine in login() resumes execution on the main thread and returns the result of the network request.
  • After receiving the result, process the corresponding result and update the UI

handle exception

When making network requests or time-consuming operations, exceptions are often thrown. In order to handle possible exceptions in Repository, we can use try-catch block to catch and handle corresponding exceptions:

class LoginViewModel(
    private val loginRepository: LoginRepository
): ViewModel() {
    
    

    fun makeLoginRequest(username: String, token: String) {
    
    
        viewModelScope.launch {
    
    
            val jsonBody = "{ username: \"$username\", token: \"$token\"}"
            //使用try-catch捕捉异常
            val result = try {
    
    
                loginRepository.makeLoginRequest(jsonBody)
            } catch(e: Exception) {
    
    
                Result.Error(Exception("Network request failed"))
            }
            when (result) {
    
    
                is Result.Success<LoginResponse> -> // 登录成功,跳转主页。。。
                else -> // 登录失败,提示用户错误信息
            }
        }
    }
}

In the code example above, any unexpected exceptions thrown by the makeLoginRequest() call are handled as UI errors.

Summarize

In this way, we use the coroutine to fully realize the operation of a login request. In the process, we only need to use the withContext function in loginRepository to declare the scheduler, which can avoid the problem of time-consuming operations blocking the main thread, and does not require developers Manage the corresponding thread by yourself. And because of the existence of viewModelScope, we don't need to deal with the request cancellation problem after the page is destroyed. While optimizing performance, it also greatly reduces the amount of our code. It is an excellent asynchronous code processing mode.

at last

If you want to become an architect or want to break through the 20-30K salary range, then don't be limited to coding and business, but you must be able to select models, expand, and improve programming thinking. In addition, a good career plan is also very important, and the habit of learning is very important, but the most important thing is to be able to persevere. Any plan that cannot be implemented consistently is empty talk.

If you have no direction, here I would like to share with you a set of "Advanced Notes on the Eight Major Modules of Android" written by the senior architect of Ali, to help you organize the messy, scattered and fragmented knowledge systematically, so that you can systematically and efficiently Master the various knowledge points of Android development.
img
Compared with the fragmented content we usually read, the knowledge points of this note are more systematic, easier to understand and remember, and are arranged strictly according to the knowledge system.

Welcome everyone to support with one click and three links. If you need the information in the article, you can directly scan the CSDN official certification WeChat card at the end of the article to get it for free↓↓↓

PS: There is also a ChatGPT robot in the group, which can answer your work or technical questions
picture

Guess you like

Origin blog.csdn.net/YoungOne2333/article/details/131668050