Coroutines在Android中的实践

前面两篇文章讲了协程的基础知识和协程的通信.

见:

这篇我们就从Android应用的角度, 看看实践中都有哪些地方可以用到协程.

本文被收录在: https://github.com/mengdd/KotlinTutorials

Coroutines的用途

Coroutines在Android中可以帮我们做什么:

  • 取代callbacks, 简化代码, 改善可读性.
  • 保证Main safety.
  • 结构化管理和取消任务, 避免泄漏.

这有一个例子:

suspend fun fetchDocs() {                      // Dispatchers.Main
val result = get("developer.android.com") // Dispatchers.Main
show(result) // Dispatchers.Main
} suspend fun get(url: String) = // Dispatchers.Main
withContext(Dispatchers.IO) { // Dispatchers.IO (main-safety block)
/* perform network IO here */ // Dispatchers.IO (main-safety block)
} // Dispatchers.Main
}

这里get是一个suspend方法, 只能在另一个suspend方法或者在一个协程中调用.

get方法在主线程被调用, 它在开始请求之前suspend了协程, 当请求返回, 这个方法会resume协程, 回到主线程. 网络请求不会block主线程.

main-safety是如何保证的呢?

dispatcher决定了协程在什么线程上执行. 每个协程都有dispatcher. 协程suspend自己, dispatcher负责resume它们.

  • Dispatchers.Main: 主线程: UI交互, 更新LiveData, 调用suspend方法等.
  • Dispatchers.IO: IO操作, 数据库操作, 读写文件, 网路请求.
  • Dispatchers.Default: 主线程之外的计算任务(CPU-intensive work), 排序, 解析JSON等.

一个好的实践是使用withContext()来确保每个方法都是main-safe的, 调用者可以在主线程随意调用, 不用关心里面的代码到底是哪个线程的.

管理协程

之前讲Scope和Structured Concurrency的时候提过, scope最典型的应用就是按照对象的生命周期, 自动管理其中的协程, 及时取消, 避免泄漏和冗余操作.

在协程之中再启动新的协程, 父子协程是共享scope的, 也即scope会track其中所有的协程.

协程被取消会抛出CancellationException.

coroutineScopesupervisorScope可以用来在suspend方法中启动协程. Structured concurrency保证: 当一个suspend函数返回时, 它的所有工作都执行完毕.

它们两者的区别是: 当子协程发生错误的时候, coroutineScope会取消scope中的所有的子协程, 而supervisorScope不会取消没有发生错误的其他子协程.

Activity/Fragment & Coroutines

在Android中, 可以把一个屏幕(Activity/Fragment)和一个CoroutineScope关联, 这样在Activity或Fragment生命周期结束的时候, 可以取消这个scope下的所有协程, 好避免协程泄漏.

利用CoroutineScope来做这件事有两种方法: 创建一个CoroutineScope对象和activity的生命周期绑定, 或者让activity实现CoroutineScope接口.

方法1: 持有scope引用:

class Activity {
private val mainScope = MainScope() fun destroy() {
mainScope.cancel()
}
}

方法2: 实现接口:

class Activity : CoroutineScope by CoroutineScope(Dispatchers.Default) {
fun destroy() {
cancel() // Extension on CoroutineScope
}
}

默认线程可以根据实际的需要指定.

Fragment的实现类似, 这里不再举例.

ViewModel & Coroutines

Google目前推广的MVVM模式, 由ViewModel来处理逻辑, 在ViewModel中使用协程, 同样也是利用scope来做管理.

ViewModel在屏幕旋转的时候并不会重建, 所以不用担心协程在这个过程中被取消和重新开始.

方法1: 自己创建scope

private val viewModelJob = Job()

private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)

默认是在UI线程.

CoroutineScope的参数是CoroutineContext, 是一个配置属性的集合. 这里指定了dispatcher和job.

在ViewModel被销毁的时候:

override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}

这里viewModelJob是uiScope的job, 取消了viewModelJob, 所有这个scope下的协程都会被取消.

一般CoroutineScope创建的时候会有一个默认的job, 可以这样取消:

uiScope.coroutineContext.cancel()

方法2: 利用viewModelScope

如果我们用上面的方法, 我们需要给每个ViewModel都这样写. 为了避免这些boilerplate code, 我们可以用viewModelScope.

注: 要使用viewModelScope需要添加相应的KTX依赖.

  • For ViewModelScope, use androidx.lifecycle:lifecycle-viewmodel-ktx:2.1.0-beta01 or higher.

viewModelScope绑定的是Dispatchers.Main, 会自动在ViewModel clear的时候自动取消.

用的时候直接用就可以了:

class MainViewModel : ViewModel() {
// Make a network request without blocking the UI thread
private fun makeNetworkRequest() {
// launch a coroutine in viewModelScope
viewModelScope.launch(Dispatchers.IO) {
// slowFetch()
}
} // No need to override onCleared()
}

所有的setting up和clearing工作都是库完成的.

LifecycleScope & Coroutines

每一个Lifecycle对象都有一个LifecycleScope.

同样也需要添加依赖:

  • For LifecycleScope, use androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-alpha01 or higher.

要访问CoroutineScope可以用lifecycle.coroutineScope或者lifecycleOwner.lifecycleScope属性.

比如:

activity.lifecycleScope.launch {}
fragment.lifecycleScope.launch {}
fragment.viewLifecycleOwner.launch {}

lifecycleScope可以启动协程, 当Lifecycle结束的时候, 任何这个scope中启动的协程都会被取消.

这比较适合于处理一些带delay的UI操作, 比如需要用handler.postDelayed的更新UI的操作, 有多个操作的时候嵌套难看, 还容易有泄漏问题.

用了lifecycleScope之后, 既避免了嵌套代码, 又自动处理了取消.

lifecycleScope.launch {
delay(DELAY)
showFullHint()
delay(DELAY)
showSmallHint()
}

LifecycleScope和ViewModelScope

但是LifecycleScope启动的协程却不适合调用repository的方法. 因为它的生命周期和Activity/Fragment是一致的, 太碎片化了, 容易被取消, 造成浪费.

设备旋转时, Activity会被重建, 如果取消请求再重新开始, 会造成一种浪费.

可以把请求放在ViewModel中, UI层重新注册获取结果. viewModelScopelifecycleScope可以结合起来使用.

举例: ViewModel这样写:

class NoteViewModel: ViewModel {
val noteDeferred = CompletableDeferred<Note>() viewModelScope.launch {
val note = repository.loadNote()
noteDeferred.complete(note)
} suspend fun loadNote(): Note = noteDeferred.await()
}

而我们的UI中:

fun onCreate() {
lifecycleScope.launch {
val note = userViewModel.loadNote()
updateUI(note)
}
}

这样做之后的好处:

  • ViewModel保证了数据请求没有浪费, 屏幕旋转不会重新发起请求.
  • lifecycleScope保证了view没有leak.

特定生命周期阶段

尽管scope提供了自动取消的方式, 你可能还有一些需求需要限制在更加具体的生命周期内.

比如, 为了做FragmentTransaction, 你必须等到Lifecycle至少是STARTED.

上面的例子中, 如果需要打开一个新的fragment:

fun onCreate() {
lifecycleScope.launch {
val note = userViewModel.loadNote()
fragmentManager.beginTransaction()....commit() //IllegalStateException
}
}

很容易发生IllegalStateException.

Lifecycle提供了:

lifecycle.whenCreated, lifecycle.whenStarted, lifecycle.whenResumed.

如果没有至少达到所要求的最小生命周期, 在这些块中启动的协程任务, 将会suspend.

所以上面的例子改成这样:

fun onCreate() {
lifecycleScope.launchWhenStarted {
val note = userViewModel.loadNote()
fragmentManager.beginTransaction()....commit()
}
}

如果Lifecycle对象被销毁(state==DESTROYED), 这些when方法中的协程也会被自动取消.

LiveData & Coroutines

LiveData是一个供UI观察的value holder.

LiveData的数据可能是异步获得的, 和协程结合:

val user: LiveData<User> = liveData {
val data = database.loadUser() // loadUser is a suspend function.
emit(data)
}

这个例子中的liveData是一个builder function, 它调用了读取数据的方法(一个suspend方法), 然后用emit()来发射结果.

同样也是需要添加依赖的:

  • For liveData, use androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha01 or higher.

实际上使用时, 可以emit()多次:

val user: LiveData<Result> = liveData {
emit(Result.loading())
try {
emit(Result.success(fetchUser()))
} catch(ioException: Exception) {
emit(Result.error(ioException))
}
}

每次emit()调用都会suspend这个块, 直到LiveData的值在主线程被设置.

LiveData还可以做变换:

class MyViewModel: ViewModel() {
private val userId: LiveData<String> = MutableLiveData()
val user = userId.switchMap { id ->
liveData(context = viewModelScope.coroutineContext + Dispatchers.IO) {
emit(database.loadUserById(id))
}
}
}

如果数据库的方法返回的类型是LiveData类型, emit()方法可以改成emitSource(). 例子见: Use coroutines with LiveData.

网络/数据库 & Coroutines

根据Architecture Components的构建模式:

  • ViewModel负责在主线程启动协程, 清理时取消协程, 收到数据时用LiveData传给UI.
  • Repository暴露suspend方法, 确保方法main-safe.
  • 数据库和网络暴露suspend方法, 确保方法main-safe. Room和Retrofit都是符合这个pattern的.

Repository暴露suspend方法, 是主线程safe的, 如果要对结果做一些heavy的处理, 比如转换计算, 需要用withContext自行确定主线程不被阻塞.

Retrofit & Coroutines

Retrofit从2.6.0开始提供了对协程的支持.

定义方法的时候加上suspend关键字:

interface GitHubService {
@GET("orgs/{org}/repos?per_page=100")
suspend fun getOrgRepos(
@Path("org") org: String
): List<Repo>
}

suspend方法进行请求的时候, 不会阻塞线程.

返回值可以直接是结果类型, 或者包一层Response:

@GET("orgs/{org}/repos?per_page=100")
suspend fun getOrgRepos(
@Path("org") org: String
): Response<List<Repo>>

Room & Coroutines

Room从2.1.0版本开始提供对协程的支持. 具体就是DAO方法可以是suspend的.

@Dao
interface UsersDao {
@Query("SELECT * FROM users")
suspend fun getUsers(): List<User> @Insert
suspend fun insertUser(user: User) @Update
suspend fun updateUser(user: User) @Delete
suspend fun deleteUser(user: User)
}

Room使用自己的dispatcher来确定查询运行在后台线程.

所以你的代码不应该使用withContext(Dispatchers.IO), 会让代码变得复杂并且查询变慢.

更多内容可见: Room

Kotlin Coroutines在Android中的实践的更多相关文章

  1. Android架构(一)MVP架构在Android中的实践

    Android架构(一)MVP架构在Android中的实践 https://www.300168.com/yidong/show-2790.html   核心提示:为什么要重视程序的架构设计 对程序进 ...

  2. Kotlin 第二弹:Android 中 PDF 创建与渲染实践

    这是 Kotlin 练习的的第二篇.这一篇的由来是因为刚刚在 Android 开发者官网查看 API 的时候,偶然看到了角落里面的 pdf 相关. 我仔细看看了详细文档,发现这个还蛮有意思的,关键是编 ...

  3. Android中保存静态秘钥实践(转)

    本文我们将讲解一个Android产品研发中可能会碰到的一个问题:如何在App中保存静态秘钥以及保证其安全性.许多的移动app需要在app端保存一些静态字符串常量,其可能是静态秘钥.第三方appId等. ...

  4. Kotlin Coroutines不复杂, 我来帮你理一理

    Coroutines 协程 最近在总结Kotlin的一些东西, 发现协程这块确实不容易说清楚. 之前的那篇就写得不好, 所以决定重写. 反复研究了官网文档和各种教程博客, 本篇内容是最基础也最主要的内 ...

  5. 探究高级的Kotlin Coroutines知识

    要说程序如何从简单走向复杂, 线程的引入必然功不可没, 当我们期望利用线程来提升程序效能的过程中, 处理线程的方式也发生了从原始时代向科技时代发生了一步一步的进化, 正如我们的Elisha大神所著文章 ...

  6. Coroutines in Android - One Shot and Multiple Values

    Coroutines in Android - One Shot and Multiple Values 在Android中, 我们用到的数据有可能是一次性的, 也有可能是需要多个值的. 本文介绍An ...

  7. Android中的Coroutine协程原理详解

    前言 协程是一个并发方案.也是一种思想. 传统意义上的协程是单线程的,面对io密集型任务他的内存消耗更少,进而效率高.但是面对计算密集型的任务不如多线程并行运算效率高. 不同的语言对于协程都有不同的实 ...

  8. Android游戏开发实践(1)之NDK与JNI开发01

    Android游戏开发实践(1)之NDK与JNI开发01 NDK是Native Developement Kit的缩写,顾名思义,NDK是Google提供的一套原生Java代码与本地C/C++代码&q ...

  9. Android中的接口回调技术

    Android中的接口回调技术有很多应用的场景,最常见的:Activity(人机交互的端口)的UI界面中定义了Button,点击该Button时,执行某个逻辑. 下面参见上述执行的模型,讲述James ...

随机推荐

  1. 神舟+win10+ubuntu16.04+256GSSD+1THHD双系统安装加openssl踩坑之旅

    上海最近搞活动调休,要搞深度学习,win上还是不方便,准备弄个ubuntu.于是有以下回忆文字. 在机器上装了个双系统.花了两天.再也不想玩了. 准备用ubuntu来做深度学习的. 本文写于2019年 ...

  2. access 2013下载 access 2010下载 access 2007下载 Access 2003下载 安装交流的论坛

    在网上搜索了一个access 2013下载 access 2010下载 access 2007下载 Access 2003下载 安装交流的论坛 office安装的常见问题: http://www.of ...

  3. python 3.7.5 官方tutorial 学习笔记

    用了好久python,还没有完整看过官方的tutorial,这几天抽空看了下,还是学到些东西 --- Table of Contents 1. 课前甜点 2. 使用 Python 解释器 2.1. 调 ...

  4. AtCoder Grand Contest 038E - Gachapon

    \(\bf Description\) 一个 \(0\) 到 \(n-1\) 的随机数生成器,生成 \(i\) 的概率是 \(A_i/S\) ,其中 \(S=\sum_{i=0}^{n} A_i\) ...

  5. CSPS模拟 78

    大敛好稳啊..居然在模拟赛拿了540.. 有点畏惧.jpg 而我就是什么什么不行级人物了.. 真正在联赛拉开那么多分怎么追啊.. T1kmp?hash? T2 概率小到炸精时,对答案也就没贡献了 然后 ...

  6. js 重写a标签的href属性和onclick事件

    适应场景:假如移动端拨打电话,需要给a标签添加href属性,但是由于需求,需要链接跳转的同时给a标签添加onclick事件,如果不做任何处理的话,默认执行点击事件,而不会跳转href属性的链接. 怎么 ...

  7. Project Euler 57: Square root convergents

    五十七.平方根收敛(Square root convergents) 二的平方根可以表示为以下这个无穷连分数: \[ \sqrt 2 =1+ \frac 1 {2+ \frac 1 {2 +\frac ...

  8. JavaScrip 基础

    JavaScript 基础 前段的三剑客之一JS,来来来,看看它是什么鬼!到底如何让网页动起来的呢,今天就搞他一下. 一.JavaScript的简单介绍 javascript是一门动态弱类型的解释型编 ...

  9. Win7无法远程桌面

    Win7在设置里开启允许其他计算机远程连接,但局域网计算机还是连不上: 然后点击上面的为远程桌面启用windows防火墙例外,发现远程桌面是允许的: 实际上这个不是,问题的真正原因在于用于远程的338 ...

  10. Linux系统中nc工具那些不为人知的用法

    Linux nc命令用法 参考地址:https://www.cnblogs.com/jjzd/p/6306273.html -g<网关>:设置路由器跃程通信网关,最多设置8个; -G< ...