3

一看就会 Android协程的使用与封装

 2 years ago
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的接口定义就行。

源码在此。

ftd2

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK