一.Kotlin 协程启动方式总结

1.withContext 同步串行 带返回
2.launch 异步 不带返回
3.asyc 异步 带返回
4.runblocking 同步 不带返回

二.作用域函数 ,T的扩展函数 (with不是T的扩展函数)都是内联函数
在这里插入图片描述

Kotlin中的lateinit和by lazy有以下几个区别:

by lazy { … } 只能被用在被val修饰的变量上,而lateinit只能被用var修饰的变量上。
lateinit 不能用在可空的属性上和Java的基本类型上,但 by lazy { … } 可以。
lateinit 可以在任何位置初始化并且可以初始化多次,而 by lazy { … } 在第一次被调用时就被初始化,想要被改变只能重新定义。
lateinit 有支持(反向)域(Backing Fields),但 by lazy { … } 没有。

三.协程
‌Android 协程的原理是通过挂起和恢复让状态机状态流转实现把层层嵌套的回调代码变成像同步代码那样直观、简洁,从而避免回调地狱的出现。‌

协程在Android开发中作为一种异步编程的解决方案,通过挂起和恢复机制,使得异步代码的执行更加直观和简洁。具体来说,协程的本质是方法的挂起与恢复,它允许程序自行控制挂起和恢复的过程,从而实现多任务的协作执行。与传统的线程不同,协程在执行过程中可以暂停、恢复和取消,而不会阻塞线程,从而提供了一种更高效、更简洁的并发编程模型。

在Android平台上,协程通过Kotlin语言实现,利用suspend关键字定义挂起函数,这些函数可以在其中使用协程的相关特性。例如,使用delay函数可以模拟耗时操作,暂停协程的执行而不阻塞线程。协程的调度和执行依赖于CoroutineContext,它决定了协程将在哪个线程上执行任务。当挂起函数执行完毕后,协程会自动根据CoroutineContext切回原来的线程继续执行。

此外,Android协程的引入解决了在Android开发中常见的线程管理和同步问题,使得开发者能够更方便地处理并发任务。通过集成协程库,如kotlinx-coroutines-core和kotlinx-coroutines-android,开发者可以在Android应用中充分利用协程的优势,实现更高效的异步编程‌。

总结:
协程是一种解决方案,是一种解决嵌套, 并发、 弱化线程概念的方案。能让多个任务之间更好的协作,能够以同步的方式编排代码完成异步工作,将异步代码写的
像同步代码一样直观。
重点协程的本质是方法的挂起与恢复: return + callback

协程的作用域 :
1.顶级作用域
在 Android 中,某些 KTX 库为某些生命周期类提供自己的 CoroutineScope。主要有以下4种:
GlobeScope:全局范围,不会自动结束执行。
MainScope:主线程的作用域,全局范围
lifecycleScope:生命周期范围,用于activity等有生命周期的组件,在Desroyed的时候会自动结束。
viewModeScope:ViewModel范围,用于ViewModel中,在ViewModel被回收时会自动结束

所有的Scope都是 CoroutineScope 的子类。以上4种可以认为是最顶级的协程作用域,能在Activity、Fragment、ViewModel等类的 普通函数中 直接调用
2. coroutineScope & 3.supervisorScope
这两个就是2个挂起函数,分别表示协同作用域和主从作用域,因为是挂起函数所以也必须在协程块或挂起函数内调用:
private fun request() {
lifecycleScope.launch {
coroutineScope { // 协同作用域,抛出未捕获异常时会取消父协程
launch { }
}
supervisorScope { // 主从作用域,抛出未捕获异常时不会取消父协程
launch { }
}
}
}
二者的区别:
supervisorScope 表示主从作用域,会继承父协程的上下文,它的特点就是子协程的异常不会影响父协程,内部的 子协程挂掉 不会影响外部的父协程和兄弟协程的继续运行,它就像一道防火墙,隔离了异常,保证程序健壮,但是如果外部协程挂掉还是可以取消子协程的,即 单向传播。它的设计应用场景多用于 子协程为独立对等的任务实体的时候,比如一个下载器,每一个子协程都是一个下载任务,当一个下载任务异常时,它不应该影响其他的下载任务。
coroutineScope 表示 协同作用域, 内部的协程 出现异常 会向外部传播,子协程未捕获的异常会向上传递给父协程, 子协程 可以挂掉外部协程 , 外部协程挂掉也会挂掉子协程,即 双向传播 。 任何一个子协程异常退出,会导致整体的退出。

Kotlin中Any、Nothing、Unit 类型的概念和用法
Any类型
Any 是 Kotlin 类层次结构的根。每个 Kotlin 类都将 Any 作为其父类。这意味着 Any 是你在 Kotlin 中定义的任何类的父类,它与 Java 中的 Object 完全等效。

下面是 Any 类的定义:

public open class Any {
/**
* 判断某个对象是否与当前对象相等。
*/
public open operator fun equals(other: Any?): Boolean

/** 
 * 返回对象的哈希码值。hashCode 的一般约定是:
 * 
 */
public open fun hashCode(): Int

/** 
 * 返回对象的字符串表示。
 */
public open fun toString(): String

}
在上面的示例中,可以看到 Any 类提供了 equals、hashCode 和 toString 方法。这些方法可以在任何 Kotlin 类中使用,因为每个类都是 Any 类的子类。

Nothing 类型
在 Kotlin 中,可以使用 Nothing 类型表示“永远不存在的值”。例如,如果一个函数的返回类型为 Nothing,那么它意味着该函数永远不会返回(总是抛出异常)。

下面是 Nothing 类型的定义:

public class Nothing private constructor() {
// Nothing 类没有实例
}
上面的定义意味着你无法实例化这个类,也意味着没有其他类可以继承它。

以下是一个使用 Nothing 的函数示例,该函数总是抛出异常,永远不会返回任何值:

fun error(message: Any): Nothing {
throw IllegalStateException(message.toString())
}
上述函数始终抛出异常,不会返回任何值。需要注意的是,必须明确指定 Nothing 作为返回类型,否则返回类型仍然是 Unit。

Unit 类型
Unit 类型是只有一个值的类型,即 Unit 对象。这个类型对应于 Java 中的 void 类型。

以下是 Unit 类型的定义:

public object Unit {
override fun toString() = “kotlin.Unit”
}
在 Kotlin 中,如果你没有显式指定函数的返回类型,那么函数的默认返回类型是 Unit。这意味着如果函数的返回类型没有显式指定,Kotlin 编译器将使用 Unit 作为返回类型。

下面的示例展示了两个行为相似的函数:

fun display() {
print(“kotlin”)
}

fun display(): Unit {
print(“kotlin”)
}
在上面的示例中,这两个函数的行为是相似的。如果没有指定返回类型,Kotlin 编译器会将其默认设置为 Unit。

那么提问Kotlin中的void和Unit的区别是什么?
答:结构不同,void是一个java关键字,而Unit在kotlin中是一个真实的object,Java中要求函数必须有返回值,当什么都不返回时可以使用void,而kotlin中也可以返回Unit,从这个角度来说kotlin的Unit相当于java中的Void,但在kotlin中 默认都是返回Unit并且可以省略。

协程启动模式 :

DEFAULT 模式 : 默认的 协程启动模式 , 协程创建后 , 马上开始调度执行 , 如果在 执行前或执行时 取消协程 , 则进入 取消响应 状态 ;
1、DEFAULT 模式
DEFAULT 模式 : 默认的 协程启动模式 , 协程创建后 , 马上开始调度执行 , 如果在 执行前或执行时 取消协程 , 则进入 取消响应 状态 ; 如果在执行过程中取消 , 协程也会被取消 ;

代码示例 : DEFAULT 模式的协程可以被取消 ;

代码语言:javascript
复制
runBlocking {
// 调用 runBlocking 函数 , 可以将 主线程 包装成 协程

// 指定协程的启动模式为 CoroutineStart.DEFAULT
// 默认的 协程启动模式 , 协程创建后 , 马上开始调度执行 ,
// 如果在调度前取消协程 , 则进入 取消响应 状态 ;
val job = launch (start = CoroutineStart.DEFAULT) {
    Log.i(TAG, "协程开始执行")
    delay(2000)
    Log.i(TAG, "协程执行完毕")
}
delay(1000)
// 取消协程
job.cancel()

}
执行结果 :

代码语言:javascript
复制
00:44:46.329 I 协程开始执行
00:44:48.372 I 协程执行完毕
如果协程没有执行完 , 就取消协程任务 , 则协程会被中途取消 ;

协程是 基于线程 的 , 线程 由 调度器 控制 , 线程包含主线程和子线程 ;

上述代码中 , 调用 runBlocking 函数 , 可以将 主线程 包装成 协程 , launch 启动协程 , 该协程运行在主线程中 ,

运行到 delay(2000) 代码时 , 该 delay 函数是挂起函数 , 主线程会被挂起 , 主线程被调度器调度 , 执行其它的操作 如 刷新 UI 等操作 , 挂起函数中的内容会在子线程中执行 ,

如果 launch 启动协程时 , 此时会被调度器 立即调度 , 但是 主线程不会立即执行 , 如 主线程正在执行 刷新 UI 等任务 , 此时如果取消该协程 , 则协程直接取消 ;

如果在主线程中执行协程 , 协程挂起后 , 主线程继续执行其它任务, 如刷新 UI 等 , 主线程不会阻塞 , 挂起函数会在子线程中执行 ;

一般会将耗时操作放在 协程的挂起函数 中执行 ;

2、ATOMIC 模式
ATOMIC 模式 : 协程创建后 , 马上开始调度执行 , 协程执行到 第一个挂起点 之前 , 如果取消协程 , 则不进行响应取消操作 ;

代码示例 : 在下面的代码中 , 协程执行后 , 遇到的 第一个挂起函数是 delay(2000) 函数 , 该 挂起函数之前的代码执行过程中 , 如果取消协程 , 则该 协程不会取消 , 直到执行到 第一个挂起函数是 delay(2000) 函数 时 , 协程才会取消 ;

代码语言:javascript
复制
runBlocking {
// 调用 runBlocking 函数 , 可以将 主线程 包装成 协程
// 指定协程的启动模式为 CoroutineStart.ATOMIC
// 协程创建后 , 马上开始调度执行 ,
// 协程执行到 第一个挂起点 之前 , 如果取消协程 , 则不进行响应取消操作 ;
val job = launch (start = CoroutineStart.ATOMIC) {
Log.i(TAG, “协程开始执行”)
delay(2000)
Log.i(TAG, “协程执行完毕”)
}
// 取消协程
job.cancel()
}
3、LAZY 模式
ATOMIC 模式 : 协程创建后 , 不会马上开始调度执行 , 只有 主动调用协程的 start , join , await 方法 时 , 才开始调度执行协程 , 如果在 调度之前取消协程 , 该协程直接报异常 进入异常响应状态 ;

代码示例 : 在下面的代码中 , val job = async (start = CoroutineStart.LAZY) 只是定义协程 , 并不会马上执行 , 在执行 job.start() 或 job.await() 代码时 , 才开始调度执行协程 , 如果在这之前调用 job.cancel() 取消协程 , 则协程直接取消 ;

代码语言:javascript
复制
runBlocking {
// 调用 runBlocking 函数 , 可以将 主线程 包装成 协程
// 指定协程的启动模式为 CoroutineStart.LAZY
// 协程创建后 , 不会马上开始调度执行 ,
// 只有 主动调用协程的 start , join , await 方法 时 , 才开始调度执行协程 ,
// 如果在 调度之前取消协程 , 该协程直接报异常 进入异常响应状态 ;
val job = async (start = CoroutineStart.LAZY) {
Log.i(TAG, “协程开始执行”)
delay(2000)
Log.i(TAG, “协程执行完毕”)
“Hello” // 返回一个字符串
}
delay(1000)

// 取消协程 , 在调度之前取消 , 协程直接进入异常响应状态 
//job.cancel()

// 执行下面两个方法中的任意一个方法 ,
// 启动执行协程
job.start()
// 获取协程返回值
job.await()

}
4、UNDISPATCHED 模式
UNDISPATCHED 模式 : 协程创建后 , 立即在当前的 函数调用栈 执行协程任务 , 直到遇到第一个挂起函数 , 才在子线程中执行挂起函数 ;

如果在主线程中启动协程 , 则该模式的协程就会直接在主线程中执行 ;
如果在子线程中启动协程 , 则该模式的协程就会直接在子线程中执行 ;
代码示例 : Dispatchers.IO 调度器是将协程调度到子线程执行 , 但是如果 协程启动模式为 UNDISPATCHED , 则 立刻在当前的主线程中执行协程 , 协程创建后 , 立即在当前的 函数调用栈 执行协程任务 , 打印当前线程时 会打印主线程 ;

runBlocking {
// 调用 runBlocking 函数 , 可以将 主线程 包装成 协程
// 指定协程的启动模式为 CoroutineStart.UNDISPATCHED
// 协程创建后 , 立即在当前的 函数调用栈 执行协程任务 ,
// 直到遇到第一个挂起函数 , 才在子线程中执行挂起函数 ;
val job = async ( context = Dispatchers.IO, start = CoroutineStart.UNDISPATCHED ) {
// Dispatchers.IO 调度器是将协程调度到子线程执行
// 但是如果 协程启动模式为 UNDISPATCHED , 则立刻在当前的主线程中执行协程
// 此时打印出主线程 ,
// 协程创建后 , 立即在当前的 函数调用栈 执行协程任务 , 因此会打印主线程
Log.i(TAG, “协程开始执行 当前线程 : ${Thread.currentThread()}”)
// 遇到挂起函数会在子线程执行该挂起函数
// 挂起函数都是耗时任务
delay(2000)
Log.i(TAG, “协程执行完毕”)
“Hello” // 返回一个字符串
}
}

‌挂起函数的工作原理‌

挂起函数的工作原理主要依赖于Kotlin的协程机制。挂起函数内部通常会调用一些可能阻塞的操作,如delay函数,这个函数会让协程暂停执行一段时间。在暂停期间,协程会释放CPU资源,等待指定的时间过去后再恢复执行。这种机制允许协程在等待时不会占用CPU时间,从而提高了程序的效率。

在介绍flow具体用法之前,先说明下flow的一些概念:
Flow组成

Producers(生产者):数据流的产生emit
Customers(消费者):数据流的收集collect
Operators(中间操作符):数据流的二次加工

flow的冷流&热流
在 Kotlin 的协程中,“冷流”(Cold Flow)和"热流"(Hot Flow)是用来描述 Flow 和 SharedFlow 两种不同的数据流的特性,还有一种特别的热流,StateFlow,它继承自SharedFlow
kotlin 代码解读复制代码public interface StateFlow : SharedFlow {
/**
* The current value of this state flow.
*/
public val value: T
}

cold flow & hot flow区别

冷流(Cold Flow):

冷流是指每次订阅都会重新开始并独立运行的数据流。
当每个订阅者开始收集数据时,冷流会从头开始发射数据,每个订阅者都会独立地接收到完整的数据流。
例如,通过调用 Flow 的 collect 或 collectLatest 函数,可以订阅冷流并收集数据。

热流(Hot Flow):

热流是指已经开始发射数据并在订阅之前运行的数据流。
热流在启动时就开始发射数据,无论是否有订阅者。
如果订阅者在流已经开始发射数据后加入,它们可能会错过一些数据。
例如,通过调用 SharedFlow 的 asSharedFlow 函数,可以创建热流,并可以通过 collect 函数订阅。

Flow使用
class SecondFragment : Fragment() {

//……省略无关代码

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    binding.buttonFlow.setOnClickListener {
        lifecycleScope.launch {
            val value = createFlow().first()
            Log.d("flow", "flow.first() = $value")

            val acc = createFlow().fold(0) { acc, item ->
                acc + item
            }
            Log.d("flow", "flow.fold() = $acc")

            try {
                val value = createFlow().single()
                Log.d("flow", "flow.single() = $value")
            } catch (e: Exception) {
                Log.d("flow", e.toString())
            }
        }
    }

    binding.collectLastBtn.setOnClickListener {
        lifecycleScope.launch {
            createFlow().collectLatest { value ->
                println("Collecting $value")
                delay(1000) // Emulate work
                println("$value collected")
            }
        }
    }

}

private fun createFlow(): Flow<Int> {
    return flow {
        emit(100)
        delay(500)
        emit(200)
        emit(300)
    }
}

}

flow创建
创建一个普通flow很简单,直接如上所述方法createFlow(),直接调用flow{},代码块中使用emit(value)发射数据;另外还有一些其他方式创建flow,例如T.asFlow()和flowOf(value: T)等方法,本质都是调用了flow{},具体使用细节看后续Demo;
public fun Iterable.asFlow(): Flow = flow {
forEach { value ->
emit(value)
}
}
flow的常用操作符
first
顾名思义获取到flow数据流中的第一个元素,与之对应的是last()
single
上述例子中有这样一段code:

try {
    val value = createFlow().single()
    Log.d("flow", "flow.single() = $value")
} catch (e: Exception) {
    Log.d("flow", e.toString())
}

这里的single()操作符作用如下:

获取单个元素:single() 操作符用于获取 Flow 中的单个元素。如果 Flow 中只包含一个元素,它将返回该元素;如果 Flow 中包含多个元素或没有元素,它将抛出 IllegalArgumentException 异常。
用于确保 Flow 只包含一个元素:single() 可以用作 Flow 的检查机制,确保 Flow 中只包含一个元素。如果 Flow 中的元素数量不符合预期,single() 将抛出异常,提供了一种简单的验证和安全性检查。
简化处理单个元素的情况:当你只关心 Flow 中的单个元素,并希望在处理该元素时终止流的收集时,可以使用 single()。它能够简化对单个元素的处理逻辑。
distinctUntilChanged
数据去重

createFlow().distinctUntilChanged().collectLatest {
    println("emit value = $it")
}

StateFlow
创建
stateFlow初始化的时候必须要有一个初始值

public fun <T> MutableStateFlow(value: T): MutableStateFlow<T> = StateFlowImpl(value ?: NULL)

用法也很简单,几乎和LiveData一样,都有一个value属性,赋值都是给value赋值

private val _stateFlow = MutableStateFlow("Hello world")
val stateFlow: StateFlow<String> = _stateFlow.asStateFlow()

fun triggerStateFlow() {
    _stateFlow.value = "StateFlow"
}

使用

lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        launch {
            viewModel.stateFlow.collectLatest {
                binding.stateText.text = it
                Snackbar.make(
                    binding.root,
                    it,
                    Snackbar.LENGTH_LONG
                ).show()
            }
        }
     }
 }

每次给stateflow.value赋值,都会触发collect方法,类似livedata.observe(this), 只不过collect是协程挂起函数,需要在Coroutine.Scope中执行代码块。
上述代码如果使用下面的写法则不安全:
lifecycleScope.launch {
viewModel.triggerFlow().collectLatest {
binding.flowText.text = it
}
}
当app进入后台的时候,生命周期函数是走到onStop,但是此刻flow所在的协程还是处在活跃状态,可以正常收集数据,这就造成了数据的浪费,甚至产生内存泄漏现象,当我们使用repeatOnLifecycle(Lifecycle.State.STARTED)的时候,当app进入后台的时候我们的协程挂起函数会处于挂起状态,此时会停止收集flow;重新进入前台后,挂起函数会重新运行;
粘性数据(数据倒灌)
当屏幕翻转或跳转返回,或者弹Dialog的时候,stateFlow会发生数据倒灌,stateflow的value会重新发送给消费者,触发collect代码块;
这与LiveData是一致的,后面ShareFlow会讲到如何避免这种情况!
SharedFlow代替使用就不会数据倒灌。
之前我们可能会在第一个接口请求成功的回调里继续调用第二个接口,以此类推,这样虽然能实现,但是会导致回调层级很深,也就是所谓的回调地狱;此时可以使用Flow的flatMapConcat将多个接口串联起来。

四. java中的按位或| 在kotlin中用or代替,并且只能在一行,不能换行。

五.kotlin list 转换成map
方法 适用场景 是否推荐
list.map { k to v }.toMap() 已有 Pair 数据 或 小数据量 ⚠️ 一般情况不推荐
associate() 一次性生成新 Map,需转换 value ✅ 推荐
associateBy() 用某个字段作 key,value 为原对象 ✅ 强烈推荐
associateTo() / associateByTo() 需要复用 map 容器 ✅ 特定场景推荐

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐