手把手教你搭建android项目框架(四)network网络库封装

161 阅读3分钟

前面我们说到,目前网络框架选择基本都为retrofit,目前算是最好用的android网络框架之一了。

今天我们来封装一下retorit,让他更加好用。

以下代码使用的lifecycleScope,均可使用viewModelScope。本文发布时均在activity中进行代码测试,正常开发过程中应使用viewmodel

先看效果

 //最简单的get请求
        repo {
            api { "https://www.baidu.com/" }
        }.request<StringBaseResponse>(lifecycleScope) { result ->
            //以下方法不使用可忽略
            result.onSuccess {
                //do something
            }
            result.onFailure {
                //do something
            }
        }
//带参数的请求
  repo {
            api { "https://www.baidu.com/" }
            params { "a" to "b" }
            params { "c" to "d" }
            requestMode { RequestMode.GET }
        }.request<StringBaseResponse>(lifecycleScope) { result ->
            //以下方法不使用可忽略
            result.onSuccess {
                //do something
            }
            result.onCancel {
                //do something
            }
            result.onCompletion {
                //do something
            }
            result.onFailure {
                //do something
            }
        }
//并发请求,一个失败全失败
  val repo1 = repo {
            api { "https://www.baidu.com/" }
            params { "a" to "b" }
            params { "c" to "d" }
            requestMode { RequestMode.GET }
        }
        val repo2 = repo {
            api { "https://www.github.com/" }
            params { "a" to "b" }
            params { "c" to "d" }
            requestMode { RequestMode.GET }
        }
        lifecycleScope.request<StringBaseResponse, StringBaseResponse>(repo1, repo2) { result ->
            //以下方法不使用可忽略
            result.onSuccess {
                val r1 = it.first
                val r2 = it.second
                //do something
            }
            result.onCancel {
                //do something
            }
            result.onCompletion {
                //do something
            }
            result.onFailure {
                //do something
            }
        }

下面开始封装思路,retrofit创建就不多说了,直接看代码

object RetrofitClient {
  //这里我们填写自己需要的主域名,当然后期可更换,因此按规则填一个就行,否则创建retrofit会报错。
    private const val BASE_URL = "http://www.baidu.com"

    private fun provideOkHttpClient(): OkHttpClient {
        return OkHttpClient.Builder()
            .connectTimeout(10, TimeUnit.SECONDS)
            .writeTimeout(10, TimeUnit.SECONDS)
            .readTimeout(10, TimeUnit.SECONDS)
            .retryOnConnectionFailure(true)
            .build()
    }

    fun provideRetrofit(): Retrofit {
        return Retrofit.Builder()
            .client(provideOkHttpClient())
            .baseUrl(BASE_URL)
//这里我们不使用gsonconvertadapter,我们先都取String,后续我们再说为什么
            .addConverterFactory(StringConverterFactory())
            .build()
    }
}

class StringConverterFactory : Converter.Factory() {
    override fun responseBodyConverter(
        type: Type,
        annotations: Array<out Annotation>,
        retrofit: Retrofit
    ): Converter<ResponseBody, *> {
        return Converter<ResponseBody, String> { value -> value.string() }
    }
}

request属性类封装

//这里的属性可能不全,需要的自行添加
class BaseRequest {
    var api: String? = null

    var requestMode: Int = RequestMode.GET

    var file: File? = null

    private var params = mutableMapOf<String, Any>()

    var contentType: MediaType? = null
    
    fun api(init: () -> String) {
        api = init()
    }

    fun requestMode(init: () -> Int) {
        requestMode = init()
    }

    fun file(init: () -> File) {
        file = init()
    }

    fun params(init: () -> Pair<String, Any>) {
        val p = init()
        params[p.first] = p.second
    }

    fun contentType(init: () -> MediaType?) {
        contentType = init()
    }

    override fun toString(): String {
        return "api:$api \n params :$params"
    }

    fun reflectParameters(): MutableMap<String, Any> {
        return params
    }
    
}

repo请求类封装,这里的思路是将请求包裹成一个类,然后利用请求的各种参数进包裹后,使用固定的apiService进行请求。

class Repo {
    var req: BaseRequest = object : BaseRequest() {}

    private fun injectApiService(): Api {
        return RetrofitClient.provideRetrofit().create(Api::class.java)
    }

    suspend fun execute() = withContext(Dispatchers.IO) {
        val request = req
        val params = request.reflectParameters()
        val apiService = injectApiService()

        val api = request.api ?: throw RuntimeException("repo没有传入地址")
        when (request.requestMode) {
            RequestMode.MULTIPART -> {
                val requestBody: RequestBody =
                    request.file?.asRequestBody(request.contentType)
                        ?: throw RuntimeException("execute MULTIPART 时,file 不能为空")
                apiService.uploadFile(
                    api,
                    MultipartBody.Part.createFormData(
                        "uploadFile",
                        request.file?.name,
                        requestBody
                    ),
                )
            }

            RequestMode.POST -> {
                apiService.post(
                    api,
                    buildBody(params)
                )
            }

            RequestMode.GET -> {
                apiService.get(
                    api,
                    buildBody(params)
                )
            }

            RequestMode.PUT -> {
                apiService.put(
                    api,
                    buildBody(params)
                )
            }

            RequestMode.DELETE -> {
                apiService.delete(
                    api,
                    buildBody(params)
                )
            }

            RequestMode.DELETE_BODY -> {
                apiService.deleteBody(
                    api,
                    buildBody(params)
                )
            }

            else -> {
                throw UnsupportedOperationException("不支持的requestMode")
            }
        }
    }

    private fun buildBody(body: MutableMap<String, Any>): MutableMap<String, Any> {
        return body
    }

}

上述常规封装完成,下面开始优化,我们使用kotlin的扩展方法让repo创建支持dsl,并且能够简化调用。 首先来看repo创建

inline fun repo(init: BaseRequest.() -> Unit): Repo {
    val repo = Repo()
    val req = BaseRequest()
    req.init()
    repo.req = req
    return repo
}
//通过扩展方法可以在任意类中创建repo,并且让repo初始化baseRequest属性类时支持dsl

接下来,将retrofit请求结果转换为flow,便于操作

//这里我们不处理异常,异常正常抛出即可,接下来通过flow的操作符进行异常处理
inline fun <reified T : BaseResponse> Repo.toFlow() = flow {
    val jsonResult = execute()
    //接收String类型返回值,特殊处理
    if (T::class.java == StringBaseResponse::class.java) {
        emit(StringBaseResponse(jsonResult) as T)
    } else {
        val rsp: T = jsonResult.toObject<T>()
            ?: throw JsonSyntaxException("请求网络执行结果转化为object失败")
        emit(rsp)
    }
}.flowOn(Dispatchers.IO)

拿到flow后,我们通过flow操作符进行操作,并处理异常。

inline fun <reified T : Any> Flow<T>.completedIn(
    scope: CoroutineScope,
    crossinline callback: (r: Result<T>) -> Unit
) {
    this@completedIn.catch {
        val pair = it.toPair()
        if (pair.first == NetworkExceptionConstantCode.CANCEL) {
            callback(Result.Cancel())
        } else {
            callback(Result.Failure(pair))
        }
    }.onEach {
        callback(Result.Success(it))
    }.catch {
        val pair = it.toPair()
        if (pair.first == NetworkExceptionConstantCode.CANCEL) {
            callback(Result.Cancel())
        } else {
            callback(Result.Failure(pair))
        }
    }.onCompletion {
        callback(Result.Completion())
    }.launchIn(scope).start()
}

将上述两个方法通过一个request方法关联,便可达到文章开头的使用效果

inline fun <reified T : BaseResponse> CoroutineScope.request(
    repo: Repo, crossinline callback: (r: Result<T>) -> Unit
) {
    repo.toFlow<T>().completedIn(this, callback)
}

如此,我们便完成了retrofit的基础封装,本封装较为基础,如有高级操作请自行添加。 完整代码:项目链接