Kotlin coroutines and their use in Android are summarized (four coroutines are used in combination with Retrofit, Room, WorkManager)

Insert picture description here

0 Thinking of designing a new Android app architecture

I have seen articles like this before. If you were to redesign an app, what architecture would you use, which three-party libraries would you use, how to encapsulate a module for others to use, and since Jetpack on Android, these problems seem to have Clearer answer.

Some grounds for these considerations:

  • Simple and robust

The use of all frameworks or libraries should reduce the generation of template code, and even achieve 0 template code. There is no doubt that Kotlin language will become the first choice for native development. Through the built-in empty security, standard function library, extended attributes, and convenient Custom DSL, attribute proxy and class proxy with built-in support, method inlining, data class, etc.

In addition to the language level, by using some of the functional classes that come with the Android API (such as Lifecycle with built-in support), and the use of Jetpack architecture components (such as LiveData and ViewModel), you can pass control logic that is not related to the business logic of the program to the system. Internal processing, reduce your own code to deal with memory leaks and null pointers.

Many other components in Jetpack, such as the view binding library ViewBinding, database Room, background task processing WorkManager, etc., allow us to reduce the template code, while providing built-in security mechanisms to make our code more robust

  • Easy to read and maintainable

At the same time as the code is concise, it also needs to be easy to read, which definitely requires team members to have an understanding of related new technologies.
At the same time, complete test cases are essential, which can greatly reduce the time for debugging and reduce the security risks caused by code changes. At the same time, the use of Android Studio's own Lint code detection and custom Lint detection rules can also further improve the code Maintainability.

  • Main thread safety mian-safe

This point is mainly to explain the use of coroutines. In addition to everyone knowing that it can reduce the nesting of Callback, and use seemingly synchronous code to write asynchronous logic, the safety of the main thread is also a very important aspect. By transforming the time-consuming operation into a suspend function, and the function writer uses the Dispatcher to specify the thread used by the function, then the caller does not need to consider whether calling this function will affect the safety of the main thread.

The following is the focus of this article, how to use coroutines combined with some function libraries to simplify the code, improve the conciseness of the code (the code written by yourself is reduced) and stability (the stability is guaranteed by the built-in logic of each function library, instead of using Code control).

The complete project code link of the content involved in the article:
git clone https://github.com/googlecodelabs/kotlin-coroutines.git
is located in the finished_code of the coroutines-codelab path

1 Coroutines in Room & Retrofit

If you do n’t understand Room, you can learn it first. Here is a direct example.

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTitle(title: Title)

By adding a suspendkeyword in front of the function , Room will provide main thread safety, namely main-safe, and it will automatically execute it on a background thread. Of course, this function can only be called in a coroutine at this time.

No, the use of coroutines in Room is so simple.

About Retrofit, everyone should be familiar with it, and give direct examples.

// add suspend modifier to the existing fetchNextTitle
// change return type from Call<String> to String

interface MainNetwork {
   @GET("next_title.json")
   suspend fun fetchNextTitle(): String
}

In addition to adding a suspendkeyword to the function in the interface , for the return value form of the function, the original Call-wrapped result is changed to a direct result type, just as String is returned above, of course, it can also be your custom Json Data class.

The code before transformation may look like this:

suspend fun refreshTitle() {
   // interact with *blocking* network and IO calls from a coroutine
   withContext(Dispatchers.IO) {
       val result = try {
           // Make network request using a blocking call
           network.fetchNextTitle().execute()
       } catch (cause: Throwable) {
           // If the network throws an exception, inform the caller
           throw TitleRefreshError("Unable to refresh title", cause)
       }
      
       if (result.isSuccessful) {
           // Save it to database
           titleDao.insertTitle(Title(result.body()!!))
       } else {
           // If it's not successful, inform the callback of the error
           throw TitleRefreshError("Unable to refresh title", null)
       }
   }
}

The modified code looks like this:

//TitleRepository.kt

suspend fun refreshTitle() {
   try {
       // Make network request using a blocking call
       val result = network.fetchNextTitle()
       titleDao.insertTitle(Title(result))
   } catch (cause: Throwable) {
       // If anything throws an exception, inform the caller
       throw TitleRefreshError("Unable to refresh title", cause)
   }
}

The transformation is still quite simple. Here, Room will use the set query and transaction executor to execute the coroutine body. Retrofit will create a new Call object in the background thread and call the queue on it to send the request asynchronously. When the result returns Resume the execution of the coroutine.

At the same time, Room and Retrofit provide main thread safety main-safe, so we do not need to use withContext (Dispatcher.IO) when calling ,

2 Use coroutines in higher-order functions

In the above code, although it has been simplified a lot, but if there are multiple request logics, then you need to write a set of try-catch and state initialization and exception assignment logic. These are also template codes. The code is as follows:

// MainViewModel.kt

fun refreshTitle() {
   viewModelScope.launch {
       try {
           _spinner.value = true
           // 假设_spinner.value的赋值和其他异常逻辑都是通用的
           // 那么下面这行代码才是唯一需要关注的,
           repository.refreshTitle() 
       } catch (error: TitleRefreshError) {
           _snackBar.value = error.message
       } finally {
           _spinner.value = false
       }
   }
}

At this time, we can use the functional programming style to write a high-order function, encapsulating the general business processing logic, as follows:

private fun launchDataLoad(block: suspend () -> Unit): Job {
   return viewModelScope.launch {
       try {
           _spinner.value = true
           block()
       } catch (error: TitleRefreshError) {
           _snackBar.value = error.message
       } finally {
           _spinner.value = false
       }
   }
}

So when we finally call it in the ViewModel, there is only one key line of code left:

// MainViewModel.kt

fun refreshTitle() {
   launchDataLoad {
       repository.refreshTitle()
   }
}

In fact, just like we use functional programming to write other higher-order functions, here is just a suspendkeyword modification to the parameters . That is, the suspendlambda can call the suspend function, which is how the coroutine builder launch and runBlocking are implemented.

// suspend lambda

block: suspend () -> Unit

3 Use coroutines and WorkManager together

WorkManager is part of Android Jetpack . Using the WorkManager API, you can easily schedule deferred asynchronous tasks that should run even when the application exits or the device restarts.

The main function:

  • Highest backward compatibility to API 14
    • Use JobScheduler on devices running API 23 and above
    • Use BroadcastReceiver and AlarmManager on devices running API 14-22
  • Add work constraints such as network availability or state of charge
  • Schedule one-time or periodic asynchronous tasks
  • Monitor and manage planned tasks
  • Link tasks
  • Ensure task execution, even when the application or device restarts
  • Follow power saving features such as low power consumption mode

WorkManager is designed for tasks that can run deferred (that is, do not need to run immediately) and must be able to run reliably when the application exits or the device restarts. E.g:

  • Send logs or analyze data to back-end services
  • Periodically synchronize application data with the server

WorkManager is not suitable for running background work that can be safely terminated at the end of the application process, nor for tasks that need to be executed immediately.

Take the use directly as an CoroutineWorkerexample here , customize a class to RefreshMainDataWorkinherit from CoroutineWorker, and copy the doworkmethod as follows:

override suspend fun doWork(): Result {
   val database = getDatabase(applicationContext)
   val repository = TitleRepository(network, database.titleDao)

   return try {
       repository.refreshTitle()
       Result.success()
   } catch (error: TitleRefreshError) {
       Result.failure()
   }
}

Note * CoroutineWorker.doWork () * is a suspend function, which is different from the configuration thread pool used by the ordinary Worker class. It uses the dispatcher in the coroutineContext to control the thread scheduling (the default is Dispatchers.Default).

4 About the handling of coroutine cancellation and timeout

None of the code we wrote above has logic about cancellation of coroutines, but this is also an essential part of code robustness. Although in most cases, we can use the viewModelScope and lifecycleScope provided by Android to cancel the internal coroutine at the end of the page life cycle, there are still some situations that require us to handle the cancellation and timeout logic ourselves.

This part can refer to the introduction of kotlin official website: Cancellation and Timeouts

Using the cancel and join methods or the cancelAndJoin method, we can cancel a job as follows:

import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch {
        repeat(1000) { i ->
            println("job: I'm sleeping $i ...")
            delay(500L)
        }
    }
    delay(1300L) // 在外部协程体中延迟1300毫秒,上面的job会先执行
    println("main: I'm tired of waiting!")
    job.cancel() // 取消当前的job
    job.join() // 等待直到这个job完成后结束 
    println("main: Now I can quit.")    
}

The printed log is:

job: I’m sleeping 0 …
job: I’m sleeping 1 …
job: I’m sleeping 2 …
main: I’m tired of waiting!
main: Now I can quit.

Let's take a look at the source code of Job's cancel and join methods:

abstract fun cancel(
    cause: CancellationException? = null
): Unit (source

abstract suspend fun join(): Unit (source)

When canceling, an optional cause parameter can be provided to specify an error message or provide other detailed information about the reason for cancellation for debugging.
As for this join suspend function and cancelAndJoin function will wait for all coroutine body execution to complete, including the logic in the try-finally block.

After calling the cancel method of a coroutine job, it only marks its status as canceled, and its internal logic will continue to execute. This should not be the result we expect, such as the following code:

import kotlinx.coroutines.*

fun main() = runBlocking {
    val startTime = System.currentTimeMillis()
    val job = launch(Dispatchers.Default) {
        var nextPrintTime = startTime
        var i = 0
        while (i < 5) { // computation loop, just wastes CPU
            // print a message twice a second
            if (System.currentTimeMillis() >= nextPrintTime) {
                println("job: I'm sleeping ${i++} ...")
                nextPrintTime += 500L
            }
        }
    }
    delay(1300L) // delay a bit
    println("main: I'm tired of waiting!")
    job.cancelAndJoin() // cancels the job and waits for its completion
    println("main: Now I can quit.")    
}

The printed log is:

job: I’m sleeping 0 …
job: I’m sleeping 1 …
job: I’m sleeping 2 …
main: I’m tired of waiting!
job: I’m sleeping 3 …
job: I’m sleeping 4 …
main: Now I can quit.

There are two ways to solve the above problem. The first one is to periodically call the suspend function to check cancellation. For this, a yield function is a good choice. The other is to explicitly check the cancellation status. Let's try the latter method:

import kotlinx.coroutines.*

fun main() = runBlocking {
    val startTime = System.currentTimeMillis()
    val job = launch(Dispatchers.Default) {
        var nextPrintTime = startTime
        var i = 0
        while (isActive) { // 通过使用CoroutineScope的扩展属性isActive来使得该计算循环可取消
            // print a message twice a second
            if (System.currentTimeMillis() >= nextPrintTime) {
                println("job: I'm sleeping ${i++} ...")
                nextPrintTime += 500L
            }
        }
    }
    delay(1300L) // delay a bit
    println("main: I'm tired of waiting!")
    job.cancelAndJoin() // cancels the job and waits for its completion
    println("main: Now I can quit.")    
}

The printed log is:

job: I’m sleeping 0 …
job: I’m sleeping 1 …
job: I’m sleeping 2 …
main: I’m tired of waiting!
main: Now I can quit.

Use the suspend function again or throw a Cancel exception in the try-finally block, because the coroutine body has been canceled at this time. Although the common resource release and close operations are non-blocking and will not introduce suspend function calls, in extreme cases, by using withContext (NonCancellable), the cancelled coroutine can be suspended again, and You can continue to call the suspend function:

import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch {
        try {
            repeat(1000) { i ->
                println("job: I'm sleeping $i ...")
                delay(500L)
            }
        } finally {
            withContext(NonCancellable) {
                println("job: I'm running finally")
                delay(1000L)
                println("job: And I've just delayed for 1 sec because I'm non-cancellable")
            }
        }
    }
    delay(1300L) // delay a bit
    println("main: I'm tired of waiting!")
    job.cancelAndJoin() // cancels the job and waits for its completion
    println("main: Now I can quit.")    
}

Timeout timeout control

For example, we have a network request that specifies 15 seconds as a timeout. After timeout, we need to display the timeout UI. Then we can use the withTimeout function as follows:

import kotlinx.coroutines.*

fun main() = runBlocking {
    withTimeout(1300L) {
        repeat(1000) { i ->
            println("I'm sleeping $i ...")
            delay(500L)
        }
    }
}

The printed log is as follows:

I’m sleeping 0 …
I’m sleeping 1 …
I’m sleeping 2 …
Exception in thread “main” kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1300 ms

Then we can use * try {…} catch (e: TimeoutCancellationException) {…} * to handle the timeout logic.
By using withTimeoutOrNull will return null after timeout. With this feature, it can also be used to handle timeout logic.

fun main() = runBlocking {
    val result = withTimeoutOrNull(1300L) {
        repeat(1000) { i ->
            println("I'm sleeping $i ...")
            delay(500L)
        }
        "Done" // will get cancelled before it produces this result
    }
    println("Result is $result")
}

The printed log is as follows:

I’m sleeping 0 …
I’m sleeping 1 …
I’m sleeping 2 …
Result is null

5 About writing test cases

Coroutines are still a new thing for us after all, and it is inevitable that something will go wrong, so for the coroutine code we write, it is essential to write more unit tests. The kotlinx-coroutines-test library can help us test the coroutine code, although its still It is in the testing stage, but you can still learn it.
In view of the limited space, the official test case writing instructions are temporarily posted here:
Testing coroutines through behavior
Testing coroutines directly

参考:
Using Kotlin Coroutines in your Android App
Advanced Coroutines with Kotlin Flow and LiveData

Published 82 original articles · Like 86 · Visit 110,000+

Guess you like

Origin blog.csdn.net/unicorn97/article/details/105170501