本章前言
本章节中除了会对协程做讲解外,不会对其他引入的框架做讲解。文章是基于用户已经对这些框架有一定的入门基础上,对与框架如何结合kotlin
协程的使用做一个引导。整个篇幅会有些长,我们会在结合使用的同时,做一些架构上的封装,也是为了方便后续在实战的时候,大家能更方便、直观的理解代码。
笔者也只是一个普普通通的开发者,架构上的设计不一定合理,大家可以自行吸收文章精华,去糟粕。
kotlin协程的使用封装
在上一章节中,我们已经了解了协程在Activity
、Fragment
、Lifecycle
、Viewmodel
的基础使用,以及如何简单的自定义一个协程。本章节中,我们主要是做一些基础的封装工作。我们将在上一章节的内容基础上,引入DataBinding
、LiveData
、Flow
等做一些基础封装。比如:Base类的定义,协程的使用封装,常用扩展函数等。
我们先引入本章节所使用到的相关库:
// Kotlin
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.4.32"
// 协程核心库
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3"
// 协程Android支持库
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3"
implementation "androidx.activity:activity-ktx:1.2.2"
implementation "androidx.fragment:fragment-ktx:1.3.3"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.3.1"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1"
// ok http
implementation "com.squareup.okhttp3:okhttp:4.9.0"
implementation 'com.squareup.okhttp3:logging-interceptor:4.9.0'
// retrofit
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation "com.squareup.retrofit2:converter-scalars:$retrofit_version"
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
复制代码
现在我们就可以开始做一些基础的封装工作,同时在app的bulid.gradle
文件中开启dataBinding
的使用
android {
buildFeatures {
dataBinding = true
}
//省略...
}
复制代码
文章中基于DataBinding
的使用,可以参考【封装DataBinding让你少写万行代码】
ViewModel
的使用可以参考【ViewModel的日常使用封装】
这两篇文章是为了本章节专门写的扩展性阅读。
最近因为电脑炸机了,信号输出不稳定,开始以为是显卡坏了,折腾了几天还是没整好,最后发现是主板被腐蚀导致线路故障,当前用的主板停产很久了,最后只能找个兼容的,等了几天才到货,最终也导致本章节稍微延后了几天。电子产品太脆弱了,一定要注意防摔
、防磕碰
、防腐蚀
!废话不多说,下面进入我们今天的正题。
协程的常用环境
在实际的开发过程中,我们经常需要把耗时处理移到非主线程
上执行,等耗时操作异步完成以后,再回到主线程
上刷新界面。基于这些需求,我们大致可以把使用协程的环境分为下面五种环境:
网络请求
回调处理
数据库操作
文件操作
其他耗时操作
下面我们首先对网络请求
这块进行处理。目前市面上大多数APP的在处理网络请求
时候,都是使用的RxJava
结合Retrofit
、OkHttp
进行网络请求处理。我们最终的目的也是使用协程
结合Retrofit
、okHttp
进行网络请求处理。
我们在这里只是针对Retrofit
、OkHttp
结合协程
、ViewModel
、LiveData
使用讲解,如果需要了解Retrofit
和okHttp
的原理,可以看看其他作者的原理分解文章。
协程在网络请求下的封装及使用
为了演示效果,笔者在万维易源申请了一面免费的天气API,我们使用的接口地址:
http[s]://route.showapi.com/9-2?showapi_appid=替换自己的值&showapi_sign=替换自己的值
复制代码
此接口返回的通用数据格式,其中showapi_res_body
返回的json内容比较多,笔者从中挑选了我们主要关注的几个字段:
参数名称 | 类型 | 描述 |
---|---|---|
showapi_res_body |
String | 消息体的JSON封装,所有应用级的返回参数将嵌入此对象 。 |
showapi_res_code |
int | 查看错误码 |
showapi_res_error |
String | 错误信息的展示 |
{
"showapi_res_error":"",
"showapi_res_code":0,
"showapi_res_body":{
"time":"20210509180000", //预报发布时间
"now":{
"wind_direction":"西风", //风向
"temperature_time":"01:30", //获得气温的时间
"wind_power":"0级", //风力
"aqi":"30", //空气指数,越小越好
"sd":"40%", //空气湿度
"weather_pic":"http://app1.showapi.com/weather/icon/day/00.png", //天气小图标
"weather":"晴", //天气
"rain":"0.0", //降水量(mm)
"temperature":"15" //气温
}
}
}
复制代码
当然我们还需要一个接收数据的对象,为了避免和其他库容易弄混淆,我们命名为CResponse
,这个结构大家都很熟悉:
data class CResponse<T>(
@SerializedName("showapi_res_code")
val code: Int,
@SerializedName("showapi_res_error")
val msg: String? = null,
@SerializedName("showapi_res_body")
val data: T
)
复制代码
由于API返回的字段名称实在是不符合笔者的胃口,而且用起来也不美观。所以笔者通过Gson
的注解SerializedName
将属性进行重命名。我们在实际开发中常常也会遇到这种问题,同样可以通过这种方法进行处理。
data class Weather(
val now: WeatherDetail,
val time: String
)
data class WeatherDetail(
val aqi: String,
val rain: String,
val sd: String,
val temperature: String,
@SerializedName("temperature_time")
val temperatureTime: String,
val weather: String,
@SerializedName("weather_pic")
val weatherPic: String,
@SerializedName("wind_direction")
val windDirection: String,
@SerializedName("windPower")
val windPower: String
)
复制代码
然后我们创建一下okHttp
、Retrofit
。在Retrofit
2.6版本以后我们不再需要引入Retrofit
的coroutine-adapter
适配器库,我们直接使用即可:
object ServerApi {
val service: CoroutineService by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
build()
}
private fun build():CoroutineService{
val retrofit = Retrofit.Builder().apply {
baseUrl(HttpConstant.HTTP_SERVER)
client(OkHttpClientManager.mClient)
addConverterFactory(ScalarsConverterFactory.create())
addConverterFactory(GsonConverterFactory.create())
}.build()
return retrofit.create(CoroutineService::class.java)
}
}
object HttpConstant {
internal val HTTP_SERVER = "https://route.showapi.com"
}
复制代码
object OkHttpClientManager {
val mClient: OkHttpClient by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
buildClient()
}
private fun buildClient(): OkHttpClient {
val logging = HttpLoggingInterceptor()
logging.level = if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.NONE
return OkHttpClient.Builder().apply {
addInterceptor(CommonInterceptor())
addInterceptor(logging)
followSslRedirects(true)
}.build()
}
}
复制代码
由于我们在调用的天气API接口的时候showapi_appid
和showapi_sign
必传的值,所以我们增加了一个CommonInterceptor
拦截器来统一处理:
class CommonInterceptor : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val oldRequest = chain.request()
val httpUrl = oldRequest.url
val host = httpUrl.host
if (HttpConstant.HTTP_SERVER != host ) {
return chain.proceed(oldRequest)
}
val urlBuilder = httpUrl.newBuilder()
//这里填写自己申请appid和sign
urlBuilder.addQueryParameter("showapi_appid", SHOW_API_APPID)
urlBuilder.addQueryParameter("showapi_sign", SHOW_API_SIGN)
val request = oldRequest
.newBuilder()
.url(urlBuilder.build())
.build()
return chain.proceed(request)
}
}
复制代码
为了方便快速演示,笔者从请求的参数列表中只抽取了一个用来演示,接下来我们定义我们在请求需要通过Retrofit
使用的接口CoroutineService
:
请求参数 | 类型 | 描述 |
---|---|---|
area |
String | 要查询的地区名称 。 |
interface CoroutineService {
@FormUrlEncoded
@POST("/9-2")
suspend fun getWeather(
@Field("area") area: String
): CResponse<Weather>
复制代码
可以看到我们在使用Retrofit
结合协程使用时,我们只需要在函数前增加suspend
关键字就可以,同时返回结果可以直接定义为,我们需要从请求结果中解析出来的数据对象,而不再是像以前一样定义为Call<T>
。
到此为止,我们基于基础数据的定义已经结束了,下面我们将正式进入我们今天的主题。为了更加清晰的理解,笔者这里不会采用直接一步到位的方式。那样可能会有很多人阅读理解起来有困难。笔者将会对请求过程进行一步一步的封装,这里需要一点耐心。
我们先创建一个Repository
来请求数据:
class WeatherRepository {
suspend fun getWeather(
area: String
): CResponse<Weather>{
return ServerApi.service.getWeather(area)
}
}
复制代码
同时在创建一个MainViewModel
来使用Repository
class MainViewModel(private val repository: WeatherRepository):ViewModel() {
private val _weather:MutableLiveData<Weather> = MutableLiveData()
val mWeather: LiveData<Weather> = _weather
fun getWeather( area: String){
requestMain {
val result = repository.getWeather(area)
_weather.postValue(result.data)
}
}
}
复制代码
现在我们就可以在MainActivity
中创建MainViewModel
来调用方法获取天气数据。我们在创建ViewModel
对象的时候不再使用 ViewModelProviders.of(this).get(MainViewModel::class.java) 这种方式。而是使用activity-ktx
库中的viewModels
方法去创建:
public inline fun <reified VM : ViewModel> ComponentActivity.viewModels(
noinline factoryProducer: (() -> Factory)? = null
): Lazy<VM> {
val factoryPromise = factoryProducer ?: {
defaultViewModelProviderFactory
}
return ViewModelLazy(VM::class, { viewModelStore }, factoryPromise)
}
复制代码
这个方法需要我们传入一个Factory
,我们自己定义一个实现:
object ViewModelUtils {
fun provideMainViewModelFactory(
): MainViewModelFactory {
return MainViewModelFactory(MainRepository())
}
}
class MainViewModelFactory(
private val repository: MainRepository
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return MainViewModel(repository) as T
}
}
复制代码
接下来我们在MainActivity
使用,通过使用ViewModelUtils
获取MainViewModelFactory
,然后使用viewModels
进行创建我们需要的viewModel
对象:
class MainActivity : BaseActivity<ActivityMainBinding>() {
private val viewModel by viewModels<MainViewModel> {
ViewModelUtils.provideMainViewModelFactory()
}
override fun initObserve() {
viewModel.mWeather.observe(this) {
mBinding.contentTv.text = "$it"
}
}
override fun ActivityMainBinding.initBinding() {
this.mainViewModel = viewModel
}
}
复制代码
initObserve
是我们在BaseActivity
中定义的抽象方法。我们只在activity_main.xml
简单定义了一个Textview
来显示数据,虽然在XML中引入了mainViewModel
,但是为演示过程,我们没有使用DataBinding
直接做数据绑定。而在实际开发中应该是使用DataBinding
直接在XML中数据绑定。
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="mainViewModel"
type="com.carman.kotlin.coroutine.request.viewmodel.MainViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.MainActivity">
<TextView
android:id="@+id/content_tv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="这里显示获取到的数据"
android:textColor="@color/black"
android:textSize="18sp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
复制代码
我们成功的请求到数据,并且显示在我们界面上。但是有个问题上我们现在的请求是没有做异常处理的。现在我们处理下请求过程中的异常:
fun getWeather(area: String){
requestMain {
val result = try {
repository.getWeather(area)
} catch (e: Exception) {
when(e) {
is UnknownHostException -> {
//...
}
//... 各种需要单独处理的异常
is ConnectException -> {
//...
}
else ->{
//...
}
}
null
}
_weather.postValue(result?.data)
}
}
复制代码
这种做法虽然处理了异常,但是非常丑陋,而且我们需要在每一个请求地方写的时候,那将会是一个噩梦般的诅咒。 接下来将会是我们的重点内容,笔者将会封装出三种形式的调用,开始的时候对应的场景使用即可。
高阶函数方式
这个时候我们需要创建一个BaseRepository
来进行封装处理,我们通过onSuccess
获取成功的结果,通过onError
来处理针对此次请求的特有异常,以及通过onComplete
来处理执行完成的操作:
open class BaseRepository {
suspend inline fun <reified T : Any> launchRequest(
crossinline block: suspend () -> CResponse<T>,
noinline onSuccess: ((T?) -> Unit)? = null,
noinline onError: ((Exception)-> Unit) ? = null,
noinline onComplete: (() -> Unit)? = null ){
try {
val response = block()
onSuccess?.invoke(response?.data)
} catch (e: Exception) {
e.printStackTrace()
when (e) {
is UnknownHostException -> {
//...
}
//... 各种需要单独处理的异常
is ConnectException -> {
//...
}
else -> {
//...
}
}
onError?.invoke(e)
}finally {
onComplete?.invoke()
}
}
}
复制代码
这个时候我们再修改一下WeatherRepository
中的getWeather
方法,我们需要通过launchRequest
来包裹一下请求就可以:
suspend fun getWeather(
area: String,
onSuccess: (Weather?) -> Unit,
onError: (Exception) -> Unit,
onComplete: () -> Unit,
){
launchRequest({
ServerApi.service.getWeather(area)
}, onSuccess,onError, onComplete)
}
复制代码
然后我们修改一下MainViewModel
中的getWeather
方法,我们在处理异常的位置处理此次接口特有的异常即可,同时可以在请求结束后做一些收尾工作:
fun getWeather(area: String) {
requestMain {
repository.getWeather(area, {
_weather.postValue(it)
}, {
it.printStackTrace()
Log.d(Companion.TAG, "异常提示处理")
}, {
Log.d(TAG, "请求结束,处理收尾工作")
})
}
}
复制代码
同时除第一个执行请求的参数外,后面三个参数都可以传入为空实现。避免在不需要处理成功,异常,执行完成等操作的时候,出现这种影响美观的代码。假如我们通过sendData
服务器发送一个数据,这个数据是否处理成功我们不需要关心,这个时候我们就可以如下操作:
fun sendData(data: String) {
requestMain {
repository.launchRequest({
repository.sendData(data)
})
}
}
复制代码
我们再回过头来看看launchRequest
方法,我们在处理请求返回的结果时候直接就返回response
。但是实际开发中我们一般在请求接口返回数据的时候,是需要判断接口数据状态code
值是成功的时候才能返回数据。
我们本例中这个状态值是0
。这个时候我们需要处理一下增加一个处理response
的方法.我们再修改一下launchRequest
方法:
suspend inline fun <reified T : Any> launchRequest(
crossinline block: suspend () -> CResponse<T>,
noinline onSuccess: ((T?) -> Unit)? = null,
noinline onError: ((Exception) -> Unit)? = null,
noinline onComplete: (() -> Unit)? = null
) {
try {
val response = block()
when (response.code) {
HttpConstant.OK -> {
val isListType = T::class.isSubclassOf(List::class)
if (response.data == null && isListType) {
onSuccess?.invoke(Collections.EMPTY_LIST as? T)
} else {
onSuccess?.invoke(response?.data)
}
}
else -> onError?.invoke(CException(response))
}
} catch (e: Exception) {
e.printStackTrace()
when (e) {
is UnknownHostException -> {
}
//... 各种需要单独处理的异常
is ConnectException -> {
}
else -> {
}
}
onError?.invoke(e)
} finally {
onComplete?.invoke()
}
}
复制代码
可以看到我们在处理response
的时候,我们先通过判断返回的诗句类型是否为List
集合类型。如果是集合类型且数据返回了一个null
的时候,我们就尝试把一个的空集合转换为结果。
val isListType = T::class.isSubclassOf(List::class)
if (response.data == null && isListType) {
onSuccess?.invoke(Collections.EMPTY_LIST as? T)
} else {
onSuccess?.invoke(response?.data)
}
复制代码
多状态函数返回值方式
上面的封装方式我们是通过kotlin的高阶函数去实现的。假如我们想直接通过请求结果的时候,再结合其他请求处理数据通知界面刷新的时候,上面就显得很麻烦,而且好像又走到的无限嵌套的坑里。
这个时候我们就需要直接通过函数返回值来处理。现在我们首先的创建一个DataResult
来封装一下返回结果,我们将返回的数据分成成功或者失败两种:
sealed class DataResult<out T> {
data class Success<out T>(val data: T) : DataResult<T>()
data class Error(val exception: Exception) : DataResult<Nothing>()
}
复制代码
然然后在创建一个launchRequestForResult
把之前的launchRequest
代码拷贝过来稍作修改:
suspend inline fun <reified T : Any> launchRequestForResult(
noinline block: suspend () -> CResponse<T>
): DataResult<T> {
return try {
val response = block()
if (0 == response.code) {
val isListType = T::class.isSubclassOf(List::class)
if (response.data == null && isListType) {
DataResult.Success(Collections.EMPTY_LIST as? T) as DataResult<T>
} else {
DataResult.Success(response.data)
}
} else {
DataResult.Error(CException(response))
}
} catch (e: Exception) {
when (e) {
is UnknownHostException -> {
}
//... 各种需要单独处理的异常
is ConnectException -> {
}
else -> {
}
}
DataResult.Error(e)
}
}
复制代码
我们在WeatherRepository
中再增加getWeather
方法,通过launchRequestForResult
来处理请求:
suspend fun getWeather(area: String): DataResult<Weather> {
return launchRequestForResult {
ServerApi.service.getWeather(area)
}
}
复制代码
然后我们同时我们也在MainViewModel
中增加一个getWeatherForResult
方法,这个时候我们就可以按我们的常规的编写代码顺序处理结果:
fun getWeatherForResult(area: String) {
requestMain {
val result = repository.getWeather(area)
when(result){
is DataResult.Success ->{
_weather.postValue(result.data)
}
is DataResult.Error ->{
Log.d(TAG, "${(result?.exception}")
}
}
}
}
复制代码
当然,这种方式处理起来还是相对有些繁琐,因为当我们有多个请求是,我们需要写多个when
来判断结果。那如果我们也不想写这些模板代码又该如何处理呢
直接返回值的方式
这个时候我们就需要在launchRequestForResult
的基础上进一步的处理:
suspend inline fun <reified T : Any> launchRequest(
crossinline block: suspend () -> CResponse<T>
): T? {
return try {
block()
} catch (e: Exception) {
e.printStackTrace()
when (e) {
is UnknownHostException -> {
}
//... 各种需要单独处理的异常
is ConnectException -> {
}
else -> {
}
}
throw e
}?.run {
if (0 == code) {
val isListType = T::class.isSubclassOf(List::class)
return if (data == null && isListType) {
Collections.EMPTY_LIST as? T
} else {
data
}
} else {
throw CException(this)
}
}
}
复制代码
因为考虑到实际开发环境中,我们还是可能需要在外部处理异常提示的所以在这里还是通过throw
重新抛出异常。如果外部不想处理非内接口CException
异常,可以参考下面直接在catch
返回null
即可:
suspend inline fun <reified T : Any> launchRequest(
crossinline block: suspend () -> CResponse<T>
): T? {
return try {
block()
} catch (e: Exception) {
null
}?.run {
if (0 == code) {
val isListType = T::class.isSubclassOf(List::class)
if (data == null && isListType) {
Collections.EMPTY_LIST as? T
} else {
data
}
} else {
throw CException(this)
}
} ?: let {
null
}
}
复制代码
同样在WeatherRepository
中再增加getWeather
方法,通过获取返回值的launchRequest
来处理请求:
suspend fun getWeather(area: String): Weather? {
return launchRequest{
ServerApi.service.getWeather(area)
}
}
复制代码
因为我们在launchRequest
重新抛出了异常,所以我们需要在请求的地方捕获一下:
fun getWeather(area: String) {
requestMain {
val weather = try {
repository.getWeather(area)
} catch (e: Exception) {
//二次异常处理...
}
}
}
复制代码
上面的三种方式算上一种抛砖引玉,其实我们还可以进一步的通过抽象ViewModel
来统一处理内部接口请求异常。
如果您有更好的方法或者思路想法,欢迎交流。架构的演进以及代码的封装需要不断的学习和沟通,每一次知识交流与碰撞都是有意义的。
作者:一个被摄影耽误的程序猿
链接:https://juejin.cn/post/6962921891501703175
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。