乐趣区

关于前端:深入浅出Kotlin

1. Kotlin Coroutines 简介

在过来几年间,协程这个概念发展势头迅猛,到当初曾经被诸多支流编程语言采纳,例如:GoPython 等都能够在语言层面上实现协程,甚至是 Java 也能够通过应用扩大库来间接地反对协程。今日配角 Kotlin 也紧跟步调,在 1.3 版本中增加了对协程的反对。

Kotlin CoroutinesKotlin 提供的一套线程解决框架。开发者能够应用 Kotlin Coroutines 简化异步代码,使得不同线程的代码能够在同一代码块中执行,使代码显得更加线性,浏览起来更自若。

然而 协程 Coroutines并不是 Kotlin 提出来的新概念,其源自 SimulaModula-2 语言,这个术语早在 1958 年就被 Melvin Edward Conway 创造并用于构建汇编程序,这阐明了协程是一种编程思维,并不局限于特定的语言。

Kotlin CoroutinesAndroid 开发者解决了以下痛点

  • 主线程平安问题
  • 回调天堂「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 关键字润饰的函数称之为挂起函数。例如 withContextdelay 这些都属于挂起函数。

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 实例汇合。这个有索引的汇合相似于一个介于 SetMap 之间的数据结构。每个 element 在这个汇合有一个惟一的 Key 与之对应。对于雷同 KeyElement 是不能够反复存在的。

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 次要被 CombinedContextElementEmptyCoroutineContext 所实现。

Element 可能会比拟奇怪 “ 为什么元素自身也是汇合 ”。起因是次要是设计 API 不便,示意Element 外部只寄存 Element

EmptyCoroutineContextCoroutineContext 的空实现,不持有任何元素。

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

尽管咱们获取不到协程具体的运行状态,然而能够通过 isActiveisCompletedisCancelled 来获取以后协程是否处于三种状态。

咱们能够通过下图能够大略理解下一个协程作业从创立到实现或者勾销。

                                                                            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 创立协程能够失去一个有返回值 DeferredDeferred 接口继承自 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

    懒汉启动模式,启动后并不会有任何调度行为,直到咱们须要它执行的时候才会产生调度。也就是说只有咱们被动的调用 Jobstartjoin 或者 await 等函数时才会开始调度。

  • CoroutineStart.ATOMIC

    ATOMIC 一样也是在协程创立后立刻开始调度,然而它和 DEFAULT 模式有一点不一样,通过 ATOMIC 模式启动的协程执行到第一个挂终点之前是不响应 cancel 勾销操作的,ATOMIC 肯定要波及到协程挂起后 cancel 勾销操作的时候才有意义。

  • CoroutineStart.UNDISPATCHED:

    协程在这种模式下会间接开始在以后线程下执行,直到运行到第一个挂终点。

4.5 CoroutineScope – 协程的作用域

启动一个协程必须指定其 CoroutineScopeCoroutineScope 能够对协程进行追踪,即便协程被挂起也是如此。同调度程序 Dispatcher 不同,CoroutineScope 并不运行协程,它只是确保您不会失去对协程的追踪。

通过 CoroutineScope 能够勾销协程中的工作,在 Android 中通常咱们会在页面启动的时候做一下耗时操作,在页面敞开时这些耗时工作就没有意义了,此时 ActivityFragment 就能够通过 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 中的一员,RoomKotlin 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 来实现倒计时

ActivityFragment 中通过 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 这门年老的语言一样,一直再优化,也一直减少新的性能,例如 FlowChannel 等等。如果大家有更好的意见欢送留言评论。

本文由博客一文多发平台 OpenWrite 公布!

退出移动版