Comprehensive understanding of Kotlin coroutines (Coroutine/Channel/Flow)

Preface

The concept of coroutines began to appear in 1958, earlier than threads. Currently, many languages ​​have begun to natively support coroutines, among which Kotlin natively supports coroutines. Today, we will explain Kotlin coroutine (Coroutine/Channel/Flow).

What is a coroutine?

Coroutines are collaborative tasks, and threads are preemptive tasks. Essentially, both are concurrent. The core of coroutines is one word: Scope< a i=2>, if you understand what scope is, you will understand coroutine.

Kotlin coroutines are thread libraries, not coroutines? Is the thread pool used by internal code?

  1. The most well-known coroutine language Go also maintains threads internally.
  2. Coroutines are only convenient for developers to handle asynchronous processing (which can reduce the number of threads), and threads can exert their performance
  3. Coroutine is a concept and has nothing to do with the specific implementation method
  4. The coroutines in the kotlin standard library do not contain thread pool code, and only the extension library implements the thread pool internally.

Coroutine design source

  1. Kotlin's coroutine perfectly replicates Google's Go language's coroutine design pattern (scope/channel/select), embodying the scope as an object; and can better control the scope life cycle;
  2. await mode (JavaScript asynchronous task solution)
  3. Kotlin creates Flow with reference to RxJava reactive framework
  4. When using coroutines, you don’t need to consider thread issues. You only need to use different schedulers in different scenarios (the scheduler will optimize specific tasks).

Kotlin coroutine features

1:Use scenario

Assume that there are multiple interfaces for the same page on the mobile side. If serial network requests are used one by one, the network request time will be many times slower than concurrent network requests. This situation can be implemented through coroutines.

At present, computers use multi-core CPUs to improve computing capabilities, so mastering concurrent programming is the future trend.

2: Coroutine advantages

  1. Concurrency is easy to implement
  2. No callback nesting occurs. Code structure is clear
  3. Easy to package and extend
  4. The performance cost of creating a coroutine is better than that of creating a thread. One thread can run multiple coroutines, and a single thread can be asynchronous.

3: Experimental characteristics

The official version of coroutines was released in Kotlin 1.3. Currently, there are still unstable functions (which do not affect project development), which are identified by annotations.

@FlowPreview 代表可能以后存在Api函数变动

@ExperimentalCoroutinesApi  代表目前可能存在不稳定的因素的函数

@ObsoleteCoroutinesApi 可能存在被废弃的可能

4: Composition of coroutine

The main components of Kotlin's coroutine are divided into three parts:

  1. CoroutineScope Coroutine scope: Each coroutine body has a scope, and whether it is asynchronous or synchronous is determined by the scope.
  2. Channel channel: Data is sent and received like a channel. Data can be transferred between coroutines or control blocking and continuation.
  3. Flow response flow: similar to RxJava and other structural writing methods

Recommended project architecture MVVM/MVI + Kotlin + Coroutine + JetPack, which mainly brings the following advantages:

  1. Simple, reducing code by about 70%
  2. Two-way data binding (DataBinding)
  3. Concurrent asynchronous tasks (network) doubling speed
  4. More robust data preservation and recovery

If you want to replace RxJava, I highly recommend the following two libraries:

frame Instructions for use
Net A coroutine concurrent network request library designed specifically for Android. Timers/pollers are also designed using coroutine Channels.
Channel Event distribution framework based on coroutine/LiveData

Create coroutine

The coroutine in the kotlin standard library is too crude and not suitable for developers to use. Here we use the coroutine extension library:

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0"

1: Start the main coroutine

There are three ways to start the main coroutine:

  1. The life cycle is consistent with App, cannot be canceled (Job does not exist), and there is no thread blocking:
fun main() {
    
    
    GlobalScope.launch {
    
     // 在后台启动一个新的协程并继续
        delay(1000L)
        println("World!")
    }
    Thread.sleep(2000) // 防止JVM虚拟机退出
}

What is said here is that GlobalScope does not have a job, but all launched launches have jobs. GlobalScope itself is a scope, and launch belongs to its sub-scope;

  1. There is no thread blocking, it can be canceled, and the coroutine life cycle can be controlled through CoroutineContext:
fun main() {
    
    
    CoroutineScope(Dispatchers.IO).launch {
    
    
    }
    Thread.sleep(1000)
}

  1. Thread blocking is suitable for unit testing and does not require delayed blocking to prevent the JVM virtual machine from exiting. runBlocking is a global function and can be called anywhere

Generally we will not use runBlocking in projects, because blocking the main thread has no meaning in starting it.

fun main() = runBlocking {
    
     
    // 阻塞线程直到协程作用域内部所有协程执行完毕
}

2: Create a scope

Functions can also be used inside the coroutine to create other coroutine scopes, which are divided into two types of creation functions:

  1. The extension function of CoroutineScope can only create other scopes within the scope.
  2. Inside the function modified by suspend
  3. A coroutine will always wait for all coroutines in its internal scope to finish executing before closing the coroutine.

You can also create sub-coroutine scopes within the main coroutine. There are two types of creation functions:

  1. Blocking scope (serial): Will block the current scope
  2. Suspended scope (concurrency): Will not block the current scope

Synchronized scope function
Synchronous scope functions are all suspend functions.

  1. withContext can switch the scheduler and return results.
  2. coroutineScope creates a coroutine scope, which blocks the current scope and waits for its sub-coroutines to finish executing before resuming and returning results.
  3. supervisorScope uses SupervisorJob's coroutineScope. Exceptions will not cancel the parent coroutine.
public suspend fun <T> withContext(
    context: CoroutineContext,
    block: suspend CoroutineScope.() -> T
): T
// 返回结果; 可以和当前协程的父协程存在交互关系, 主要作用为来回切换调度器

public suspend inline operator fun <T> CoroutineDispatcher.invoke(
    noinline block: suspend CoroutineScope.() -> T
): T = withContext(this, block)
// withContext工具函数而已

public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R

public suspend fun <R>  supervisorScope(block: suspend CoroutineScope.() -> R): R

public suspend inline fun <T> suspendCoroutine(crossinline block: (Continuation<T>) -> Unit): T

Asynchronous scope function
These two functions do not belong to suspend and can only be called by CoroutineScope.

  1. launch: asynchronous and concurrent, no result returned
  2. async: asynchronous concurrency, returns results
public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job

public fun <T> CoroutineScope.async(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
): Deferred<T>

3: Concurrency

Asynchronous tasks in the same coroutine scope start executing according to the sequential principle; suitable for serial network requests, when an asynchronous task requires the result of the previous asynchronous task.

It takes time for a coroutine to suspend, so asynchronous coroutines will always execute slower than synchronous code.

fun main() = runBlocking<Unit> {
    
    
    launch {
    
    
        System.err.println("(Main.kt:34)    后执行")
    }

    System.err.println("(Main.kt:37)    先执行")
}

Concurrent tasks can be created when using async functions in coroutine scope

public fun <T> CoroutineScope.async(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
): Deferred<T>

for example:

fun main() = runBlocking<Unit> {
    
    
	val name = async {
    
     getName() }
    val title = async {
    
     getTitle() }

    System.err.println("(Main.kt:35)    result = ${
      
      name.await() + title.await()}")
    delay(2000)
}

  1. Return object Deferred; obtain the result value through function await
  2. Deferred collections can also use awaitAll() to wait for all completions
  3. If you do not execute the await task, you will also wait for the coroutine to be closed after execution.
  4. If the Deferred does not execute the await function, the exception thrown inside async will not be caught by logCat or tryCatch, but it will still cause scope cancellation and exception crash; but the exception information will be re-thrown when await is executed.

Lazy concurrency
When the start in the async function is set to CoroutineStart.LAZY, the asynchronous task (or start function) will only start to be executed when await of the Deferred object is called.

startup mode

  1. DEFAULT execute immediately
  2. LAZY does not start executing until the Job executes start or join.
  3. ATOMIC cannot be canceled before scope execution begins
  4. UNDISPATCHED does not execute any scheduler and executes directly in the current thread, but will switch according to the scheduler of the first suspended function

Exception
If an exception occurs in the coroutine, the parent coroutine will be canceled and all other child coroutines of the parent coroutine will also be cancelled.

3:Deferred

Deferred inherits from Job. Provide a global function for creating a CompletableDeferred object, which can implement custom Deferred functions.

public suspend fun await(): T 
// 结果
public val onAwait: SelectClause1<T>
// 在select中使用

public fun getCompleted(): T
// 如果完成[isCompleted]则返回结果, 否则抛出异常
public fun getCompletionExceptionOrNull(): Throwable?
// 如果完成[isCompleted]则返回结果, 否则抛出异常

for example:

fun main() = runBlocking<Unit> {
    
    
    val deferred = CompletableDeferred<Int>()
    
    launch {
    
    
        delay(1000 )
        deferred.complete(23)
    }

    System.err.println("(Demo.kt:72)    结果 = ${
      
      deferred.await()}")
}

Create the top-level function of CompletableDeferred:

public fun <T> CompletableDeferred(parent: Job? = null): CompletableDeferred<T>
public fun <T> CompletableDeferred(value: T): CompletableDeferred<T>

CompletableDeferred function:

public fun complete(value: T): Boolean
// 结果

public fun completeExceptionally(exception: Throwable): Boolean
// 抛出异常, 异常发生在`await()`时

public fun <T> CompletableDeferred<T>.completeWith(result: Result<T>): Boolean
// 可以通过标记来判断是否成功, 避免异常抛出

CoroutineScope

Creating this object means creating a coroutine scope.

Structured concurrency
If you watch coroutine tutorials, you may often see this word. This is to open a new coroutine within the scope; the parent coroutine will restrict the child coroutine. The life cycle of the process, the child coroutine inherits the context of the parent coroutine, this hierarchical relationship is structured concurrency

Open multiple sub-coroutines in a coroutine scope for concurrent behavior.

CoroutineContext

Coroutine context, I think the coroutine context can be regarded as a Context that contains the basic information of the coroutine, which can determine the name or operation of the coroutine.

Create a new scheduler:

fun newSingleThreadContext(name: String): ExecutorCoroutineDispatcher
fun newFixedThreadPoolContext(nThreads: Int, name: String): ExecutorCoroutineDispatcher

Note⚠️: Creating a new scheduler consumes resources. It is recommended to reuse it and use the close function to release it when it is no longer needed.

1:Scheduler

Dispatchers inherits from CoroutineContext. This enumeration has three implementations; represents different thread scheduling; when the function does not use a scheduler, it takes over the scheduler of the current scope:

  1. Dispatchers.Unconfined does not specify a thread. If the sub-coroutine switches threads, the next code will also run on this thread.
  2. Dispatchers.IO is suitable for IO reading and writing
  3. Dispatchers.Main varies depending on the platform, and is the main thread on Android
  4. Dispatchers.Default default scheduler, executes the coroutine body in the thread pool, suitable for calculation operations

Execute immediately

Dispatchers.Main.immediate

Immediate belongs to the attribute that all schedulers have. This attribute means that if you are currently in the scheduler and do not perform scheduler switching, it can be understood that it belongs to the synchronization coroutine scope in the same scheduler.

For example, the launch function's opening scope will be executed in a lower order than subsequent code, but coroutines using this attribute are executed sequentially.

for example:

CoroutineScope(Job() + Dispatchers.Main.immediate).launch {
    
    
	// 执行顺序 1
}

// 执行顺序 2

CoroutineScope(Job() + Dispatchers.Main).launch {
    
    
		// 执行顺序 4
}

// 执行顺序 3

2:Coroutine naming

By creating a CoroutineName object and specifying the coroutine name as the parameter in the constructor, CoroutineName inherits from CoroutineContext.

launch(CoroutineName("吴彦祖")){
    
    

}

The coroutine context name is used for debugging convenience.

3: Coroutine hangs

The yield function allows the current coroutine to temporarily suspend the execution of other coroutine bodies. If there are no other concurrent coroutine bodies, it will continue to execute the current coroutine body (equivalent to an invalid call)

public suspend fun yield(): Unit

Suspension may often be mentioned in coroutines. Suspension can be understood as pausing this code (scope) and then executing subsequent code; Suspension functions generally represent functions modified by the suspend keyword. Suspend requires only those modified by suspend. The function is called internally, but the keyword itself does not do anything. It is just to restrict developers from calling at will.

Pending function calls display an arrow icon in the left line number column.

4:Job

In coroutines, Job is usually called a job, which represents a coroutine work task. It also inherits from CoroutineContext.

val job = launch {
    
    

}

Job belongs to interface

interface Job : CoroutineContext.Element

function

public suspend fun join()
// 等待协程执行完毕都阻塞当前线程
public val onJoin: SelectClause0
// 后面提及的选择器中使用

public fun cancel(cause: CancellationException? = null)
// 取消协程
public suspend fun Job.cancelAndJoin()
// 阻塞并且在协程结束以后取消协程

public fun start(): Boolean
public val children: Sequence<Job>
// 全部子作业

public fun getCancellationException(): CancellationException

public fun invokeOnCompletion(
  onCancelling: Boolean = false, 
  invokeImmediately: Boolean = true, 
  handler: CompletionHandler): DisposableHandle
// p1: 当为true表示cancel不会回调handler
// p2: 当为true则先执行[handler]然后再返回[DisposableHandle], 为false则先返回[DisposableHandle]

public fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle
// 当其作用域完成以后执行, 主协程指定才有效, 直接给CoroutineScope指定时无效的
// 手动抛出CancellationException同样会赋值给cause

state

The current status of the JOB can be obtained through fields.

public val isActive: Boolean
public val isCancelled: Boolean
public val isCompleted: Boolean

extension function

public fun Job.cancelChildren(cause: CancellationException? = null)

public suspend fun Job.cancelAndJoin()

CoroutineContext exists in each coroutine scope. And Job objects exist in the coroutine context.

coroutineContext[Job]

5: End the coroutine

If there is a calculation task in the scope of the coroutine (including logging all the time), it cannot be canceled. If the delay function is used, it can be canceled.

fun main() = runBlocking<Unit> {
    
    

  val job = launch(Dispatchers.Default) {
    
    
    while (true){
    
    
      delay(100) // 这行代码存在则可以成功取消协程, 不存在则无法取消
      System.err.println("(Main.kt:30)    ")
    }
  }
  
  delay(500)
  job.cancel() 
  System.err.println("(Main.kt:42)    结束")
}

Determine whether it should end by using the isActive property inside the coroutine:

fun main() = runBlocking<Unit> {
    
    

    val job = launch(Dispatchers.Default) {
    
    
        while (isActive) {
    
     // 一旦协程被取消则为false
            System.err.println("(Main.kt:30)    ")
        }
    }

    delay(500)
    job.cancel()
    System.err.println("(Main.kt:42)    结束")
}

6: Release resources

Coroutines may be manually canceled, but some resources need to be released when the coroutine is canceled. This operation can be performed in finally.

Finally will be executed anyway.

fun main() = runBlocking<Unit> {
    
    

    val startTime = System.currentTimeMillis()
    val job = launch(Dispatchers.Default) {
    
    
        try {
    
    
            repeat(1000){
    
    
                System.err.println("(Main.kt:31)    it = $it")
                delay(500)
            }
        } finally {
    
    
           // 已被取消的协程无法继续挂起
        }
    }
    delay(1500)
    job.cancel()
    System.err.println("(Main.kt:42)    ")
}

Start the coroutine again
You can continue to suspend the coroutine in the canceled coroutine through withContext and NonCancellable; this usage can actually be regarded as creating a task that cannot be canceled

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")
}

7:Context combination

The coroutine scope can receive multiple CoroutineContexts as context parameters; CoroutineContext itself belongs to the interface, and many context-related classes implement it.

When configuring multiple CoroutineContexts, you can specify multiple coroutine contexts at the same time through the **+** symbol. Each implementation object may contain part of the information and can have overwriting behavior, so there is overwriting behavior in the order when adding.

val a = CoroutineScope(SupervisorJob() + coroutineContext).launch(handler) {
    
    
  delay(1000)
  System.err.println("(Main.kt:51)    ${
      
      Thread.currentThread()}")
}

launch(Dispatchers.IO + CoroutineName("吴彦祖")){
    
    	}

Coroutine local variables
You can use ThreadLocal to obtain the thread's local variables, but you need to use the extension function asContextElement to convert the coroutine context into the coroutine context as a parameter when creating the coroutine

This local variable acts within the coroutine scope that holds the coroutine context.

public fun <T> ThreadLocal<T>.asContextElement(value: T = get()): ThreadContextElement<T> = ThreadLocalElement(value, this)

8: timeout

public suspend fun <T> withTimeout(timeMillis: Long, block: suspend CoroutineScope.() -> T): T
// 超过指定时间timeMillis自动结束协程; 
// 当没有超时时返回值获取并且继续执行协程; 
// 当超时会抛出异常TimeoutCancellationException, 但是不会导致程序结束

public suspend fun <T> withTimeoutOrNull(timeMillis: Long, block: suspend CoroutineScope.() -> T): T?
// 如果超时不会结束协程而是返回null

TimeoutCancellationException cannot be thrown manually because its constructor is private

1: Global coroutine scope

The global coroutine scope belongs to a singleton object, and the entire JVM virtual machine has only one instance object; its life cycle also follows the JVM. When using the global coroutine scope, be careful to avoid memory leaks.

public object GlobalScope : CoroutineScope {
    
    
    /**
     * Returns [EmptyCoroutineContext];
     */
    override val coroutineContext: CoroutineContext
        get() = EmptyCoroutineContext
}

The global coroutine scope does not inherit the context of the parent coroutine scope, so it will not be canceled because the parent coroutine is canceled.

Startup mode:

  • DEFAULT executes the coroutine body immediately
  • ATOMIC executes the coroutine body immediately, but cannot cancel the coroutine before starting execution of the coroutine
  • UNDISPATCHED immediately executes the coroutine body in the current thread. The first suspended function is executed in the thread where the function is located, and subsequent executions are executed in the thread specified by the function.
  • LAZY will execute the coroutine only when start or join is manually executed.

9:Coroutine cancellation

If the coroutine body has been executed, it is actually non-cancelable. In the coroutine body, isActive is used to determine whether the coroutine is active.

You can customize the exception object by specifying the exception CancellationException as a parameter of the cancellation function.

1: Non-cancellable coroutine scope

NonCancellable This singleton object is used by the withContext function to create a coroutine scope that cannot be canceled.

withContext(NonCancellable) {
    
    
  delay(2000)
}

for example:

fun main() = runBlocking {
    
    
  launch {
    
    
    delay(1000)
    System.err.println("(Main.kt:19)    ")
  }

  launch {
    
    
    withContext(NonCancellable) {
    
    
      delay(2000)
      System.err.println("(Main.kt:26)    ")
    }
  }

  delay(500) // 防止launch还未开启withContext就被取消
  cancel()
}

  • When the subscope contains unterminated tasks, it will wait for the task to be completed before canceling it (delay does not exist, Thread.sleep can simulate unfinished tasks)
  • Throwing CancellationException is regarded as the end exception, and invokeOnCompletion will also be executed (containing the exception object), but other exceptions will not execute invokeOnCompletion.
2: Cancel GlobalScope

GlobalScope belongs to the global coroutine. The coroutine opened by it does not own a job, so the coroutine cannot be canceled. However, you can specify a job for the coroutine scope opened by GlobalScope and then use the job to cancel the coroutine.

10:Coroutine exception

An object with the same name can be created through the CoroutineExceptionHandler function. This interface inherits from CoroutineContext and is also passed to the global coroutine scope by setting context parameters. When the scope throws an exception, it will be received by the callback function of the object and will not throw an exception

11: Release resources

  1. CoroutineExceptionHandler is only effective as the outermost parent coroutine context, because exceptions will be thrown up layer by layer. Unless the SupervisorJob supervises the job to prohibit exception throwing, the exception handler in the child scope can catch the exception.
  2. The CoroutineExceptionHandler exception handler does not prevent the cancellation of the coroutine scope. It only monitors the exception information of the coroutine to prevent the JVM from throwing an exception and exiting the program.
  3. As long as an exception occurs, the parent coroutine and all its child coroutines will be canceled. This is a two-way exception cancellation mechanism. The supervisor job (SupervisorJob) mentioned later is a one-way downward pass (that is, it will not be thrown upward). )
  4. CoroutineExceptionHandler will be passed down to the sub-scope as the coroutine context by the scope (unless the sub-scope is specified separately)

(Example below) Do not try to use try/catch to catch launch scope exceptions, as they cannot be caught.

try {
    
    
  launch {
    
    
    throw NullPointerException()
  }
} catch (e: Exception) {
    
    
  e.printStackTrace()
}

1: Coroutine cancellation exception

Canceling a coroutine's job will cause an exception, but it will be ignored by the default exception handler. However, we can see the exception information by catching:

fun main() = runBlocking<Unit> {
    
    
  val job = GlobalScope.launch {
    
    

    try {
    
    
      delay(1000)
    } catch (e: Exception) {
    
    
      e.printStackTrace()
    }
  }

  job.cancel(CancellationException("自定义一个用于取消协程的异常"))
  delay(2000)
}

Job cancellation function

public fun cancel(cause: CancellationException? = null)

cause: If the parameter is not passed, the default is JobCancellationException.

2: Exception handling in global coroutine scope
val exceptionHandler = CoroutineExceptionHandler {
    
     coroutineContext, throwable ->
       System.err.println("(Main.kt:41):main   coroutineContext = $coroutineContext, throwable = $throwable")
}

GlobalScope.launch(exceptionHandler) {
    
     

}

It is invalid for the sub-coroutine to set an exception handler. Even if an error is set, it will still be thrown to the parent coroutine, which is meaningless. Unless an exception handler + supervisor job (SupervisorJob) is used at the same time, this will prevent the error of the sub-coroutine from being raised. Throw (the supervision operation will be explained in detail later), so that it can be handled by its internal exception handler.

3: Exception aggregation and unpacking

The global coroutine scope also has a nested child-parent relationship, so exceptions may also throw multiple exceptions in sequence.

fun main() = runBlocking {
    
    
    val handler = CoroutineExceptionHandler {
    
     _, exception ->
				// 第三, 这里的异常是第一个被抛出的异常对象
        println("捕捉的异常: $exception 和被嵌套的异常: ${
      
      exception.suppressed.contentToString()}")
    }
    val job = GlobalScope.launch(handler) {
    
    
        launch {
    
    
            try {
    
    
                delay(Long.MAX_VALUE)
            } finally {
    
     // 当父协程被取消时其所有子协程都被取消, finally被取消之前或者完成任务之后一定会执行
                throw ArithmeticException() // 第二, 再次抛出异常, 异常被聚合
            }
        }
        launch {
    
    
            delay(100)
            throw IOException() // 第一, 这里抛出异常将导致父协程被取消
        }
        delay(Long.MAX_VALUE)
    }
    job.join() // 避免GlobalScope作用域没有执行完毕JVM虚拟机就退出
}

12: Supervise operations

Generally, an exception in a child coroutine will cause the parent coroutine to be canceled. At the same time, an exception in the parent coroutine will cancel all child coroutines; but sometimes when a child coroutine encounters an exception, we do not want the parent coroutine to be canceled as well, but Only all sub-coroutines are canceled (only exceptions are passed down), this use is to use SupervisorJob job

Create supervision job objects

fun main() = runBlocking<Unit> {
    
    
    CoroutineScope(coroutineContext).launch {
    
    
        
        launch(SupervisorJob(coroutineContext[Job]) + CoroutineExceptionHandler {
    
     _, _ ->  }) {
    
    
            throw NullPointerException()
        }

        delay(500)
        println("( Process.kt:13 )    ")
    }

    println("( Process.kt:16 )    finish")
}

  1. CoroutineExceptionHandler must be added to handle exceptions, otherwise the exception will still be passed upward to cancel the parent coroutine.
  2. Directly creating the SupervisorJob() object and passing it into the scope will lead to the problem that the life cycle of the scope and the parent coroutine are not unified, that is, the child coroutine is still active after the parent coroutine is canceled, so the parameter needs to be specified as coroutineContext[Job ] That is, the job object passed into the parent coroutine
  3. SupervisorJob can only catch exceptions in the scope of the internal coroutine, and cannot directly capture the internal coroutine
    kotlin copy code
supervisorScope {
    
    
    // throw NoSuchFieldException() 抛出崩溃
    
    launch {
    
    
         throw NoSuchFieldException() // 不会抛出
    }
}

Supervised job added in withContext and async has no effect

Create an exception directly and pass it down to the scope of the supervision job

public suspend fun <R>  supervisorScope(block: suspend CoroutineScope.() -> R): R

  1. This function is blocking
  2. Has return value
  3. The supervisorScope function still uses the Job of the current scope, so it can be canceled following the current scope life cycle.
fun main() = runBlocking<Unit> {
    
    
    CoroutineScope(coroutineContext).launch {
    
    
        
      // 在该作用域内只要设置CoroutineExceptionHandler都仅会向下传递
        supervisorScope {
    
    
            launch(CoroutineExceptionHandler {
    
     _, _ ->  }) {
    
    
                throw NullPointerException()
            }

            launch {
    
    
                delay(1000) // 即使上面的launch抛出异常也会继续执行这里
                println("( Process.kt:18 )    ")
            }
        }
    }

    println("( Process.kt:16 )    finish")
}

13: Catching exceptions

Exception catching in scope is different from general exception catching:

  • CoroutineExceptionHandler can catch all exceptions in child scopes
  • Async can use supervision jobs to catch exceptions that occur internally, but its await requires trycatch
  • launch requires the supervision job to be used together with the exception handler, both are indispensable.
  • withContext/supervisorScope/coroutineScope/select can trycatch to catch exceptions

14: Original coroutine

function callback field describe
suspendCoroutine Continuation Result
suspendCancellableCoroutine CancellableContinuation Cancellable
suspendAtomicCancellableCoroutine CancellableContinuation Cancellable
1:[Continuation]
public val context: CoroutineContext
public fun resumeWith(result: Result<T>)

2:[CancellableContinuation] -| Continuation
public val isActive: Boolean
public val isCompleted: Boolean
public val isCancelled: Boolean

public fun resume(value: T, onCancellation: (cause: Throwable) -> Unit)
public fun tryResume(value: T, idempotent: Any? = null): Any?
public fun tryResumeWithException(exception: Throwable): Any?
public fun completeResume(token: Any)

public fun cancel(cause: Throwable? = null): Boolean

public fun invokeOnCancellation(handler: CompletionHandler)
public fun CoroutineDispatcher.resumeUndispatched(value: T)
public fun CoroutineDispatcher.resumeUndispatchedWithException(exception: Throwable)

Not thread safe

Solving thread unsafe issues

  1. mutex lock
  2. Switch threads to achieve single threading
  3. Channel

1: Mutually exclusive

Equivalent to Lock replacement in Java: Mutex

Create mutex object

public fun Mutex(locked: Boolean = false): Mutex
// p: 设置初始状态, 是否立即上锁

Use extension functions to automatically lock and unlock

public suspend inline fun <T> Mutex.withLock(owner: Any? = null, action: () -> T): T
// owner: 钥匙

function

public suspend inline fun <T> Mutex.withLock(owner: Any? = null, action: () -> T): T
public fun holdsLock(owner: Any): Boolean
// owner是否被用于锁
public fun tryLock(owner: Any? = null): Boolean
// 使用owner来上锁, 如果owner已上锁则返回false

Channel

  1. Multiple scopes can send and receive data through a Channel object
  2. Channel design refers to the chan design of Go language, which can be used to control the blocking and continuation of the scope (by cooperating with select)
  3. Deprecated functions began to appear in coroutines 1.5 and are not introduced here.

Channel is an interface and cannot be created directly. We need to create its implementation class through the function Channel()

public fun <E> Channel(capacity: Int = RENDEZVOUS): Channel<E> =
  when (capacity) {
    
    
    RENDEZVOUS -> RendezvousChannel() // 无缓存
    UNLIMITED -> LinkedListChannel() // 无限制
    CONFLATED -> ConflatedChannel()  // 合并
    BUFFERED -> ArrayChannel(CHANNEL_DEFAULT_CAPACITY) // 64
    else -> ArrayChannel(capacity) // 指定缓存大小
  }

Pay attention to this capacity:

1: Buffer size, default 0
2: When Channel sends a piece of data, the channel will be suspended (the subsequent code will not be executed), only when receiving this data The suspension will be lifted and execution will continue; but we can set the cache size

The channel is allowed to be traversed to obtain the currently sent data

val channel = Channel<Int>()

for (c in channel){
    
    

}

public suspend fun yield(): Unit

1:Channel

Channel接口同时实现发送渠道(SendChannel)和接收渠道(ReceiveChannel)两个接口, 所以既能发送又能接收数据
public interface Channel<E> : SendChannel<E>, ReceiveChannel<E> 

1:SendChannel
public val isClosedForSend: Boolean
// 是否关闭
public fun close(cause: Throwable? = null): Boolean
// 关闭发送通道

public fun trySend(element: E): ChannelResult<Unit>
// 发送消息, 非suspend函数

public suspend fun send(element: E)
// 发送消息

public fun invokeOnClose(handler: (cause: Throwable?) -> Unit)
// 当通道关闭时执行回调

public val onSend: SelectClause2<E, SendChannel<E>>
// 立即发送数据(如果允许), 在select中使用

  • After the sending channel is closed, you cannot continue to use ReceiveChannel to receive data, which will cause ClosedReceiveChannelException to be thrown.
  • Functions such as the prefix try* indicate that they are not suspend functions and do not need to be called in the coroutine scope.
2:ReceiveChannel
public val isClosedForReceive: Boolean
// SendChannel是否已经关闭通道, 如果关闭通道以后还存在缓存则会接收完缓存之后返回false

public val isEmpty: Boolean // 通道是否为空

public suspend fun receive(): E
public fun tryReceive(): ChannelResult<E>
public suspend fun receiveCatching(): ChannelResult<E>
// 接受通道事件

public val onReceive: SelectClause1<E> // 如果通道关闭, 抛出异常
public val onReceiveCatching: SelectClause1<ChannelResult<E>>// 此函数不会抛出异常
// 在select中使用的监听器, 推荐使用第三个函数

public suspend fun receiveOrClosed(): ValueOrClosed<E>
// `ValueOrClosed`对象可以判断通道是否已关闭

public fun cancel(cause: CancellationException? = null)
// 关闭通道

public fun <T> ReceiveChannel<T>.receiveAsFlow(): Flow<T> = ChannelAsFlow(this, consume = false)
// 将Chan转成Flow

  1. The sending and receiving of the channel will cause the scope to be blocked, but sending a message can prevent it from blocking by setting the cache, or canceling the channel can allow the blocking to continue.
  2. Channels only allow sending and receiving in suspend functions, but there are no restrictions on creating channels
  3. Closing the channel will cause receive to throw an exception
  4. SendChannel is not allowed to send or receive data after executing the close function, otherwise an exception will be thrown.
  5. If the scope of the send|receive function of Channel is canceled, canceling will not cause the channel to end (isClosedForReceive returns false)
  6. receive instead of traversing will cause the scope to get stuck
1:consume

ReceiveChannel can not only receive events through iterators, but also use the consume series of functions to receive events.

Essentially there is no difference between consume and iteration except that consume will automatically cancel the channel (through the cancel function) when an exception occurs.

Source code

public inline fun <E, R> ReceiveChannel<E>.consume(block: ReceiveChannel<E>.() -> R): R {
    
    
    var cause: Throwable? = null
    try {
    
    
        return block() // 直接返回
    } catch (e: Throwable) {
    
    
        cause = e
        throw e
    } finally {
    
    
        cancelConsumed(cause) // 如果发生异常取消通道
    }
}

The consumeEach function only iteratively receives events and exceptions are automatically canceled; it is generally recommended to use the consume function to receive events.

3:BroadcastChannel

The difference between this channel and ordinary channels is that each of its data can be received by each scope; after one data of the default channel is received, other coroutines can no longer receive data.

Broadcast channels create objects through global functions

public fun <E> BroadcastChannel(capacity: Int): BroadcastChannel<E>

The broadcast channel itself inherits from SendChannel and can only send data. The receiving channel can be obtained through the function.

public fun openSubscription(): ReceiveChannel<E>

Cancel channel

public fun cancel(cause: CancellationException? = null)

Convert Channel to BroadcastChannel

fun <E> ReceiveChannel<E>.broadcast(
    capacity: Int = 1,
    start: CoroutineStart = CoroutineStart.LAZY
): BroadcastChannel<E>

Quickly create a broadcast sending channel in the coroutine scope through extension functions

public fun <E> CoroutineScope.broadcast(
    context: CoroutineContext = EmptyCoroutineContext,
    capacity: Int = 1,
    start: CoroutineStart = CoroutineStart.LAZY,
    onCompletion: CompletionHandler? = null,
    @BuilderInference block: suspend ProducerScope<E>.() -> Unit
): BroadcastChannel<E> 

1: iteration channel

Receive channel implementation operator overloading can use iteration

public operator fun iterator(): ChannelIterator<E>

Example

for (i in produce){
    
    
	// 收到每个发型的消息
}

When multiple coroutines receive data from the same channel, they will receive the data in turn. The channel is fair to multiple coroutines.

4:Produce

The above introduction is about creating a Channel object to send and receive data, but you can also quickly create and return a ReceiveChannel object with the ability to send data through the extension function.

public fun <E> CoroutineScope.produce(
    context: CoroutineContext = EmptyCoroutineContext,
    capacity: Int = 0,
    @BuilderInference block: suspend ProducerScope<E>.() -> Unit
): ReceiveChannel<E>

  • context: The scheduler and other information can be determined through the coroutine context.
  • capacity: initialize channel space

ProducerScope This interface inherits from SendChannel and CoroutineScope, and has the function of sending channel data and coroutine scope.

When the execution of the produce scope is completed, the channel will be closed. As mentioned earlier, closing the channel cannot continue to receive data.

1: Waiting for cancellation

This function will call back its function parameters when the channel is canceled. As mentioned earlier, when the coroutine is canceled, finally can be used to release memory and other operations. However, channel cancellation cannot use finally and can only use this function.

public suspend fun ProducerScope<*>.awaitClose(block: () -> Unit = {
    
    }) 
// [SendChannel.close] or [ReceiveChannel.cancel] 代表取消通道

5:Actor

You can create a coroutine scope with channel function through the actor function

public fun <E> CoroutineScope.actor(
    context: CoroutineContext = EmptyCoroutineContext,
    capacity: Int = 0, // todo: Maybe Channel.DEFAULT here?
    start: CoroutineStart = CoroutineStart.DEFAULT,
    onCompletion: CompletionHandler? = null,
    block: suspend ActorScope<E>.() -> Unit
): SendChannel<E>

Parameter description is as follows:

  • context: coroutine context
  • capacity: channel buffer space
  • start: coroutine startup mode
  • onCompletion: completion callback
  • block: Data can be sent in the callback function

This function is similar to the produce function,

  1. produce returns ReceiveChannel, which is used to receive data externally; actor returns SendChannel, which is used to send data externally.
  2. The actor's callback function has the attribute channel:Channel, which can both send and receive data. The attribute channel of produce belongs to SendChannel
  3. Whether it is producer or actor, their channels belong to Channel. They can both send and receive data. They only need to force the type.
  4. Channel itself can carry out two-way data communication, but the design of producer and actor belongs to the producer and consumer model in the design idea.
  5. They all belong to the combination of coroutine scope and data channel
6: Round robin

Both RxJava and coroutines support the function of the cycler. In my network request library, the cycler is also given functions such as pause|continue|multiple observers|reset etc.

The coroutine cycler here is relatively simple.

public fun ticker(
    delayMillis: Long,
    initialDelayMillis: Long = delayMillis,
    context: CoroutineContext = EmptyCoroutineContext,
    mode: TickerMode = TickerMode.FIXED_PERIOD
): ReceiveChannel<Unit>

The data returned by this channel is Unit

By default, it can be understood that the channel will always send Unit data after the specified interval.

fun main() = runBlocking<Unit> {
    
    
  
    val tickerChannel = ticker(delayMillis = 1000, initialDelayMillis = 0)

  // 每秒打印
    for (unit in tickerChannel) {
    
    
        System.err.println("unit = $unit")
    }
}

But if the downstream does not receive the data immediately after sending the data, but delays the use of the receive function to receive the channel data

The TickerMode enumeration has two fields:

  • FIXED_PERIOD default value, dynamically adjusts the time interval for the channel to send data. The time interval can be regarded as the upstream sending data.
  • FIXED_DELAY will only start calculating the interval time after receiving the data. The time interval can be regarded as the downstream receiving data.

This poller does not support multiple subscriptions | pause | continue | reset | complete. Here we introduce an Interval object in the NetNet library that has implemented all functions.

7:Select

Monitor the results of multiple Deferred/Channels in the select function callback, and only the channel or result callback that receives data the fastest will be executed.

1: Action

In the previous function introduction, you can see a series of on{action} variables, and their values ​​are all SelectClause{number} interface objects;

[SelectBuilder]

public interface SelectBuilder<in R> {
    
    
    public operator fun SelectClause0.invoke(block: suspend () -> R)
    public operator fun <Q> SelectClause1<Q>.invoke(block: suspend (Q) -> R)
    public operator fun <P, Q> SelectClause2<P, Q>.invoke(param: P, block: suspend (Q) -> R)
    public operator fun <P, Q> SelectClause2<P?, Q>.invoke(block: suspend (Q) -> R) = invoke(null, block)
    @ExperimentalCoroutinesApi
    public fun onTimeout(timeMillis: Long, block: suspend () -> R)
}

According to this defined extension function, you can use it directlyAction

object Functions used
SelectClause0 onJoin
SelectClause1 OnReceive
SelectClause2 onSend

for example:

@ObsoleteCoroutinesApi
@UseExperimental(InternalCoroutinesApi::class)
suspend fun selectMulti(a: Channel<Int>, b: Channel<Int>): String = select<String> {
    
    

    b.onReceive {
    
    
        "b $it" // 优先执行第一个, 不是函数原因, 而是顺序
    }

    b.onReceiveOrClosed {
    
    
        "b $it"
    }
    
    a.onSend(23) {
    
    
        "发送 23"
    }
}

fun main() = runBlocking<Unit> {
    
    
    val a = Channel<Int>(1) // 缓冲数量, 避免发送数据时阻塞
    val b = Channel<Int>(1)
    
    launch {
    
    
        b.send(24)
        val s = selectMulti(a, b)
        println("结果 = $s")
    }
}

  • onReceive will cause an exception to be thrown when closing the channel. If you do not want to throw an exception, you should use onReceiveOrClosed instead.
  • onSend This function is equivalent to Channel.send, which is to send a value. If you register multiple onSend, it will definitely be the first one to call back and return the result.
  • Even if a member has been selected (selected), it will not cause the scope of other member coroutines to end.

[ValueOrClosed]

public val isClosed: Boolean // 通道是否已关闭

public val value: T
public val valueOrNull: T?
// 两者都是获取通道内的值, 但是第2个如果通道关闭不会抛出异常而是返回NULL

  1. When a channel in select has both sending and receiving listeners, if both are executed (that is, the select is executed without being interrupted), an exception will be thrown.
  2. If a channel is monitored repeatedly (multiple actions), the first one will be executed first
  3. Closing the channel will also receive data, onReceive throws an exception, and onReceiveOrClose data is null.

Flow

Flow is similar to RxJava and is divided into three parts:

  1. upstream
  2. Operator
  3. downstream

Downstream receiving events require execution within the coroutine scope (suspend function)

1:Create Flow

public fun <T> flow(@BuilderInference block: suspend FlowCollector<T>.() -> Unit): Flow<T>

for example:

fun shoot() = flow {
    
    
    for (i in 1.;3) {
    
    
        delay(1000) // 假装我们在这里做了一些有用的事情
        emit(i) // 发送下一个值
    }
}
  1. Collections or Sequences can be converted into Flow objects through the asFlow function.
  2. You can also create Flow objects directly through fowOf just like creating a collection.
  3. Convert Channel channel to Flow
public fun <T> ReceiveChannel<T>.consumeAsFlow(): Flow<T>                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   

  1. Even suspending functions can be converted to Flow
public fun <T> (suspend () -> T).asFlow(): Flow<T>

The callback functions of collect and flow themselves belong to the suspend function and can open the coroutine scope.

Create a function for Flow

function describe
flow Normal Flow
channelFlow Create a channel, which supports buffered channels and allows different CorotineContexts to send events
callbackFlow There is no difference from the channelFlow function except that an error will be reported if awaitClose is not used.
emptyFlow Empty Flow
flowOf Send data directly

Note⚠️:
1: The emission function emit of flow is not thread-safe and does not allow other threads to call it. If thread safety is required, please use channelFlow instead of flow
2: channelFlow uses the send function to send data

Example of sending data:

flow<Int> {
    
    
  emit(23)
}

channelFlow<Int> {
    
    
  send(23) // offer(23)
}

  1. offer can be used in non-suspend functions, send must be used in suspend functions
  2. Offer has a return value. If there is no element space, it will return false directly. Send will suspend and block waiting for new element space.

Flow can use callbackFlow to release resources when canceling the scope. Here is a demonstration of registering and canceling a broadcast AppWidgetProvider.

callbackFlow<Int> {
    
    
  val appWidgetProvider = AppWidgetProvider()
  registerReceiver(appWidgetProvider, IntentFilter()) // 注册
  awaitClose {
    
      // 该回调会在协程作用域被取消时回调
    unregisterReceiver(appWidgetProvider) // 注销
  }
}.collect {
    
     

}

1:Collect
1: Collect data

Flow is cold data, which requires calling the function collect to collect data before data is emitted; this series of functions also become terminal operators;

flow {
    
    
  emit(23)
}.collect {
    
    
	System.err.println("(Demo.kt:9)    it = $it")
}

Looking at the source code, you will find that this emit is actually the parameter function that executes collect.

The collect function represents receiving the data sent by the upstream

public suspend fun Flow<*>.collect() 
// 不做任何处理的收集器, 仅仅为了触发发射数据

public suspend inline fun <T> Flow<T>.collect(crossinline action: suspend (value: T) -> Unit): Unit
// 收集

public suspend fun <T> Flow<T>.collectLatest(action: suspend (value: T) -> Unit)
// 和上个函数的区别是: 如果在下游没有处理完情况下上游继续下个发射会导致上次的下游被取消

public suspend inline fun <T> Flow<T>.collectIndexed(
      crossinline action: suspend (index: Int, value: T) -> Unit
): Unit
// 具备索引和值的收集器

public fun <T> Flow<T>.launchIn(scope: CoroutineScope): Job
// 将Flow运行在指定的协程作用域内

[FlowCollector] emitter

public suspend fun emit(value: T)
// 发送一个数据

public suspend inline fun <T> FlowCollector<T>.emitAll(flow: Flow<T>)
// 发射另一个flow对象

2:Scheduler
1:Scheduler

By default, Flow uses the current thread or coroutine context in which it is located. Flow does not allow internal use of withContext to switch schedulers. Instead, the flowOn function should be used.

public fun <T> Flow<T>.flowOn(context: CoroutineContext): Flow<T>

What this function changes is the thread when the Flow function is internally launched, and when collect collects data, it will automatically switch back to the thread when the Flow was created.

2: cache

There is no need to wait for collection execution to execute the data emission immediately. The data is only temporarily cached to improve performance.

By default, it will be automatically cached when switching schedulers.

public fun <T> Flow<T>.buffer(capacity: Int = BUFFERED): Flow<T>

Merge function, this function is actually a buffer. When the downstream cannot process the upstream data in time, the data will be discarded.

public fun <T> Flow<T>.conflate(): Flow<T> = buffer(CONFLATED)

3: Merge

Combine multiple events and send them downstream

1:zip

Process the two flows in the callback function and return a new value R

When the lengths of the two flows are not equal, only the event with the shortest length is sent.

public fun <T1, T2, R> Flow<T1>.zip(other: Flow<T2>, transform: suspend (T1, T2) -> R): Flow<R>

for example:

val nums = (1.;3).asFlow().onEach {
    
     delay(300) } // 发射数字 1.;3,间隔 300 毫秒
val strs = flowOf("one", "two", "three").onEach {
    
     delay(400) } // 每 400 毫秒发射一次字符串
val startTime = System.currentTimeMillis() // 记录开始的时间
nums.zip(strs) {
    
     a, b -> "$a -> $b" } // 使用“zip”组合单个字符串
    .collect {
    
     value -> // 收集并打印
            println("$value at ${
      
      System.currentTimeMillis() - startTime} ms from start") 
    } 

2:combine
public fun <T1, T2, R> Flow<T1>.combine(
    flow: Flow<T2>, transform: suspend (a: T1, b: T2) -> R
): Flow<R>
// 组合两个流,在经过第一次发射以后,任意方有新数据来的时候就可以发射,另一方有可能是已经发射过的数据

public fun <T1, T2, R> Flow<T1>.combineTransform(
    flow: Flow<T2>,
    @BuilderInference transform: suspend FlowCollector<R>.(a: T1, b: T2) -> Unit
): Flow<R>

4: Collection

Flow is directly converted into an aggregate function

public suspend fun <T> Flow<T>.toList(destination: MutableList<T> = ArrayList()): List<T>
public suspend fun <T> Flow<T>.toSet(destination: MutableSet<T> = LinkedHashSet()): Set<T>
public suspend fun <T, C : MutableCollection<in T>> Flow<T>.toCollection(destination: C): C

5: Overlay
public suspend fun <S, T : S> Flow<T>.reduce(operation: suspend (accumulator: S, value: T) -> S): S

public suspend inline fun <T, R> Flow<T>.fold(
    initial: R,
    crossinline operation: suspend (acc: R, value: T) -> R
): R
// `acc`为上次回调函数返回值, 第一次为初始值, 等同于叠加效果; 该函数和reduce的区别就是支持初始值; reduce累计两次元素才会回调函数

public fun <T> Flow<T>.onEach(action: suspend (T) -> Unit): Flow<T>
public fun <T, R> Flow<T>.scan(initial: R, @BuilderInference operation: suspend (accumulator: R, value: T) -> R): Flow<R>
public fun <T, R> Flow<T>.scan(initial: R, @BuilderInference operation: suspend (accumulator: R, value: T) -> R): Flow<R>

6: Conversion
public inline fun <T, R> Flow<T>.map(crossinline transform: suspend (value: T) -> R): Flow<R>
public inline fun <T, R: Any> Flow<T>.mapNotNull(crossinline transform: suspend (value: T) -> R?): Flow<R>

public fun <T, R> Flow<T>.flatMapMerge(
    concurrency: Int = DEFAULT_CONCURRENCY,
    transform: suspend (value: T) -> Flow<R>
): Flow<R> = map(transform).flattenMerge(concurrency)
// 上游先发送所有的元素, 然后上游每个元素会导致回调函数中的Flow发送所有元素一次

public fun <T, R> Flow<T>.flatMapConcat(transform: suspend (value: T) -> Flow<R>): Flow<R>
// 等同于RxJava的FlatMap

public fun <T> Flow<Flow<T>>.flattenConcat(): Flow<T>
// 串行收集数据

public fun <T> Flow<Flow<T>>.flattenMerge(
  concurrency: Int = DEFAULT_CONCURRENCY
): Flow<T>
// 并发收集数据

public inline fun <T, R> Flow<T>.flatMapLatest(
  @BuilderInference crossinline transform: suspend (value: T) -> Flow<R>
): Flow<R> 
// 在每次 emit 新的数据以后,会取消先前的 collect

public fun <T> Flow<T>.withIndex(): Flow<IndexedValue<T>>
// 包含元素索引

7: Life cycle
public fun <T> Flow<T>.onStart(
    action: suspend FlowCollector<T>.() -> Unit
): Flow<T>
// 开始

public fun <T> Flow<T>.onCompletion(
    action: suspend FlowCollector<T>.(cause: Throwable?) -> Unit
): Flow<T>
// 回调函数中的参数`cause`如果为null表示正常完成没有抛出异常, 反之则抛出异常非正常结束, 
// 和catch函数一样只能监听到上游发生的异常, 但是无法避免异常抛出只能在异常抛出之前执行回调函数

public fun <T> Flow<T>.catch(action: suspend FlowCollector<T>.(cause: Throwable) -> Unit): Flow<T>
// 该函数只能捕获上游异常, 如果异常处于函数调用之下则依然会被抛出

8:Filter

Limit stream sending

public fun <T> Flow<T>.take(count: Int): Flow<T>
// 只接受指定数量事件
public fun <T> Flow<T>.takeWhile(predicate: suspend (T) -> Boolean): Flow<T>

public fun <T> Flow<T>.drop(count: Int): Flow<T>
// 丢弃指定数量事件
public fun <T> Flow<T>.dropWhile(predicate: suspend (T) -> Boolean): Flow<T>
// 回调函数判断是否丢弃或者接收, 只要丢弃或者接收后面就不会继续发送事件(结束流)

public inline fun <T> Flow<T>.filter(crossinline predicate: suspend (T) -> Boolean): Flow<T>
public inline fun <T> Flow<T>.filterNot(crossinline predicate: suspend (T) -> Boolean): Flow<T>
public inline fun <reified R> Flow<*>.filterIsInstance(): Flow<R> = filter {
    
     it is R } as Flow<R>
public fun <T: Any> Flow<T?>.filterNotNull(): Flow<T>

public suspend fun <T> Flow<T>.single(): T
// 期待只有一个元素, 否则抛出`IllegalStateException`
public suspend fun <T: Any> Flow<T>.singleOrNull(): T?
// 不抛出异常, 但如果不是仅有元素则返回null

public suspend fun <T> Flow<T>.first(): T
// 如果不存在一个元素则会抛出`NoSuchElementException`
public suspend fun <T> Flow<T>.first(predicate: suspend (T) -> Boolean): T
// 返回回调函数判断为true的第一个条件符合的元素

9: Try again
public fun <T> Flow<T>.retry(
    retries: Long = Long.MAX_VALUE, // 重试次数
    predicate: suspend (cause: Throwable) -> Boolean = {
    
     true }
): Flow<T>

public fun <T> Flow<T>.retryWhen(
  predicate: suspend FlowCollector<T>.(cause: Throwable, attempt: Long) -> Boolean
): Flow<T>

10:Filtering
public inline fun <T, R> Flow<T>.transform(
    @BuilderInference crossinline transform: suspend FlowCollector<R>.(value: T) -> Unit
): Flow<R>
// 转换函数, 可以在回调函数中发送新的元素

public fun <T, R> Flow<T>.transformLatest(                                      
  @BuilderInference transform: suspend FlowCollector<R>.(value: T) -> Unit
): Flow<R>

The difference between scan and reduce is:

  1. reduce is collected after all superposition calculations are completed
  2. Scan collects data once after each superposition.
11:StateFlow/SharedFlow

Inheritance relationship of class:
SharedFlow

|- MutableSharedFlow

|- StateFlow

​ |- MutableStateFlow

SharedFlow is hot flow data. It will be sent even if it is not collected, and then replayed during collection. You can use shareIn to convert cold flow into hot flow. You can also use the following function to create it directly.

public fun <T> MutableSharedFlow(
    replay: Int = 0, // 重放数量
    extraBufferCapacity: Int = 0, // 缓存数量(不包含重放数量)
    onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND
): MutableSharedFlow<T>

UseBufferOverflow

  1. DROP_LATEST discards the latest value
  2. DROP_OLDEST loses the oldest value
  3. SUSPEND suspend blocking

StateFlow can be seen as adding the features of LiveData on the basis of Flow. However, there is no life cycle following (unless life cycle scope such as lifecycleScope is used), and data can always be collected.

class LatestNewsViewModel(
    private val newsRepository: NewsRepository
) : ViewModel() {
    
    

    // Backing property to avoid state updates from other classes
    private val _uiState = MutableStateFlow(LatestNewsUiState.Success(emptyList()))
    // The UI collects from this StateFlow to get its state updates
    val uiState: StateFlow<LatestNewsUiState> = _uiState

    init {
    
    
        viewModelScope.launch {
    
    
            newsRepository.favoriteLatestNews
                // Update View with the latest favorite news
                // Writes to the value property of MutableStateFlow,
                // adding a new element to the flow and updating all
                // of its collectors
                .collect {
    
     favoriteNews ->
                    _uiState.value = LatestNewsUiState.Success(favoriteNews)
                }
        }
    }
}

For example, to convert flow from cold flow to hot flow use the function shareIn

public fun <T> Flow<T>.shareIn(
    scope: CoroutineScope,
    started: SharingStarted,
    replay: Int = 0
)

SharingStarted:

  1. WhileSubscribed starts sharing data after the first subscriber appears and keeps the data stream active forever
  2. Lazily will keep the upstream provider active while there are subscribers
  3. Eagerly Launch Provider Now

Use in Android

Many components in the Jetpack library released by Google come with KTX extension dependencies, which mainly add kotlin and coroutine support.

1:Lifecycle

Officially provides rapid creation implementation of life cycle coroutine scope

  1. Specify the life cycle to run the coroutine
  2. Automatically cancel the coroutine in onDestory

Introduce ktx dependency library

implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-rc03"

Run the coroutine when execution reaches a certain life cycle

fun launchWhenCreated(block: suspend CoroutineScope.() -> Unit): Job

fun launchWhenStarted(block: suspend CoroutineScope.() -> Unit): Job

fun launchWhenResumed(block: suspend CoroutineScope.() -> Unit): Job

suspend fun <T> Lifecycle.whenStateAtLeast(
    minState: Lifecycle.State,
    block: suspend CoroutineScope.() -> T
)

These functions all belong to the extension functions of Lifecycle and LifecycleOwner.

2:LiveData

Add dependencies:

implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-rc03"

Only these two functions are provided for developers to use. The functions of the two functions are the same, but the receiving time units of each parameter are different.

fun <T> liveData(
    context: CoroutineContext = EmptyCoroutineContext,
    timeoutInMs: Long = DEFAULT_TIMEOUT,
    @BuilderInference block: suspend LiveDataScope<T>.() -> Unit
): LiveData<T> = CoroutineLiveData(context, timeoutInMs, block)

fun <T> liveData(
    context: CoroutineContext = EmptyCoroutineContext,
    timeout: Duration,
    @BuilderInference block: suspend LiveDataScope<T>.() -> Unit
): LiveData<T> = CoroutineLiveData(context, timeout.toMillis(), block)

  1. timeout: If there is no active observer of liveData, its scope will be canceled within the specified time (unit milliseconds) [block]
  2. block: This scope will only be triggered in the active state. By default, it is in the Dispatchers.Main.immediate scheduler.

The liveData scope has the function of emitting data and LiveData

interface LiveDataScope<T> {
    
    
    /**
     * Set's the [LiveData]'s value to the given [value]; If you've called [emitSource] previously,
     * calling [emit] will remove that source.
     *
     * Note that this function suspends until the value is set on the [LiveData];
     *
     * @param value The new value for the [LiveData]
     *
     * @see emitSource
     */
    suspend fun emit(value: T)

    /**
     * Add the given [LiveData] as a source, similar to [MediatorLiveData.addSource]; Calling this
     * method will remove any source that was yielded before via [emitSource];
     *
     * @param source The [LiveData] instance whose values will be dispatched from the current
     * [LiveData];
     *
     * @see emit
     * @see MediatorLiveData.addSource
     * @see MediatorLiveData.removeSource
     */
    suspend fun emitSource(source: LiveData<T>): DisposableHandle

    /**
     * References the current value of the [LiveData];
     *
     * If the block never `emit`ed a value, [latestValue] will be `null`; You can use this
     * value to check what was then latest value `emit`ed by your `block` before it got cancelled.
     *
     * Note that if the block called [emitSource], then `latestValue` will be last value
     * dispatched by the `source` [LiveData];
     */
    val latestValue: T?
}

  1. It has no effect if emitSource is executed before emit
  2. This scope will be executed every time it is active. If the application is switched from the background to the foreground, the scope will be executed again, but the observer will only receive data when it is active.

Summarize

  • Coroutines are collaborative tasks, and threads are preemptive tasks. Essentially, both are concurrent. The core of coroutine is one word: scope. If you understand what scope is, you will understand coroutine.
  • Understanding the respective composition and functions of Coroutine/Channel/Flow will help you master and use coroutines flexibly.

Guess you like

Origin blog.csdn.net/ljx1400052550/article/details/134841846