

一看就会 Android协程的使用与封装
source link: http://www.androidchina.net/12557.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Kotlin协程的使用与封装
相信作为一个现代Android开发者,应该都用过或者听过协程了,还不少大佬都已经把协程给扒皮了,其本质就是线程池的封装。源码的解析和性能的对比,都有解说。
协程的性能还不如原生线程池,为什么我要用协程,是因为协程可以把碎片化的方法很方便的加入异步处理,发挥Android设备多核的优势。合理的使用协程,应用反而更流畅。
下面我就不涉及太多原理理论了,直接上代码,Android中如何使用和封装协程。
一. 协程的使用
常用的几个关键的函数方法
launch ,runBlocking, withContext ,async/await
前两者启动协程,后两者调度线程。
lauch 是非阻塞的 而runBlocking是阻塞的。直接上例子:
private fun testCoroutine1() {
//这里只是协程作用域
// GlobalScope.launch {
// lifecycleScope.launch {
// viewModelScope.launch { 都可以
CoroutineScope(Dispatchers.Main).launch {
delay(500)
YYLogUtils.w("协程1作用域内部执行")
}
YYLogUtils.w("协程1作用域wai部执行")
}
结果是先执行外部,再执行内部
而runBlocking恰恰相反
private fun testCoroutine2() {
runBlocking {
delay(500)
YYLogUtils.w("协程2作用域内部执行")
}
YYLogUtils.w("协程2作用域wai部执行")
}
结果是先执行内部,再执行外部,因为阻塞了。
所以一般我们开发绝大多数都是使用launch了。 而我们切换线程一般用withContext和 async/await.区别就是你想顺序执行还是并发执行。
顺序执行:
这里会先等待1秒输入1234,然后调用接口获取Industry,请求完成之后再调用接口获取School,当前全部完成之后隐藏Loading。
其中网络请求异常的处理已经在内部封装处理了,后面会讲到。
viewModelScope.launch {
//开始Loading
loadStartProgress()
val startTimeStamp = System.currentTimeMillis()
val res = withContext(Dispatchers.Default) {
//异步执行
delay(1000)
return@withContext "1234"
}
val endTimeStamp = System.currentTimeMillis()
YYLogUtils.w("res: $res time: ${endTimeStamp-startTimeStamp}")
//网络请求获取行业数据
val industrys = mRepository.getIndustry()
//返回的数据是封装过的,检查是否成功
industrys.checkResult({
//成功
_industryLD.postValue(it)
}, {
//失败
toastError(it)
})
//上面的请求执行完毕才会执行这个请求
val schools = mRepository.getSchool()
//返回的数据是封装过的,检查是否成功
schools.checkSuccess {
_schoollLD.postValue(it)
}
//完成Loading
loadHideProgress()
}
并发执行:
这里会同时调用Industry和School接口,等待两者都完成之后再展示UI。
viewModelScope.launch {
//开始Loading
loadStartProgress()
val industryResult = async {
mRepository.getIndustry()
}
val schoolResult = async {
mRepository.getSchool()
}
val localDBResult = async {
//loadDB()
YYLogUtils.w("thread:" + CommUtils.isRunOnUIThread())
delay(10000)
}
//一起处理数据
val industry = industryResult.await()
val school = schoolResult.await()
//如果都成功了才一起返回
if (industry is OkResult.Success && school is OkResult.Success) {
loadHideProgress()
_industryLD.postValue(industry.data!!)
_schoollLD.postValue(school.data!!)
}
YYLogUtils.e(localDBResult.await().toString() + "完成")
}
大家开发App常用的两种方式都已经掌握了,还有一个不常用但是很重要的点,就是网络请求去重。
场景:点击CheckBox调用接口是否开启通知,那么我们就要把用户推送id传给服务器。如果用户狂点CheckBox,那么我怎么请求网络?
常用的两种去重手段。一种是取消上一次的,另一种是队列排队一个一来。
老规矩直接上代码了:
/**
* 网络请求去重
*/
private var controlledRunner = ControlledRunner<OkResult<List<Industry>>>() //取消之前的
private val singleRunner = SingleRunner() //任务队列,排队,单独的
fun netDuplicate() {
viewModelScope.launch {
//比较常用
//取消上一次的,执行这一次的
controlledRunner.cancelPreviousThenRun {
return@cancelPreviousThenRun mRepository.getIndustry()
}.checkSuccess {
YYLogUtils.e("请求成功:")
_industryLD.postValue(it)
}
//前一个执行完毕了,再执行下一个
// singleRunner.afterPrevious {
// mMainRepository.getIndustry()
// }.checkSuccess {
// YYLogUtils.e("测试重复的数据:" + it.toString())
// }
}
}
控制器源码如下:
class SingleRunner {
private val mutex = Mutex()
/**
* 加入到任务队列,前一个任务执行完毕再执行下一个任务
*/
suspend fun <T> afterPrevious(block: suspend () -> T): T {
mutex.withLock {
return block()
}
}
}
class ControlledRunner<T> {
private val activeTask = AtomicReference<Deferred<T>?>(null)
suspend fun cancelPreviousThenRun(block: suspend () -> T): T {
activeTask.get()?.cancelAndJoin()
return coroutineScope {
val newTask = async(start = LAZY) {
block()
}
newTask.invokeOnCompletion {
activeTask.compareAndSet(newTask, null)
}
val result: T
while (true) {
if (!activeTask.compareAndSet(null, newTask)) {
activeTask.get()?.cancelAndJoin()
yield()
} else {
result = newTask.await()
break
}
}
result
}
}
/**
* 不执行新任务,返回上一个任务的结果
*/
suspend fun joinPreviousOrRun(block: suspend () -> T): T {
activeTask.get()?.let {
return it.await()
}
return coroutineScope {
val newTask = async(start = LAZY) {
block()
}
newTask.invokeOnCompletion {
activeTask.compareAndSet(newTask, null)
}
val result: T
while (true) {
if (!activeTask.compareAndSet(null, newTask)) {
val currentTask = activeTask.get()
if (currentTask != null) {
newTask.cancel()
result = currentTask.await()
break
} else {
yield()
}
} else {
result = newTask.await()
break
}
}
result
}
}
}
二. 网络请求协程的封装
Retrofit+协程的使用:
原理就是调用Retrofit方法,对它try-catch.得到的是网络请求错误信息,可以根据不同的Type类型。然后对Retrofit的返回结果再判断如果code不是200,那么就是Api错误(例如Token失效)。对错误和成果的结果做统一的封装返回给ViewModel处理。
方式一:
处理BaseRepository:
open class BaseRepository {
//无异常处理 -> 一般不用这个,一旦报错会App崩溃
suspend inline fun <T : Any> handleApiCall(call: suspend () -> BaseBean<T>): BaseBean<T> {
return call.invoke()
}
/**
* 推荐使用拓展函数extRequestHttp
* 如果要使用Base里面的方法请求网络这么使用
* return handleErrorApiCall(call = {
handleApiErrorResponse()
})
* 都可以实现网络请求
*/
//处理Http错误-内部再处理Api错误
suspend fun <T : Any> handleErrorApiCall(call: suspend () -> OkResult<T>, errorMessage: String = ""): OkResult<T> {
return try {
call()
} catch (e: Exception) {
if (!TextUtils.isEmpty(errorMessage)) {
OkResult.Error(IOException(errorMessage))
} else {
OkResult.Error(handleExceptionMessage(e))
}
}
}
//处理Api错误,例如403Token过期 把BaseBean的数据转换为自定义的Result数据
suspend fun <T : Any> handleApiErrorResponse(
response: BaseBean<T>,
successBlock: (suspend CoroutineScope.() -> Unit)? = null,
errorBlock: (suspend CoroutineScope.() -> Unit)? = null
): OkResult<T> {
return coroutineScope {
//执行挂起函数
if (response.code == 200) { //这里根据业务逻辑来 200 -1 等
successBlock?.let { it() }
OkResult.Success(response.data)
} else {
errorBlock?.let { it() }
OkResult.Error(IOException(response.message))
}
}
}
//处理自定义错误消息
fun handleExceptionMessage(e: Exception): IOException {
return when (e) {
is UnknownHostException -> IOException("Unable to access domain name, unknown domain name.")
is JsonParseException -> IOException("Data parsing exception.")
is HttpException -> IOException("The server is on business. Please try again later.")
is ConnectException -> IOException("Network connection exception, please check the network.")
is SocketException -> IOException("Network connection exception, please check the network.")
is SocketTimeoutException -> IOException("Network connection timeout.")
is RuntimeException -> IOException("Error running, please try again.")
else -> IOException("unknown error.")
}
}
}
使用如下:
suspend fun getServerTime(): OkResult<ServerTimeBean> {
return handleErrorApiCall({
handleApiErrorResponse(
MainRetrofit.apiService.getServerTime(
Constants.NETWORK_CONTENT_TYPE,
Constants.NETWORK_ACCEPT_V1
)
)
})
}
方式二:
使用扩展方法的直接一步到位处理:
suspend fun <T : Any> BaseRepository.extRequestHttp(call: suspend () -> BaseBean<T>): OkResult<T> {
//两种方式都可以,自用下面一种方式
// runCatching {
// call.invoke()
// }.onSuccess { response: BaseBean<T> ->
// if (response.code == 200) {
// OkResult.Success(response.data)
// } else {
// OkResult.Error(ApiException(response.code, response.message))
// }
// }.onFailure { e ->
// e.printStackTrace()
// OkResult.Error(handleExceptionMessage(Exception(e.message, e)))
// }
return try {
val response = call()
if (response.code == 200) {
OkResult.Success(response.data)
} else {
OkResult.Error(ApiException(response.code, response.message))
}
} catch (e: Exception) {
e.printStackTrace()
OkResult.Error(handleExceptionMessage(e))
}
}
suspend inline fun getIndustry(): OkResult<List<Industry>> {
return extRequestHttp {
DemoRetrofit.apiService.getIndustry(
Constants.NETWORK_CONTENT_TYPE,
Constants.NETWORK_ACCEPT_V1
)
}
}
调用接口都是固定的模板代码,和之前MVP的方式一样,只需要定义Retrofit-Api的接口定义就行。
源码在此。
Recommend
-
25
图标作为一种“视觉语言”,早已被大家所熟知。它能够将文字具象化的表达出来,让用户能够好的理解、认知产品的功能。 并且,在产品的 UI 设计中, 图标可以指引用户完成目标操作,还能节省空间、增...
-
12
一个 1 分钟就学会的 vue 小技巧(真的一看就会)爱前端不爱恋爱关注微信公众号:web前端学习圈,领取85G前端全套...
-
9
一看就会,效率翻倍!解锁摹客在线设计隐藏技能-UI中国用户体验设计平台 一看就会,效率翻倍!解锁摹客在线设计隐藏技能 ...
-
9
双十一爆款背后的10个消费心理学,一看就会,秒变“社群带货王” ...
-
9
柯理化是一种关于函数的高阶技术。柯里化是一种函数的转换,它是指将一个函数从可调用的 f(a, b, c) 转换为可调用的 f(a)(b)(c)。柯里化不会调用函数。它只是对函数进行转换。一个最简单的例子// 原本的sum函数以及应用 funct...
-
7
摹客RP是一款集快速设计、强大交互、矢量编辑于一体的设计工具,支持多人实时编辑,更能与摹客平台完美结合,快速进行设计协作和交付;摹客RP也是行业内
-
6
刮刮乐想必大家都玩过,小时候兜里一有钱,就喊上小伙伴兴冲冲的跑去家旁边的小卖部,用那稚嫩地小手递给老板那被捏的皱巴巴的五毛钱,满眼期待的刮着买来的刮刮乐,心里早已想好中了100块钱大钞要去买好多好多辣条、卡片、陀螺、奥迪双钻的悠悠...
-
8
大概 19 年前,在表哥哄骗下,我拿出了压岁钱,人生第一次进入网吧,第一次感受到了互联网冲浪的快乐。 为什么说是哄骗呢? 因为当时的我完全不会操控电脑,鼠标、键盘都不会用,只能坐在表哥旁边看着他打开一个又一个窗口,不一会那个「砍砍砍」...
-
3
新版Hilt在Android各种场景的详细使用手册 关于Hilt与Koin的性能对比,之前有过分析。Hilt可以在Java/Kotin的语言环境中使用,内存使用、启动时间、体积大小等都小有优势,但是...
-
6
...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK