美文网首页程序员Android
项目实践-网络层

项目实践-网络层

作者: 十思叶 | 来源:发表于2018-04-10 14:32 被阅读39次

    概述

    最近上线了一个项目,姑且称为TestApp,开发过程中遇到了一些坑,决定做一点总结。
    网络层使用了目前比较流行的Retrofit和Rxjava框架,编程语言使用了Kotlin。

    配置

    配置网络层Rxjava2.x和Retrofit最新的依赖

        // RxJava
        implementation deps.rxjava2
        implementation deps.rx_android
        // Network
        implementation deps.retrofit.runtime
        implementation deps.retrofit.gson
        implementation deps.retrofit.rxjava_adapter
        implementation deps.okhttp_logging_interceptor // 打印网络层log
    

    其中deps是versions.gradle文件中的一个变量, versions.gradle文件用于管理各个依赖的版本信息,可在全局gradle文件中引入。

    一般实践

    与后端通信的接口数据协议如下

    {
      "code": "0", // 0表示业务请求成功, 非0表示业务请求异常
      "message": "操作成功",
      "data": {
        "result": 1523326559000 // data可以为空、可以为单个数据、可以为规范的json数据
      },
      "responseTime": 1523326674474
    }
    
    

    对应的Model如下

    data class ApiResponse<out T>(val code: String,
                                  val data: T?,
                                  val message: String?) : Serializable
    

    Android端部分基础接口API如下

    
    interface TestApi {
        // 登录
        // 这里需要说明下,因为data即这里的TokenModel可以为空,Rxjava里不允许传递空,而response是不能为空的,所以最后返回response作为处理的对象。
        @POST("auth/login")
        fun login(@Body loginRequest: Map<String, String>
        ): Flowable<ApiResponse<TokenModel>>
       
        @GET("salt")
        fun getSalt(): Flowable<ApiResponse<SaltModel>>
    
        // 获取用户基本信息
        // token: header里的动态Token值
        @GET("users/v1/baseInfo")
        fun getUserBaseInfo(@Header("X-Authorization") token: String
        ): Flowable<ApiResponse<UserModel>>
    
        // 修改头像后通知服务端
        @PUT("users/v1/avatar")
        fun notifyAvatarUpdate(@Header("X-Authorization") token: String
        ): Flowable<ApiResponse<Boolean?>>
    
        // 获取上传头像需要的信息,我们上传头像到阿里云服务器,之后会单独说明这部分
        @GET("aliyun/sts")
        fun getAliOSSSTS(@Header("X-Authorization") token: String,
                         @Query("right") right: String,
                         @Query("prod") prod: String
        ): Flowable<ApiResponse<AliOSSSTSModel>>
    }
    

    实现接口的服务如下

    
    object TestService {
        private val netApi = createNetApi() 
        // 远程服务地址
        private val API_BASE_URL = BuildConfig.SERVICE_PLATFORM
    
        private val parameterInterceptor: Interceptor
            get() = Interceptor { chain ->
                val request = chain.request()
                val newUrlBuilder = request.url()
                        .newBuilder()
                        .scheme(request.url().scheme())
                        .host(request.url().host())
    
                val newRequest = request.newBuilder()
                        .method(request.method(), request.body())
                        .url(addParameters(newUrlBuilder, DataManager.baseRequestParams().get()).build()) // 这里是在Url的末尾添加基本请求参数,如版本号、平台类型、时间等
                        .removeHeader("Pragma")
                        .build()
                chain.proceed(newRequest)
            }
    
        private fun addParameters(builder: HttpUrl.Builder, params: Map<String, String>): HttpUrl.Builder {
            for ((key, value) in params) {
                builder.addQueryParameter(key, value)
            }
            return builder
        }
    
        fun api(): TestApi {
            return netApi
        }
    
        private fun createNetApi(): TestApi {
            val okHttpClient = OkHttpClient.Builder()
                    .addInterceptor(parameterInterceptor) // 添加基本参数
                    .addInterceptor(HttpLoggingInterceptor()
                           .setLevel(HttpLoggingInterceptor.Level.BODY))  // 打印log
                    .readTimeout(5, TimeUnit.SECONDS)
                    .connectTimeout(10, TimeUnit.SECONDS)
                    .build()
    
            val retrofit = Retrofit.Builder()
                    .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                    .addConverterFactory(GsonConverterFactory.create())
                    .baseUrl(API_BASE_URL)
                    .client(okHttpClient)
                    .build()
            return retrofit.create(TestApi::class.java)
        }
    }
    
    

    一般到达这一步就可以直接调用API了,如:

    // 检查版本
    TestService.api().checkVersion()
                    .compose(apply())
                    .subscribeOn(Schedulers.io())
                    .observeOn(AndroidSchedulers.mainThread()
                    .subscribe({ response->
                        // on success
                        // do something for response successful
                        // has new version or not
                    }, { t ->
                        // on error
                        // do something for net error
                    })
    

    但这样写需要考虑一下几个问题

    1. 代码重复。每次都要写线程切换等重复代码
    2. 异常处理。subscribe()的onSuccess处理的是响应成功的结果,包括业务正常code==0和业务异常code!=0两种情况,我们一般希望业务异常和网络异常放在一起统一处理。另外,网络异常的话,提示信息不友好,我们希望对其进行统一处理。
    3. 链式调用。比如请求登录的时候,需要对密码做加盐处理,即现请求盐值接口,之后请求登录接口,直接在调用的地方写则比较臃肿,多次调用会出现代码重复,增加维护难度。
    4. 接口统一。这里只显示了一个远程服务,如果有多个远程服务以及多个网络数据协议的话(比如上传头像到阿里云服务器)则需要分别写接口以及接口实现的服务,导致接口调用分散。

    解决以上几点,我们需要对基础的API方法做一层封装

    API封装

    下面对接口做封装

    
    object WrapNetApi {
    
        // version
        fun checkVersion(): Flowable<ApiResponse<VersionModel?>> {
            return TestService.api()
                    .checkVersion()
                    .compose(apply())
        }
    
        // 登录。链式请求
        fun loginWithSalt(mobile: String, password: String): Flowable<ApiResponse<TokenModel?>> {
            return TestService.api()
                    .getSalt()
                    .flatMap { response ->
                       // 从response里获取盐值并对mobile进行加密,得到参数map
                       // ----省略部分代码
                        return@flatMap TestService.api().login(map)
                    }
                    .compose(apply())
        }
        
        // 获取基本信息
        fun getUserBaseInfo(): Flowable<ApiResponse<UserModel?>> {
            return TestService.api()
                    .getUserBaseInfo(token())
                    .compose(apply())
        }
    
        // 上传头像。后面有具体说明
        fun uploadAvatar(context: Context, avatarData: ByteArray): Flowable<ApiResponse<Boolean?>> {
            return TestService.api()
                    .getAliOSSSTS(token(),"WRITE", "OSS")
                    .flatMap { response ->
                        AliOSSService.uploadFile(context, response.data!!, avatarData)
                    }
                    .subscribeOn(Schedulers.io())
                    .observeOn(Schedulers.io())
                    .flatMap {
                        TestService.api().notifyAvatarUpdate(token())
                    }
                    .compose(apply())
        }
    
        // 异常处理
        private fun <T> apply(): FlowableTransformer<ApiResponse<T?>, ApiResponse<T?>> {
            return FlowableTransformer { flowable ->
                flowable.map({ response ->
                    if (ApiCode.SUCCESS.code != response.code) {
                        // 业务异常。
                        throw ApiException(response)
                    }
                    response
                })
                        .onErrorResumeNext { t: Throwable ->
                            // 非业务异常
                            // ExceptionEngine.handleException将网络等非业务异常封装成ApiException类并赋予友好的提示message
                            Flowable.error(ExceptionEngine.handleException(t))
                        }
                        .subscribeOn(Schedulers.io())
                        .observeOn(AndroidSchedulers.mainThread())// 线程调度
            }
        }
    
        // 获取Token
        private fun token(): String {
            // 省略
        }
    }
    
    

    其中,ApiException如下

    
    open class ApiException(val code: String, message: String?) : Exception(message) {
        var data: Any? = null
    
        constructor(code: String, message: String?, data: Any?) : this(code, message) {
            this.data = data
        }
    
        constructor(apiResponse: ApiResponse<*>) : this(apiResponse.code, apiResponse.message, apiResponse.data)
    }
    
    

    ExceptionEngine代码如下

    object ExceptionEngine {
        fun handleException(e: Throwable): ApiException {
            if (e is ApiException) {
                return e
            }
    
            val apiCode: ApiCode = if (e is HttpException) {
                ApiCode.ERROR_HTTP
            } else if (e is JsonParseException
                    || e is JSONException
                    || e is ParseException) {
                ApiCode.ERROR_PARSE
            } else if (e is SocketException
                    || e is UnknownHostException) {
                ApiCode.ERROR_NETWORK
            } else {
                ApiCode.ERROR_UNKNOWN
            }
            return ApiException(apiCode.code, apiCode.description)
        }
    }
    

    如此,则实现了对API接口的封装,解决了上面提到的几个问题。
    下面说一下上传头像到阿里云的业务及代码实现。

    上传文件到阿里云

    阿里云文件服务是单独的服务,需要另外写接口类和实现接口的服务类,但阿里文件服务有自己的接口API,因此不需要写接口类,只需要写一个服务类封装即可,如下。

    阿里云文件服务

    object AliOSSService {
        private val clientConfiguration = config()
    
        // 基础配置
        private fun config(): ClientConfiguration {
            val conf = ClientConfiguration()
            conf.connectionTimeout = 15 * 1000 // 连接超时,默认15秒
            conf.socketTimeout = 15 * 1000 // socket超时,默认15秒
            conf.maxConcurrentRequest = 5 // 最大并发请求数,默认5个
            conf.maxErrorRetry = 2 // 失败后最大重试次数,默认2次
            return conf
        }
    
        // 上传文件,并改造API为Rxjava格式
        fun uploadFile(context: Context, stsModel: AliOSSSTSModel, fileData: ByteArray): Flowable<ApiResponse<Boolean?>> {
            val credentialProvider = OSSStsTokenCredentialProvider(
                    stsModel.credentials.accessKeyId,
                    stsModel.credentials.accessKeySecret,
                    stsModel.credentials.securityToken)
            val oss = OSSClient(context, stsModel.endpoint, credentialProvider, clientConfiguration)
            val put = PutObjectRequest(stsModel.bucket, stsModel.objectKey, fileData)
            return Flowable.create({ subscriber ->
                oss.asyncPutObject(put, object : OSSCompletedCallback<PutObjectRequest, PutObjectResult> {
                    override fun onSuccess(request: PutObjectRequest?, result: PutObjectResult?) {
                        subscriber.onNext(ApiResponse(ApiCode.SUCCESS.code, true, "success"))
                        subscriber.onComplete()
                    }
    
                    override fun onFailure(request: PutObjectRequest?, clientException: ClientException?, serviceException: ServiceException?) {
                        subscriber.onError(ApiException(
                                code = if (serviceException == null) "10" else serviceException.errorCode,
                                message = serviceException?.rawMessage
                        ))
                    }
                })
            }, BackpressureStrategy.LATEST)
        }
    }
    

    在WrapNetApi里进一步封装,增加Test服务相关请求

        // 上传头像。链式调用,先从Test服务获取上传头像需要的凭证,之后上传头像到阿里云服务,上传成功后通知Test服务
        fun uploadAvatar(context: Context, avatarData: ByteArray): Flowable<ApiResponse<Boolean?>> {
            return TestService.api()
                    .getAliOSSSTS(token(),"WRITE", "OSS") // 获取凭证
                    .flatMap { response ->
                        // 上传头像
                        AliOSSService.uploadFile(context, response.data!!, avatarData)
                    }
                    .subscribeOn(Schedulers.io())
                    .observeOn(Schedulers.io())
                    .flatMap {
                        // 通知“上传成功”
                        TestService.api().notifyAvatarUpdate(token())
                    }
                    .compose(apply())
        }
    

    相关文章

      网友评论

        本文标题:项目实践-网络层

        本文链接:https://www.haomeiwen.com/subject/phfnhftx.html