Android中使用Kotlin协程(Coroutines)和Retrofit进行网络请求(三)之异常处理与封装

  • 写在前面
    前面文章介绍了一些kotlin协程和retorfit结合进行网络请求的基础,但是如果在前面的demo执行的过程中,我们断开手机网络,会发生什么?没错,APP会因为IO异常而崩溃!为什么呢,这是因为retrofit在执行excute()方法时会throws IOException,而enqueue()方法则不会,因为它会把IOException处理放在callback的onFailure方法里.所以如果我们需要使用excute()方法就需要手动处理异常.

  • 异常的分类
    当一个网络请求得到的结果并非是你的期望值,我们就可以看作是请求异常,那么请求异常有哪些原因呢,我把请求异常分为三类:
    第一类,网络问题,可能是设备网络未连接,连接信号弱,网络拥堵,等等原因,这就是IOException;
    第二类,连接服务器问题,我们都知道正常的网络访问结果返回的code是200,如果返回502,404等,那就是服务器,可能是请求地址有问题可能是请求方式有问题或者服务器原因,这种在retrofit中也有表达;
    第三类,返回值异常,就是服务器能正常返回给你数据,但是数据不是你想要的,这就是逻辑问题,可能你的参数不对,可能后端处理不对,也可能根据你的请求本身逻辑就应该显示这种异常.这种需要根据你项目具体的接口文档进行决定,需要自行对结果进行判断
    好,类型定了,那就先用代码表现出来,先写一个枚举类

    enum class ErrorType {
        NETWORK_ERROR,//网络出错
        SERVICE_ERROR,//服务器访问异常
        RESPONSE_ERROR//请求返回值异常
    }
    

    再来一个错误类用于封装错误信息

     /**
     * 网络请求出错的响应
     */
    data class ErrorResponse(
        val errorType:ErrorType,//错误类型
        val errorTag:String,//错误tag,用于区别哪个请求出错
        val errorCode: String?,//错误代码
        val message: String?//错误信息
    )
    
  • 创建ApiSerevice
    retrofit需要创建一个接口类,不过我们在kotlin中可以对这个类直接进行初始化,上一篇文章中已经演示了如何通过伴生对象直接获取retrofit单例实例(实际上接口没办法做到真正的单例,因为无法私有构造函数)这次我们通过invoke()函数的方式来实例化retrofit,至于单例,实际项目中可以通过依赖注入框架来实现,话不多说先上代码

    interface ApiService {
    
        @POST("versionupdate/getCurrentAppVersion")
        fun getCurrentAppVersion(@Query("json") json:String) :Call<UpdateResult>
    
        @POST("userinfo/signin")
        fun userLogin(@Query("json") json:String):Call<LoginResult>
    
        companion object {
    		//operator 是构造函数操作符, operator fun invoke()相当于是实现java的构造函数
            operator fun invoke(): ApiService {
                //自定义一个拦截器,打印请求地址和请求结果
                val paramInterceptor = Interceptor{ chain ->
                    val url = chain.request().url().url().toString()
    
                    LogUtil.d("发送请求:${URLDecoder.decode(url,"utf-8")}")
                    val response = chain.proceed(chain.request())
                    //注意这里不能直接使用response.body.string(),否则流会关闭,会报异常
                    val responseBody = response.peekBody(1024*1024)
                    LogUtil.d("请求结果:${responseBody.string()}")
                    return@Interceptor response
                }
    
                val okHttpClient = OkHttpClient.Builder()
                    .addInterceptor(paramInterceptor)
                    .build()
    
                return Retrofit.Builder()
                     .baseUrl("http://*****/") //代码节选自我个人真实项目,为安全起见,隐藏baseUrl
                     .addConverterFactory(GsonConverterFactory.create())
                     .client(okHttpClient)
                     .build()
                     .create(ApiService::class.java)
            }
        }
    }	
    
  • 网络请求类
    我在前面的文章里说过了,在实际项目不应该在activity或者fragment中直接调用网络请求API,需要在一个专门的网络请求类中进行.为什么呢?这是因为如果在activity或者fragment中直接操作网络请求,由于是异步操作,有内存泄漏的风险,另外也会不符合解耦的原则,如果网络请求遍布project各个地方,还会对接口测试造成困难.所以我们需要创建网络请求类,基于解耦原则,我们先创建一个接口

    /**
     * 用于网络请求获取数据的接口,两个方法对应上面两个网络请求
     */
    interface EduNetworkDataSource {
    	//用于封装请求错误的LiveData,UI通过监听这个值来判断网络是否发生异常
        val errorResult:LiveData<ErrorResponse>
    
        suspend fun fetchCurrentAppVersion(param: GetVersionParam):LiveData<UpdateResult>
    
        suspend fun userLogin(param:LoginParam):LiveData<LoginResult>
    }
    

    请注意,我上面所有的网络请求结果都用LiveData封装,它是一个可观察数据改变的对象,具体关于LiveData和ViewModel,我在前面MVVM架构的文章中略作过讲解,如果你想详细了解,请参看google的官方文档.简单点说就是当它的值发生改变时,它会通知它的观察者,并且它会监听UI的生命周期.之所以使用suspend关键字,是因为网络请求是一个阻塞操作,而我们网络请求都会放在协程中进行.
    好,下面我们看看具体请求实现类

    /**
     * 网络接口请求实现类
     */
    class EduNetworkDataSourceImpl(
        private val apiService: ApiService,
        private val context: Context
    ) : EduNetworkDataSource {
    
        //请求错误结果,这里的SingleLiveData是继承自LiveData,当读取error信息之后自动清空其中的信息,防止下一个观察者读取到之前的错误信息
        private val _errorResult = SingleLiveData<ErrorResponse>()
        /*这个用于给外界使用,LiveData值无法在修改,MutableLiveData才可以修改,防止其他类修改值*/
        override val errorResult: LiveData<ErrorResponse>
            get() = _errorResult
    
        private val gson = Gson()
    
        //从服务器获取最新版本
        override suspend fun fetchCurrentAppVersion(param: GetVersionParam) =
            handleRequest("获取最新版本") {
                apiService.getCurrentAppVersion(gson.toJson(param)).execute()
            }
    
        //用户登陆
        override suspend fun userLogin(param: LoginParam) =
            handleRequest("用户登陆") {
                apiService.userLogin(gson.toJson(param)).execute()
            }
    
    
        /**
         * 统一处理请求错误以及向LiveData发送数据
         * 这是一个高阶函数,第一个参数用于标识是哪个网络请求
         * 第二个参数就是一个方法(这和javaScript中把方法当作参数传递类似)
         * 这个BaseResult是用来判断业务逻辑失败的,根据项目实际情况自行定义,下面"description"和"code"来自于BaseResult
         */
        private suspend fun <T : BaseResult> handleRequest(
            tag: String,
            action: () -> Response<T>
        ): LiveData<T> {
            val liveData = MutableLiveData<T>()
            withContext(Dispatchers.IO) {
                try {
                    val response = action()
                    if (response.isSuccessful) {
                        val body = response.body()
                        //这个根据具体前后端接口协议来定
                        if (body != null && "0" == body.code) {
                            liveData.postValue(body)
                        } else {
                        	//逻辑异常通过实际项目中具体制定的前后端接口文档来决定,我们将异常结果发送出去
                            context.showToast("$tag 失败:${body?.description}")
                            _errorResult.postValue(
                                ErrorResponse(
                                    ErrorType.RESPONSE_ERROR,
                                    tag,
                                    body?.code,
                                    body?.description
                                )
                            )
                        }
    
                    } else {
    					//这表示请求结果非200,服务器异常,将错误信息和响应码发送出去
                        context.showToast("$tag 失败:${response.code()} - ${response.message()}")
                        _errorResult.postValue(
                            ErrorResponse(
                                ErrorType.SERVICE_ERROR,
                                tag,
                                response.code().toString(),
                                response.message()
                            )
                        )
                    }
    
                } catch (e: IOException) {
                	//如果有IO异常,那说明是网络有问题,直接将错误信息的值发送出去
                    e.printStackTrace()
                    LogUtil.e(e.toString())
                    context.showToast("$tag 失败:$e")
                    _errorResult.postValue(
                        ErrorResponse(
                            ErrorType.NETWORK_ERROR,
                            tag,
                            null,
                            e.toString()
                        )
                    )
                }
            }
            return liveData
        }
    }
    

    上面用到了SingleLiveData,代码如下

    class SingleLiveData<T> : MutableLiveData<T>() {
        override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
            super.observe(owner, Observer {
                if(it != null){
                    observer.onChanged(it)
                    postValue(null)
                }
            })
        }
    }
    
  • 使用网络请求
    首先说明一点,在实际项目开发中,你不应该在UI类(我所说的UI类就是指Activity或者Fragment)直接使用NetworkDataSource 类进行网络请求,因为你请求到的数据可能需要进行逻辑处理,一系列的数据转换,以及存储到数据库等等操作,在UI类中操作会让UI类变得庞大繁复难以维护和测试,如果你项目采用MVP架构,你应该在Presenter中进行,如果使用MVVM架构,你应该在Repository中进行,这里我简单演示一下,在Repository中进行网络请求,以及它如何传递给UI,实际数据传递过程类似于我前面的文章中讲到的架构
    首先是Repository类

    class EduRepositoryImpl(
        private val eduNetworkDataSource: EduNetworkDataSource
    ) : EduRepository {
    
        private val userinfo = MutableLiveData<Userinfo>()
    
        //获取用户信息
        fun getUserInfo() = userinfo as LiveData<Userinfo>
    
        //用户登陆,登陆之后更新用户信息
        override suspend fun userLogin(param: LoginParam): LiveData<LoginResult> {
            return eduNetworkDataSource.userLogin(param).also { loginResultLiveData ->
                loginResultLiveData.value?.let {loginResult ->
                    userinfo.postValue(loginResult.userinfo)
                }
            }
        }
    
        //获取最新版本
        override suspend fun getCurrentAppVersion(param: GetVersionParam): LiveData<UpdateResult> {
            return eduNetworkDataSource.fetchCurrentAppVersion(param)
        }
    
        //错误信息
        override suspend fun getError(): LiveData<ErrorResponse> {
            return eduNetworkDataSource.errorResult
        }
    
    }
    

    然后是ViewModel类,这里只展示一个接口的使用

    class SplashViewModel(private val eduRepository: EduRepository): ViewModel() {
    
        suspend fun getCurrentAppVersion(param: GetVersionParam) = eduRepository.getCurrentAppVersion(param)
    
        suspend fun getError() = eduRepository.getError()
    }
    

    然后是ViewModelFactory类

    class SplashViewModelFactory(private val eduRepository: EduRepository): ViewModelProvider.NewInstanceFactory() {
    
        override fun <T : ViewModel?> create(modelClass: Class<T>): T {
            @Suppress("UNCHECKED_CAST")
            return SplashViewModel(eduRepository) as T
        }
    }
    

    最后是在UI类中的相关代码,注意,UI类还是需要继承CoroutineScope ,并且初始化job和coroutineContext,这个和前面的代码一样,我就不贴出来了,具体请参见前面的文章讲解

    launch {
            viewModel.getCurrentAppVersion(GetVersionParam("1")).observe(this@SplashActivity, Observer {
            //网络访问成功,这里会接收到livedata的数据更新通知
                if (it.versionupdate.versioncode > BuildConfig.VERSION_CODE) {
                    showToast("有新版本${it.versionupdate.versionname}可用")
                } else {
                    delayToHome()
                }
            })
            viewModel.getError().observe(this@SplashActivity, Observer {
            //网络访问失败,livedata会通知这里
                delayToHome()
            })
    }
    

    如此,在UI类中,只需要极少量的代码就可以实现网络访问以及异常监听,并且即使你不监听错误,程序也不会崩溃或者运行异常,因为在DataSource类有try/catch.
    最后,这一切,无论是网络请求还是数据监听,都是在UI生命周期安全范围内进行的,因为Livedata监听了UI生命周期,我们的协程launch也同样在UI生命周期内才会执行,并且我们的observer监听,在主线程,又在协程中,所以你既可以操作UI,又可以放心大胆的进行耗时操作而无需担心会阻塞线程(当然是不建议在UI类中执行耗时操作的,因为UI类由系统监管,随时可能因为UI类的回收导致你的耗时操作被中断).

原创文章,转载请注明出处,谢谢

你可能感兴趣的:(kotlin)