1. 添加依赖
dependencies {
...
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:x.x.x"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:x.x.x"
}
具体版本号去官网搜索即可
2. 协程作用
协程可以用来解决回调地狱问题,作为一门新的技术,我们总会要去跟之前的代码作比对,看看使用新的技术能够带来哪方面的提升。
首先我们已网络获取数据为例,看下之前的实现方式:
@UiThread
fun makeNetworkRequest() {
slowFetch { result ->
show(result)
}
}
然后在看下协程实现的方式:
@UiThread
suspend fun makeNetworkRequest() {
val result = slowFetch()
show(result)
}
// slowFetch is main-safe using coroutines
suspend fun slowFetch(): SlowResult { ... }
suspend 关键词 可能跟其他语言中的async 类似,都是不阻塞当前线程,异步等待数据返回。
通过比对我们可以发现,使用协程有如下优势:
- 让代码更容易理解,而且让代码更加精简
- 不会阻塞线程
- 可以链式操作
上面示例中因为就只有一个从网络获取数据的方法,可能还看不出其威力,假设我们有如下需求,两次从服务端获取数据,然后在把数据写入到数据库,让我们来看看用协程的方式实现:
// Request data from network and save it to database with coroutines
// Because of the @WorkerThread, this function cannot be called on the
// main thread without causing an error.
@WorkerThread
suspend fun makeNetworkRequest() {
// slowFetch and anotherFetch are suspend functions
val slow = slowFetch()
val another = anotherFetch()
// save is a regular function and will block this thread
database.save(slow, another)
}
// slowFetch is main-safe using coroutines
suspend fun slowFetch(): SlowResult { ... }
// anotherFetch is main-safe using coroutines
suspend fun anotherFetch(): AnotherResult { ... }
3.协程关键点
CoroutineScope
协程作用域,通过Job来管理作用域范围内的所有协程,当job cancel 时会取消作用域范围内的所有协程。
应用场景: 当从Activity 或者 Fragment退出时,可以调用job cancel 用来取消其所有协程
Dispatcher
使用协程时可以通过Dispatcher很方便的切换线程,指定线程技巧:
1.开销较小的可以直接在主线程中操作而不用担心阻塞线程,这样可以节省线程间切换的资源消耗
2.开销较大的如操作数据库,解析大的json文件等需要指定后台线程
像Room、Retrofit等这种本身就不会占用主线程时间的可以直接在主线程中应用即可,从而简化代码。
viewModelScope
viewModelScope是ViewModel的扩展方法,这个作用域是绑定在Dispatchers.Main上的,在ViewModel onCleared的时候自动cancel,当然要使用这个方法需要添加依赖:
dependencies {
...
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:x.x.x"
}
在ViewModel中的使用示例:
使用前代码:
private fun updateTaps() {
// TODO: Convert updateTaps to use coroutines
tapCount++
BACKGROUND.submit {
Thread.sleep(1_000)
_taps.postValue("$tapCount taps")
}
}
使用viewModelScope后代码:
fun updateTaps() {
// launch a coroutine in viewModelScope
viewModelScope.launch {
tapCount++
// suspend this coroutine for one second
delay(1_000)
// resume in the main dispatcher
// _snackbar.value can be called directly from main thread
_taps.postValue("$tapCount taps")
}
}
4.协程替代callback实现方式
看下使用前的代码:
fun refreshTitle() {
// TODO: Convert refreshTitle to use coroutines
_spinner.value = true
repository.refreshTitleWithCallbacks(object : TitleRefreshCallback {
override fun onCompleted() {
_spinner.postValue(false)
}
override fun onError(cause: Throwable) {
_snackBar.postValue(cause.message)
_spinner.postValue(false)
}
})
}
在以前,我们一般用回调来处理异步耗时数据
tips: object: TitleRefreshCallback
这个是在kotlin中构建匿名类的一种方式,它创建了一个新的实现TitleRefreshCallback
接口实例对象
然后在看下我们使用协程之后的方式:
fun refreshTitle() {
viewModelScope.launch {
try {
_spinner.value = true
repository.refreshTitle()
} catch (error: TitleRefreshError) {
_snackBar.value = error.message
}finally {
_spinner.value = false
}
}
}
refreshTitle 模仿网络请求实现如下:
suspend fun refreshTitle() {
delay(500)
}
上面代码通过异常来处理错误场景,从而避免使用接口来自定义错误处理
示例关键点说明
协程启动:
1.新建一个协程,我们需要使用launch来启动协程
2.已经有一个协程,从协程内部启动协程,有两种方式:
- 当不需要返回值时,使用
launch
启动 - 当需要返回值时,使用
async
启动
Exception异常:
在协程中使用uncaught exceptions
和在正常的方法中使用基本是类似的,不过需要注意的是,默认情况下,他们会取消Job
,
即会通知父协程取消作用域内的所有协程,如果这些异常没有去handle,则最终会传给协程作用域CoroutineScope
。
5.协程替代耗时操作
回调实现方案:
fun refreshTitleWithCallbacks(titleRefreshCallback: TitleRefreshCallback) {
// This request will be run on a background thread by retrofit
BACKGROUND.submit {
try {
// Make network request using a blocking call
val result = network.fetchNextTitle().execute()
if (result.isSuccessful) {
// Save it to database
titleDao.insertTitle(Title(result.body()!!))
// Inform the caller the refresh is completed
titleRefreshCallback.onCompleted()
} else {
// If it's not successful, inform the callback of the error
titleRefreshCallback.onError(
TitleRefreshError("Unable to refresh title", null))
}
} catch (cause: Throwable) {
// If anything throws an exception, inform the caller
titleRefreshCallback.onError(
TitleRefreshError("Unable to refresh title", cause))
}
}
}
使用协程后方案:
suspend fun refreshTitle() {
// interact with *blocking* network and IO calls from a coroutine
withContext(Dispatchers.IO) {
val result = try {
// Make network request using a blocking call
network.fetchNextTitle().execute()
} catch (cause: Throwable) {
// If the network throws an exception, inform the caller
throw TitleRefreshError("Unable to refresh title", cause)
}
if (result.isSuccessful) {
// Save it to database
titleDao.insertTitle(Title(result.body()!!))
} else {
// If it's not successful, inform the callback of the error
throw TitleRefreshError("Unable to refresh title", null)
}
}
}
技能点:
1.协程使用withContext
来切换不同的dispatcher
2.协程默认提供三种Dispatchers
:
Main:主线程
IO:针对IO操作,如从网络或者磁盘中读取数据等
Default: 主要针对CPU密集型任务
使用room和retrofit自带协程接口
在上面小结中,我们使用withContext在自己管理线程切换,因为上面只是从网络中获取数据和插入数据到数据库,这两步如果使用room框架和retrofit框架自带协程支持可以更加进一步的简化代码,首先我们改造下插入数据库代码和从网络中获取代码:
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTitle(title: Title)
interface MainNetwork {
@GET("next_title.json")
suspend fun fetchNextTitle(): String
}
然后将上一步中手动管理线程代码改造如下:
suspend fun refreshTitle() {
try {
// Make network request using a blocking call
val result = network.fetchNextTitle()
titleDao.insertTitle(Title(result))
} catch (cause: Throwable) {
// If anything throws an exception, inform the caller
throw TitleRefreshError("Unable to refresh title", cause)
}
}
这样代码比之前精简了非常多
tips: Room和Retrofit 是自己内部自定义的线程,而不是使用的Dispatchers.IO
高级函数传递suspend方法
在写代码时,我们需要把重复代码抽离出来,下面示例中就是通过高阶函数抽离等待加载代码,首先先看下之前的实现代码:
fun refreshTitle() {
viewModelScope.launch {
try {
_spinner.value = true
repository.refreshTitle()
} catch (error: TitleRefreshError) {
_snackBar.value = error.message
}finally {
_spinner.value = false
}
}
}
直接上改造之后的代码:
fun refreshTitle() {
launchDataLoading{
repository.refreshTitle()
}
}
private fun launchDataLoading(block: suspend () ->Unit) : Job {
return viewModelScope.launch {
try {
_spinner.value = true
block()
} catch (error: TitleRefreshError) {
_snackBar.value = error.message
}finally {
_spinner.value = false
}
}
}
上述实现中,将block: suspend () ->Unit
suspend lamda表达式当做参数传递