将kotlin协程用于网络请求---完整实例,看这一篇就够了

前言:关于kotlin协程的介绍网上一大堆,用于网络请求的介绍也是一大堆,此文章不讲解各种原理,只讲实例使用,只要你有kotlin基础保证能看懂,看完就可以实际将kotlin协程应用于网络请求,从此废弃掉回调地狱,让你的app飞起来吧

本文的网络请求使用了Retrofit2 + okhttp,因为使用的是协程,就再也不需要回调地狱了,所以抛弃了Rxjava

1.先集成相关sdk

在app模块目录build.gradle中添加

apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

..........
dependencies {
    implementation 'androidx.core:core-ktx:1.1.0-alpha04'
    implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0-beta01'
    implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.0.0'
    implementation 'com.google.code.gson:gson:2.6.2'//解析接口返回的数据我用到了这个,如果你是用fastjson的话,可以忽略掉这个,继续用fastjson就行了
    implementation 'com.squareup.retrofit2:retrofit:2.9.0' //这个版本很重要,必须要大于2.6.0,如果你的项目是低于2.6.0的话,更新一下版本,否则实际运行时会出现错误
    implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.2"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2"
}

2.创建Retrofit公共请求类

通常我们同一个项目,和服务器后端应该存在一些固定约束,比如接口返回的固定字段,会有code、message、data这些,那么发送给接口的请求,也应该有一些固定约束,比如固定的header等,所以就需要将网络请求进行一层封装

class RetrofitClient {
    //实际项目应用中,应该存在至少dev环境和idc线上环境,笔者这里还有test环境,如果你的项目没有这些环境,那么可以直接return 唯一的地址,甚至这个方法都可以不需要,直接使用唯一的环境即可
    fun getCoroutineServiceApi():ServiceApi{
        if(HttpApi.baseIp == "xxx" || HttpApi.baseIp == "xxx"){
            return coroutineServiceApiDev
        }else{
            return coroutineServiceApiOnLine
        }
    }

    private val coroutineServiceApiDev: ServiceApi by lazy { //不清楚by lazy的自行去百度,这是委托模式,可以延迟加载,并且只在首次访问时计算值
        val retrofitClient = Retrofit.Builder()
            .baseUrl(HttpApi.baseIp)
            .client(OkHttpClient.Builder()
                .addInterceptor(HttpLoggingInterceptor(HttpLoggingInterceptor.Logger { message ->
                    Log.i(TAG, message)
                }).setLevel(HttpLoggingInterceptor.Level.BODY)
                ).addInterceptor(Interceptor { chain ->
                    //这里就可以添加一些通用请求头了
                    val request: Request = chain.request()
                        .newBuilder()
                        .addHeader("Content-Type", "application/json")
                        .addHeader("version", MyApplication.getInstances().getVersion())
                        .addHeader("deviceId", MyApplication.getInstances().getDeviceid())
                        .addHeader("osType", MyApplication.osType)
                        .addHeader("token",  token)
.addHeader("channelCode",MyApplication.getInstances().getChannelNo()+MyApplication.getInstances().getHumeChannel())
.addHeader("androidId",MyApplication.getInstances().getAndroidid())
                        .addHeader("ua",URLEncoder.encode(MyApplication.mUa,"UTF-8"))
                        .addHeader("oaid",MyApplication.getInstances().getOaid())
                        .addHeader("imei",MyApplication.getInstances().getIMEI())
                        .addHeader("uuid",MyApplication.getInstances().getuniqueId())
                        .addHeader("vendor",Build.MANUFACTURER)
                        .build()
                    KLogger.e("xiaolitest","当前uuid:"+MyApplication.getInstances().getuniqueId())
                    Log.e("xiaolitest","当前渠道:"+MyApplication.getInstances().getChannelNo())
                    chain.proceed(request)
                }).build())
            .addConverterFactory(DsGsonConverterFactory.create())//这里我是把接口返回的值序列化,就像上面说的,同一个项目,后台和客户端的数据返回应该有一些固定约束
            .build()
        retrofitClient.create(ServiceApi::class.java)
    }
    //coroutineServiceApiOnLine我就不贴了,写法完全一样,只是baseUrl不一样而已,一个测试环境的,一个线上环境的
}

3.创建ServiceApi

上文中创建了Retrofit的属性委托,返回的对象都是ServiceApi,那么就需要写这个ServiceApi了

interface ServiceApi {
 @POST("card-user/xxx") //这个不用描述吧?懂retrofit的都知道,代表的是请求方式以及请求地址
 suspend fun getUserGotoTest():BaseResult<UserJumpConfigBean> //注意到开头的suspend关键字了吗?它很重要,因为协程体调用外部的方法,它必须是suspend的,否则会报错
}


//贴一下BaseResult代码,大部分的约束也应该如此
data class BaseResult <T>(
    val code :String,
    val success:Boolean,
    val message:String,
    val time:String,
    val data: T

)

4.创建viewmodel,并在里面生命协程作用域

viewmodel并不是协程所必须创建的,但属于lifecycle的viewmodel,能感知生命周期,在生命周期的末尾取消掉协程,都不用去管内存泄漏这些问题了,它不香么?

class MyViewModel:ViewModel() {
 /**
  * 这是此 ViewModel 运行的所有协程所用的任务。
  * 终止这个任务将会终止此 ViewModel 开始的所有协程。
  */
    private val viewModelJob = SupervisorJob()

 val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
 val ioScope = CoroutineScope(Dispatchers.IO + viewModelJob)
 override fun onCleared() { //不清楚的去百度一下viewmodel生命周期
  super.onCleared()
  viewModelJob.cancel()
 }

    //协程一共有几种调度器,是有对应区别的,如下:
/*
Dispatchers 决定协程在哪个线程或线程池上运行(启动和恢复)。最重要的选项有:

    Dispatchers.Defualt:被用来执行 CPU 密集型操作
    Dispatchers.Main:被我们用来访问主线程,例如在 Android、Swing 或者 JavaFX 上
    Dispatchers.Main.immediate:它和 Dispatchers.Main 运行在同一个线程上,但如果没有必要,它不会重新调度
    Dispatchers.IO:被用来执行一些阻塞线程的操作
    调用了 limitParallelism的 Dispatchers.IO 或者带有自定义线程池的 Dispatcher.IO:我们用来处理大量的阻塞调用
    调用了 limitParallelism 并设置为1的 Dispatchers.Default 或 Dispatchers.IO,或具有单个线程的自定义调度器:被用来修改共享状态
    Dispatchers.Unconfined:当我们不关心协程在哪个线程上被挂起时使用
*/
}

5.创建一个BaseActivity吧,通常我们的项目不都有它么,在里面设置我们将要用到的viewmodel以及serviceapi

abstract class BaseActivity  : AppCompatActivity(){
//熟悉吧,又用到了委托属性,它只会加载一次,不用每次都计算,不香么
val myViewModel: MyViewModel by lazy {
        ViewModelProviders.of(this).get(MyViewModel::class.java)
    }
    val coroutineServiceApi: ServiceApi by lazy {
        RetrofitClient.getRetrofitClient().getCoroutineServiceApi()
    }
    //其它的代码我就省略了,不重要
}

6.一切都有了,那么使用吧!

class TestKT :BaseActivity(){
    override fun getLayoutId(): Int {
        return R.layout.item_hot_cake_text
    }

    override fun initView() {
        myViewModel.ioScope.launch{ //这里我用的是Dispatchers.IO调度器,因为只是请求网络读写数据呀,不用放到主线程
            var data = coroutineServiceApi.getUserGotoTest()
            initData(data)
        }
    }

//留意到了吗?又是suspend关键字,协程体调用外部方法必须是suspend的,否则会报错
    suspend fun initData(data: BaseResult<UserJumpConfigBean>){
        withContext(Dispatchers.Main){ //因为上面的调度器是IO线程的,但UI只能在主线程更新,所以这里要生命Main调度器,否则会报错,如果上面调用的是uiScope那么这里就可以不用写这个了
            tv_content.text = data.message
        }
    }

}

7.贴一下序列化的代码

楼下有网友评论说Gson序列化失败就会崩溃,这是因为你序列化的代码没有对接口的异常进行判断,这个问题比较初级哈,鉴于此篇文章就是给初步使用协程请求网络用的,就贴一下序列化的代码吧,其实这个代码是通用的,还用的是java版本

public class DsGsonConverterFactory extends Converter.Factory {

    public static final String TAG = DsGsonConverterFactory.class.getSimpleName();
    private final Gson gson;

    private DsGsonConverterFactory(Gson gson) {
        if (gson == null) throw new NullPointerException("gson == null");
        this.gson = gson;
    }

    public static DsGsonConverterFactory create() {
        return create(new Gson());
    }

    public static DsGsonConverterFactory create(Gson gson) {
        return new DsGsonConverterFactory(gson);
    }


    @Nullable
    @Override
    public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {
        TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type));
        return new GsonResponseBodyConverter<>(gson, adapter);
    }

    @Nullable
    @Override
    public Converter<?, RequestBody> requestBodyConverter(Type type, Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {
        TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type));
        return new GsonRequestBodyConverter(gson, adapter);
    }

    private final static class GsonRequestBodyConverter<T> implements Converter<T, RequestBody> {

        private static final MediaType MEDIA_TYPE = MediaType.parse("application/json;charset=UTF-8");
        private static final Charset UTF_8 = Charset.forName("UTF-8");

        private final Gson gson;
        private final TypeAdapter<T> adapter;

        GsonRequestBodyConverter(Gson gson, TypeAdapter<T> adapter) {
            this.gson = gson;
            this.adapter = adapter;
        }

        @Override
        public RequestBody convert(T value) throws IOException {
            Buffer buffer = new Buffer();
            Writer writer = new OutputStreamWriter(buffer.outputStream(), UTF_8);

            JsonWriter jsonWriter = gson.newJsonWriter(writer);
            adapter.write(jsonWriter, value);
            jsonWriter.close();
            return RequestBody.create(MEDIA_TYPE, buffer.readByteString());
        }
    }

    private final static class GsonResponseBodyConverter<T> implements Converter<ResponseBody, T> {
        private final Gson gson;
        private final TypeAdapter<T> adapter;
        private final TypeAdapter<BaseResult> mExceptionAdapter;

        GsonResponseBodyConverter(Gson gson, TypeAdapter<T> adapter) {
            this.gson = gson;
            this.adapter = adapter;
            this.mExceptionAdapter = (TypeAdapter<BaseResult>) adapter;
        }

        @Override
        public T convert(ResponseBody value) throws IOException {
            String response = value.string();
            BaseResult baseResult = gson.fromJson(response, BaseResult.class);

            MediaType contentType = value.contentType();
            Charset charset = contentType != null ? contentType.charset(UTF_8) : UTF_8;
            InputStream inputStream = new ByteArrayInputStream(response.getBytes());
            Reader reader = new InputStreamReader(inputStream, charset);
            JsonReader jsonReader = gson.newJsonReader(reader);
            T entity = null;
            try {
                entity = adapter.read(jsonReader);
            } catch (JsonSyntaxException jsonSyntaxException) { //类型转换错误
                KLogger.INSTANCE.e("json解析错误:" + jsonSyntaxException.getMessage());
                entity = (T) baseResult;
            } catch (Exception e) {
                e.printStackTrace();
                throw e;
            } finally {
                value.close();
                return entity;
            }
        }
    }

}

NOTO::写在最后,至此,只要你有kotlin基础,用过retrofit +okhttp,那么你现在已经可以将网络请求应用到实际项目中了。但是!但是!这还远远不够的,本文的目的是期望你能快速上手运用协程,将异步的代码用同步的逻辑来写,这样会使得代码更简洁,可读性也更高,而且完全废弃掉了Rxjava,也不用管什么回调地狱了,但你在会使用协程之后,就应该去关注一下更细节的东西;

比如:协程的作用域、协程里面需要再启动协程、必要时取消协程等等。举个实际项目中的例子,假设我们有两个网络请求,分别是接口A和接口B,接口B的请求依赖于接口A返回的字段,那么如果不用协程,我们是不是要写接口A的回调,然后在接口A里面去调用接口B。如果不是两个网络请求,是三个或者更多呢?代码是不是看起来就很臃肿?那么协程就真香了,因为协程有async/await来处理并发,试想一下,原本繁重的一个接口回调后再调另外一个接口,变成了如下代码,是不是代码量减少了,可读性也越高了?

coroutineScope.launch(Dispatchers.IO) {
	val a1 = async{ 接口A() }
	val userInfo = a1.await()
	val a2 = async{ 接口B(userInfo.token) }
	val msgList = a2.await()
}

还有上面我不止一次提到的suspend关键字,它到底有什么用呢?

  • 5
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值