共计 14735 个字符,预计需要花费 37 分钟才能阅读完成。
1. Kotlin Coroutines 简介
在过来几年间,协程这个概念发展势头迅猛,到当初曾经被诸多支流编程语言采纳,例如:Go
、Python
等都能够在语言层面上实现协程,甚至是 Java
也能够通过应用扩大库来间接地反对协程。今日配角 Kotlin
也紧跟步调,在 1.3 版本中增加了对协程的反对。
Kotlin Coroutines
是 Kotlin
提供的一套线程解决框架。开发者能够应用 Kotlin Coroutines
简化异步代码,使得不同线程的代码能够在同一代码块中执行,使代码显得更加线性,浏览起来更自若。
然而 协程 Coroutines
并不是 Kotlin
提出来的新概念,其源自 Simula
和 Modula-2
语言,这个术语早在 1958 年就被 Melvin Edward Conway 创造并用于构建汇编程序,这阐明了协程是一种编程思维,并不局限于特定的语言。
Kotlin Coroutines
为 Android
开发者解决了以下痛点
- 主线程平安问题
- 回调天堂「Callback Hell」
上面介绍一个简略的例子来看看协程能有多简洁。
fun test() {
thread {
// 子线程做网络申请
request1()
runOnUiThread {
// 切换到主线程更新 UI
Log.d("tag", "request1")
thread {
// 子线程做网络申请
request2()
runOnUiThread {
// 切换到主线程更新 UI
Log.d("tag", "request2")
}
}
}
}
}
private fun request1() = Unit
private fun request2() = Unit
能够看到,当要解决多个申请时,会呈现多层嵌套(多层回调)的问题,代码的易读性会很差,反观以下应用协程的代码便会清晰很多。
fun test2() {val coroutineScope = CoroutineScope(Dispatchers.Main)
coroutineScope.launch {val request1 = withContext(Dispatchers.IO) {
// 子线程做网络申请
request1()}
// 切换到主线程更新 UI
Log.d("tag", request1)
val request2 = withContext(Dispatchers.IO) {
// 子线程做网络申请
request2()}
// 切换到主线程更新 UI
Log.d("tag", request2)
}
}
suspend fun request1(): String {
// 提早 2s 模仿一次网络申请
delay(2000)
return "request1"
}
suspend fun request2(): String {
// 提早 1s 模仿一次网络申请
delay(1000)
return "request2"
}
2. 小试牛刀
上面咱们开始应用协程,首先应用 Android Studio
创立一个 Kotlin
我的项目,而后在 build.gradle
增加以下配置
buildscript {
repositories {google()
mavenCentral()}
dependencies {
classpath "com.android.tools.build:gradle:7.0.3"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.20"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
而后在 app 模块的 build.gradle
增加以下依赖
dependencies {
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0'
// ...
// 协程外围库
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0"
// 协程 Android 反对库
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0"
}
这样一来咱们就能应用协程了,接下来咱们从如何创立一个协程说起。
2.1 创立一个协程
咱们能够通过以下 3 种办法来启动一个协程
runBlocking
会阻断以后线程,直到闭包中语句执行实现,因而不会在业务开发场景应用,个别用于 main 函数以及单元测试中。CoroutineScope.launch
实用于执行一些不须要返回后果的工作。CoroutineScope.async
实用于执行须要返回后果的工作。会在下一大节讲到能够应用await
挂起函数来拿到返回后果。- 举荐应用
CoroutineContext
构建CoroutineScope
,来创立协程,因为这样能更好的管制和治理协程的生命周期。前面原理局部会专门讲这两局部。
/**
* 启动协程的三种形式
*/
fun startCoroutine() {
// 通过 runBlocking 启动一个协程
// 它会中断以后线程直到 runBlocking 闭包中的语句执行实现
runBlocking {fetchDoc()
}
// 通过 CoroutineScope.launch 启动一个协程
val coroutineScope = CoroutineScope(Dispatchers.IO)
coroutineScope.launch {fetchDoc()
}
// 通过 CoroutineScope.async 启动一个协程
val coroutineScope2 = CoroutineScope(Dispatchers.Default)
coroutineScope2.async {fetchDoc()
}
}
2.2 线程切换操作
在 Kotlin Coroutines
中次要是应用 调度器 来控制线程的切换。在创立协程时能够传入指定的调度模式来决定协程体 block
在哪个线程中执行。
// 在后盾线程中执行操作
someScope.launch(Dispatchers.Default) {// ...}
下面协程的代码块,会被散发到由协程所治理的线程池中执行。
上例中的线程池属于 Dispatchers.Default
。在将来的某一时间,该代码块会被线程池中的某个线程执行,具体执行工夫取决于线程池的策略。
除了下面例子中的Dispatchers.Default
调度器,还有以下两种调度器
Dispatchers.IO
该调度器下的代码块会在 IO 线程中执行,次要解决网络申请,以及 IO 操作。Dispatchers.Main
该调度器下的代码块会在主线程执行,次要是做更新 UI 操作。
class MainActivity : AppCompatActivity() {private val mainScope = MainScope()
override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
startLaunch()}
private fun startLaunch() {
// 创立一个默认参数的协程,其默认的调度模式为 Main 也就是说该协程的线程环境是主线程
val job1 = mainScope.launch {
// delay 是一个挂起函数
Log.d("startLaunch", "Before Delay")
delay(1000)
Log.d("startLaunch", "After Delay")
}
val job2 = mainScope.launch(Dispatchers.IO) {
// 以后线程环境是 IO 线程
Log.d("startLaunch", "CurrentThread + ${Thread.currentThread()}")
withContext(Dispatchers.Main) {
// 以后线程环境是主线程
Log.d("startLaunch", "CurrentThread + ${Thread.currentThread()}")
}
}
mainScope.launch {
// 执行实现后能够有返回值
val userInfo = getUserInfo()
Log.d("startLaunch", "CurrentThread + ${Thread.currentThread()}")
}
}
// withContext 是一个挂起函数, 能够挂起以后协程(能够传入新的 Context)private suspend fun getUserInfo() = withContext(Dispatchers.IO) {delay(2000)
"Hello World"
}
}
2.3 解决并发操作
Kotlin Coroutines
通过 async
关键字来做并发操作,通常配合 await
办法或者 awaitAll
扩大办法来应用来应用。
class MainActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
MainScope().launch {startAsync()
}
}
private suspend fun startAsync() {val coroutineScope = CoroutineScope(Dispatchers.IO)
// async 会返回 deferred 对象 能够通过 await() 返回值
val avatarJob = coroutineScope.async {fetchAvatar() }
val nameJob = coroutineScope.async {fetchName() }
// 也能够 Collections 的 awaitAll() 扩大办法返回 返回值的汇合
val listOf = listOf(avatarJob, nameJob)
val startTime = System.currentTimeMillis()
val awaitAll = listOf.awaitAll()
val endTime = System.currentTimeMillis()
Log.d("startAsync", "用的工夫 ${endTime - startTime}ms")
Log.d("startAsync", awaitAll.toString())
}
private suspend fun fetchAvatar(): String {delay(2000)
return "头像"
}
private suspend fun fetchName(): String {delay(2000)
return "昵称"
}
}
下面的代码就是一个简略的并发示例 fetchAvatar
提早了 2000ms,fetchName
提早 1000 ms 实现,这里应用了 awaitAll()
办法返回后果的汇合,它会等 fetchAvatar
实现后,也就是 2000 ms 后返回后果。
3. 了解 Kotlin Coroutines 挂起函数
在上述例子中呈现了很屡次 suspend
关键字,它是 Kotlin Coroutines
的一个关键字,咱们将用 suspend
关键字润饰的函数称之为挂起函数。例如 withContext
、delay
这些都属于挂起函数。
3.1 什么是挂起
那么挂起到底是什么意思,会阻塞以后线程吗?咱们看一个日常的例子,咱们向服务端申请用户信息,这个过程是耗时的因而要在 IO 线程获取用户信息,1 通过 withContext
挂起并切换到 IO 线程申请用户信息,2 回到 UI 线程更新 UI。此时会阻塞主线程吗,答案是当然不会,不然页面早就卡死了。
class UserViewModel: ViewModel() {
// 申请用户信息后刷新 UI
fun fetchUserInfo() {
// 启动一个上下文是 UI 线程的
viewModelScope.launch(Dispatchers.Main) {
// 1 挂起申请用户信息
val userInfo = withContext(Dispatchers.IO) {UserResp.fetchUserInfo()
}
// 2 更新 UI
Log.d("fetchUserInfo", userInfo)
}
}
咱们从两个以后线程和挂起的协程两个角色来了解挂起。
线程
其实当线程执行到协程的 suspend 函数的时候,临时不继续执行协程代码了。
那线程接下来会做什么呢?
如果它是一个后盾线程:
- 要么无事可做,被零碎回收
- 要么继续执行别的后台任务
咱们上述例子中是在协程上下文是在主线程,因而主线程会持续去做工作,也就是刷新界面的工作。
协程
协程此时就从以后线程挂起了,如果其上下文是其余线程,那么协程就会在其余线程无限度的去运行。等到工作运行实现后在切换回主线程
咱们上述例子是在 IO 线程,因而会切换到 IO 线程去申请用户的信息。
3.2 suspend 关键字有何作用
上述 suspend 润饰的函数都有挂起 / 切换线程的性能。
那是不是任何用 suspend
关键字都会有这样的个性?
答案是否定的,来看以下办法只做了打印一段文字,并没有切到某处,又切回来。所以 supend
关键字并不启到协程挂起 / 切换线程的作用
suspend fun test() {println("我是挂起函数")
}
那么 suspend
关键字到底有什么用呢?
答案是揭示”函数调用方“,被 suspend
关键字润饰的是一个耗时的函数,须要在协程中能力应用。
4. 了解 Kotlin Coroutines 几个外围概念
4.1 CoroutineContext – 上下文
CoroutineContext
即协程的上下文,次要承载了资源获取,配置管理等工作,是执行环境相干的通用数据资源的对立提供者。应用协程中运行协程的上下文是极其重要的,这样才能够实现正确的线程行为、生命周期。
CoroutineContext
蕴含用户定义的一些数据汇合,这些数据与协程密切相关。它是一个有索引的 Element
实例汇合。这个有索引的汇合相似于一个介于 Set
和 Map
之间的数据结构。每个 element
在这个汇合有一个惟一的 Key
与之对应。对于雷同 Key
的 Element
是不能够反复存在的。
Element
之间能够通过 + 号组合起来,Element
有几个子类,CoroutineContext
也次要由这几个子类组成:
Job
:协程的惟一标识,管制协程的生命周期。CoroutineDispatche
:指定协程运行的线程。CoroutineName
:协程的名称,默认为 coroutine,个别在调试的时候应用。CoroutineExceptionHandler
:指协程的异样处理器,用于解决未被捕获的异样。
CoroutineContext
接口的定义如下:
public interface CoroutineContext {// 操作符 [] 重载,能够通过 CoroutineContext[Key]这种模式来获取与 Key 关联的 Element
public operator fun <E : Element> get(key: Key<E>): E?
// 它是一个汇集函数,提供了从 left 到 right 遍历 CoroutineContext 中每一个 Element 的能力,并对每一个 Element 做 operation 操作
public fun <R> fold(initial: R, operation: (R, Element) -> R): R
// 操作符 + 重载,能够 CoroutineContext + CoroutineContext 这种模式把两个 CoroutineContext 合并成一个
public operator fun plus(context: CoroutineContext): CoroutineContext
// 返回一个新的 CoroutineContext,这个 CoroutineContext 删除了 Key 对应的 Element
public fun minusKey(key: Key<*>): CoroutineContext
// Key 定义,空实现,仅仅做一个标识
public interface Key<E : Element>
// Element 定义,每个 Element 都是一个 CoroutineContext
public interface Element : CoroutineContext {
// 每个 Element 都有一个 Key 实例
public val key: Key<*>
//...
}
}
通过接口定义能够发现 CoroutineContext
几个特点
- 重写了 get 操作符,因而能够像拜访 map 中的元素一样应用
CoroutineContext[key]
这种中括号的模式来拜访。 - 重写了 plus 操作符,因而能够应用 + 号连贯不同的
CoroutineContext
。
通过查看源码能够发现 CoroutineContext
次要被 CombinedContext
、Element
、EmptyCoroutineContext
所实现。
Element
可能会比拟奇怪 “ 为什么元素自身也是汇合 ”。起因是次要是设计 API 不便,示意Element
外部只寄存 Element
。
EmptyCoroutineContext
是 CoroutineContext
的空实现,不持有任何元素。
public operator fun plus(context: CoroutineContext): CoroutineContext =
// 如果要相加的 CoroutineContext 为空,那么不做任何解决,间接返回
if (context === EmptyCoroutineContext) this else // fast path -- avoid lambda creation
// 如果要相加的 CoroutineContext 不为空,那么对它进行 fold 操作
context.fold(this) { acc, element -> // 咱们能够把 acc 了解成 + 号右边的 CoroutineContext,element 了解成 + 号左边的 CoroutineContext 的某一个 element
// 首先从右边 CoroutineContext 中删除左边的这个 element
val removed = acc.minusKey(element.key)
// 如果 removed 为空,阐明右边 CoroutineContext 删除了和 element 雷同的元素后为空,那么返回左边的 element 即可
if (removed === EmptyCoroutineContext) element else {
// 确保 interceptor 始终在汇合的开端
val interceptor = removed[ContinuationInterceptor]
if (interceptor == null) CombinedContext(removed, element) else {val left = removed.minusKey(ContinuationInterceptor)
if (left === EmptyCoroutineContext) CombinedContext(element, interceptor) else
CombinedContext(CombinedContext(left, element), interceptor)
}
}
}
因为 CoroutineContext
是由一组元素组成的,所以加号右侧的元素会笼罩加号左侧的元素,进而组成新创建的 CoroutineContext
。比方 (Dispatchers.Main, "name") + (Dispatchers.IO) = (Dispatchers.IO, "name")
。
4.2 Job & Deferred – 工作
4.2.1 Job
Job
用于解决协程。对于每一个所创立的协程(通过 launch
或者 async
),它会返回一个 Job
实例,该实例是协程的惟一标识,并且负责管理协程的生命周期。
除了应用 launch
或者 async
创立 Job
外,还能够通过上面 Job
构造方法创立 Job
。
public fun Job(parent: Job? = null): Job = JobImpl(parent)
这个很好了解,当传入 parent 时,此时的 Job
将会作为 parent 的子 Job
。
既然 Job
是来治理协程的,那么它提供了六种状态来示意协程的运行状态。见官网表格
State | [isActive] | [isComplete] | isCancelled |
---|---|---|---|
New (optional initial state) | false |
false |
false |
Active (default initial state) | true |
false |
false |
Completing (transient state) | true |
false |
false |
Cancelling (transient state) | false |
false |
true |
Cancelled (final state) | false |
true |
true |
Completed (final state) | false |
true |
false |
尽管咱们获取不到协程具体的运行状态,然而能够通过 isActive
,isCompleted
、isCancelled
来获取以后协程是否处于三种状态。
咱们能够通过下图能够大略理解下一个协程作业从创立到实现或者勾销。
wait children
+-----+ start +--------+ complete +-------------+ finish +-----------+
| New | -----> | Active | ---------> | Completing | -------> | Completed |
+-----+ +--------+ +-------------+ +-----------+
| cancel / fail |
| +----------------+
| |
V V
+------------+ finish +-----------+
| Cancelling | --------------------------------> | Cancelled |
+------------+ +-----------+
4.2.2 Deferred
public interface Deferred<out T> : Job {
public val onAwait: SelectClause1<T>
public suspend fun await(): T
@ExperimentalCoroutinesApi
public fun getCompleted(): T
@ExperimentalCoroutinesApi
public fun getCompletionExceptionOrNull(): Throwable?}
通过应用 async
创立协程能够失去一个有返回值 Deferred
,Deferred
接口继承自 Job
接口,额定提供了 await
办法来获取 Coroutine
的返回后果。因为 Deferred
继承自 Job
接口,所以 Job
相干的内容在 Deferred
上也是实用的。
4.3 CoroutineDispatcher – 调度器
调度器是什么呢?Kotlin 官网是这么给出解释的
调度器它确定了相干的协程在哪个线程或哪些线程上执行。协程调度器能够将协程限度在一个特定的线程执行,或将它分派到一个线程池,亦或是让它不受限地运行。
Dispatchers
是一个规范库中帮咱们封装了切换线程的帮忙类,能够简略了解为一个线程池。
public actual object Dispatchers {
@JvmStatic
public actual val Default: CoroutineDispatcher = createDefaultDispatcher()
@JvmStatic
public actual val Main: MainCoroutineDispatcher
get() = MainDispatcherLoader.dispatcher
@JvmStatic
public actual val Unconfined: CoroutineDispatcher = kotlinx.coroutines.Unconfined
@JvmStatic
public val IO: CoroutineDispatcher = DefaultScheduler.IO
}
-
Dispatchers.Default
默认的调度器,适宜解决后盾计算,是一个
CPU
密集型任务调度器。如果创立Coroutine
的时候没有指定dispatcher
,则个别默认应用这个作为默认值。Default dispatcher
应用一个共享的后盾线程池来运行外面的工作。留神它和IO
共享线程池,只不过限度了最大并发数不同。 -
Dispatchers.IO
顾名思义这是用来执行阻塞
IO
操作的,是和Default
共用一个共享的线程池来执行外面的工作。依据同时运行的工作数量,在须要的时候会创立额定的线程,当工作执行结束后会开释不须要的线程。 -
Dispatchers.Unconfined
因为
Dispatchers.Unconfined
未定义线程池,所以执行的时候默认在启动线程。遇到第一个挂终点,之后由调用resume
的线程决定复原协程的线程。 -
Dispatchers.Main:
指定执行的线程是主线程,在
Android
上就是UI
线程。
4.4 CoroutineStart – 启动器
CoroutineStart
协程启动模式,是启动协程时须要传入的第二个参数。协程启动模式有 4 种:
-
CoroutineStart.DEFAULT
默认启动模式,咱们能够称之为饿汉启动模式,因为协程创立后立刻开始调度,尽管是立刻调度,但不是立刻执行,也有可能在执行前被勾销。
-
CoroutineStart.LAZY
懒汉启动模式,启动后并不会有任何调度行为,直到咱们须要它执行的时候才会产生调度。也就是说只有咱们被动的调用
Job
的start
、join
或者await
等函数时才会开始调度。 -
CoroutineStart.ATOMIC
ATOMIC
一样也是在协程创立后立刻开始调度,然而它和DEFAULT
模式有一点不一样,通过ATOMIC
模式启动的协程执行到第一个挂终点之前是不响应cancel
勾销操作的,ATOMIC
肯定要波及到协程挂起后cancel
勾销操作的时候才有意义。 -
CoroutineStart.UNDISPATCHED:
协程在这种模式下会间接开始在以后线程下执行,直到运行到第一个挂终点。
4.5 CoroutineScope – 协程的作用域
启动一个协程必须指定其 CoroutineScope
。CoroutineScope
能够对协程进行追踪,即便协程被挂起也是如此。同调度程序 Dispatcher
不同,CoroutineScope
并不运行协程,它只是确保您不会失去对协程的追踪。
通过 CoroutineScope
能够勾销协程中的工作,在 Android 中通常咱们会在页面启动的时候做一下耗时操作,在页面敞开时这些耗时工作就没有意义了,此时 Activity
或 Fragment
就能够通过 lifecycleScope
来启动协程。
为了明确父子协程之间的关系以及协程异样流传状况,官网将协程的作用域分为以下三类
-
顶级作用域
没有父协程的协程所在的作用域为顶级作用域。
-
协同作用域
协程中启动新的协程,新协程为所在协程的子协程,这种状况下,子协程所在的作用域默认为协同作用域。此时子协程抛出的未捕捉异样,都将传递给父协程解决,父协程同时也会被勾销。
-
主从作用域
与协同作用域在协程的父子关系上统一,区别在于,处于该作用域下的协程呈现未捕捉的异样时,不会将异样向上传递给父协程。
除了三种作用域中提到的行为以外,父子协程之间还存在以下规定:
父协程被勾销,则所有子协程均被勾销。因为协同作用域和主从作用域中都存在父子协程关系,因而此条规定都实用。父协程须要期待子协程执行结束之后才会最终进入实现状态,不论父协程本身的协程体是否曾经执行完。子协程会继承父协程的协程上下文中的元素,如果本身有雷同 key
的成员,则笼罩对应的 key
,笼罩的成果仅限本身范畴内无效。
5. Android 中应用协程的几个🌰
留神:以下实例代码都是在 ViewModel
中,因而能够应用 viewModelScope
5.1 网络申请
目前 Retrofit
官网曾经对 Kotlin Coroutines
做了反对。
interface UserApi {@GET("url")
suspend fun getUsers(): List<UserEntity>}
定义实现接口后,用协程来实现一个最根本的申请。
private fun fetchUsers() {
viewModelScope.launch {users.postValue(Resource.loading(null))
try {val usersFromApi = apiHelper.getUsers()
users.postValue(Resource.success(usersFromApi))
} catch (e: Exception) {users.postValue(Resource.error(e.toString(), null))
}
}
}
也能够通过 Kotlin Coroutines
实现多个接口同时申请,拿到两个接口数据后刷新 UI。
private fun fetchUsers() {
viewModelScope.launch {users.postValue(Resource.loading(null))
try {
// coroutineScope is needed, else in case of any network error, it will crash
coroutineScope {val usersFromApiDeferred = async { apiHelper.getUsers() }
val moreUsersFromApiDeferred = async {apiHelper.getMoreUsers() }
val usersFromApi = usersFromApiDeferred.await()
val moreUsersFromApi = moreUsersFromApiDeferred.await()
val allUsersFromApi = mutableListOf<ApiUser>()
allUsersFromApi.addAll(usersFromApi)
allUsersFromApi.addAll(moreUsersFromApi)
users.postValue(Resource.success(allUsersFromApi))
}
} catch (e: Exception) {users.postValue(Resource.error("Something Went Wrong", null))
}
}
}
5.2 操作 Room 数据库
作为 JetPack
中的一员,Room
对 Kotlin Coroutines
做了较好的反对。代码如下
@Dao
interface UserDao {@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertUsers(vararg users: User)
@Update
suspend fun updateUsers(vararg users: User)
@Delete
suspend fun deleteUsers(vararg users: User)
@Query("SELECT * FROM user WHERE id = :id")
suspend fun loadUserById(id: Int): User
@Query("SELECT * from user WHERE region IN (:regions)")
suspend fun loadUsersByRegion(regions: List<String>): List<User>
}
5.3 做一些耗时操作
Kotlin Coroutines
也能够做一些耗时的操作,比方 IO 读写,列表的排序等等。这里用 delay
来模仿耗时工作。
fun startLongRunningTask() {
viewModelScope.launch {status.postValue(Resource.loading(null))
try {
// do a long running task
doLongRunningTask()
status.postValue(Resource.success("Task Completed"))
} catch (e: Exception) {status.postValue(Resource.error("Something Went Wrong", null))
}
}
}
private suspend fun doLongRunningTask() {withContext(Dispatchers.Default) {
// your code for doing a long running task
// Added delay to simulate
delay(5000)
}
}
5.4 给耗时工作设置一个超时工夫
能够通过 withTimeout
给一个协程减少超时工夫,当工作超过这段时间就会抛出 TimeoutCancellationException
。
private fun fetchUsers() {
viewModelScope.launch {users.postValue(Resource.loading(null))
try {withTimeout(100) {val usersFromApi = apiHelper.getUsers()
users.postValue(Resource.success(usersFromApi))
}
} catch (e: TimeoutCancellationException) {users.postValue(Resource.error("TimeoutCancellationException", null))
} catch (e: Exception) {users.postValue(Resource.error("Something Went Wrong", null))
}
}
}
5.5 全局异样的解决
能够自定义 CoroutineExceptionHandler
,来解决一些未被拦挡的异样。
private val exceptionHandler = CoroutineExceptionHandler { _, exception ->
users.postValue(Resource.error("Something Went Wrong", null))
}
private fun fetchUsers() {viewModelScope.launch(exceptionHandler) {users.postValue(Resource.loading(null))
val usersFromApi = apiHelper.getUsers()
users.postValue(Resource.success(usersFromApi))
}
}
5.6 用 Kotlin Flow 来实现倒计时
在 Activity
或 Fragment
中通过 Flow
启动一个倒计时,每隔 1 s 更新一次 UI 状态
lifecycleScope.launch {(59 downTo 0).asFlow()
.onEach {delay(1000) }
.flowOn(Dispatchers.Default)
.onStart {Logger.d("计时器开始")
}.collect { remain ->
Logger.d("计时器残余 $remain 秒")
}
}
6. 小结
通过这篇文章带大家回顾了一下「协程是什么」、「如何应用协程」、「对挂起的了解」以及「协程在 Android
中的利用」。其实协程正如 Kotlin
这门年老的语言一样,一直再优化,也一直减少新的性能,例如 Flow
,Channel
等等。如果大家有更好的意见欢送留言评论。
本文由博客一文多发平台 OpenWrite 公布!