前言
上一篇关于MVVM 架构的基类封装,这篇会在MVVM 的基础上示范使用 kotlin+retrofit+okhttp 封装的网络框架,里面会涉及到协程的使用,
协程异常处理包装。
导入相关包
kotlin 相关配置见上一篇,这里主要是Retrofit 和okHttp3 的包
api 'com.squareup.okhttp3:logging-interceptor:3.6.0'
api 'com.squareup.retrofit2:adapter-rxjava2:2.6.0'
api 'com.squareup.retrofit2:adapter-rxjava:2.6.0'
api 'com.squareup.okhttp3:okhttp:3.12.0'
api 'com.squareup.retrofit2:retrofit:2.6.0'
api 'com.squareup.retrofit2:converter-gson:2.6.0'
api 'com.squareup.retrofit2:adapter-rxjava2:2.6.0'
封装
先看看图中基本的结构
- RetrofitClient: client 管理类
这里是整个业务封装的核心,其中包括https 证书校验、baseUrl 切换
class RetrofitClient {
/**
* retrofit 初始化build
*/
private fun RetrofitClient() {}
//做成单例
companion object {
private var retrofitClient: RetrofitClient? = null
private const val DEFAULT_TIME_OUT = 15
private val sRetrofitManager: MutableMap<Int, Retrofit> = HashMap()
fun getInstance(): RetrofitClient {
if (retrofitClient == null) {
synchronized(RetrofitClient::class.java) {
retrofitClient = RetrofitClient()
return retrofitClient as RetrofitClient
}
}
return retrofitClient as RetrofitClient
}
}
/**
* 创建连接客户端
*/
private fun createOkHttpClient(): OkHttpClient {
//设置请求头拦截器
//设置日志拦截器
val httpLoggingInterceptor = HttpLoggingInterceptor(HttpLoggingInterceptor.Logger.DEFAULT)
httpLoggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
//根据需求添加不同的拦截器
return OkHttpClient.Builder()
.connectTimeout(DEFAULT_TIME_OUT.toLong(), TimeUnit.SECONDS)
.writeTimeout(DEFAULT_TIME_OUT.toLong(), TimeUnit.SECONDS)
.readTimeout(DEFAULT_TIME_OUT.toLong(), TimeUnit.SECONDS)
.connectionPool(ConnectionPool(8, 10, TimeUnit.SECONDS)) //添加这两行代码
.sslSocketFactory(TrustAllCerts.createSSLSocketFactory()!!, TrustAllCerts())
.hostnameVerifier(TrustAllCerts.TrustAllHostnameVerifier())
.addInterceptor(httpLoggingInterceptor)
.build()
}
/**
* 根据host 类型判断是否需要重新创建Client,因为一个app 有不同的BaseUrl,切换BaseUrl 就需要重新创建Client
* 所以,就根据类型来从map中取出对应的client
*/
fun <T> getDefault(interfaceServer: Class<T>?, hostType: Int): T {
val retrofitManager = sRetrofitManager[hostType]
return if (retrofitManager == null) {
create(interfaceServer, hostType)
} else retrofitManager.create(interfaceServer!!)
}
/**
*
*/
private fun <T> create(interfaceServer: Class<T>?, hostType: Int): T {
val retrofit: Retrofit = Retrofit.Builder()
.baseUrl(BaseUrlConstants.getHost(hostType))
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.client(createOkHttpClient())
.build()
sRetrofitManager[hostType] = retrofit
if (interfaceServer == null) {
throw RuntimeException("The Api InterfaceServer is null!")
}
return retrofit.create(interfaceServer)
}
}
很多时候,我们app 的请求的域名不可能只有一个,所以,在不同情况下需要去切换baseUrl,那么在Retrofit 中,一个Url 只能对应一个Retrofit Client ,所以我们就可以用map 将retrofit的实例对象通过key -value的形式存储起来,而key就是用的BaseUrl,
当我们在传入不同BaseUrl时,会从内存中取出是否有对应创建过的Client,如没有,则创建并保存。从而达到了实时切换BaseUrl的目的。
* 根据host 类型判断是否需要重新创建Client,因为一个app 有不同的BaseUrl,切换BaseUrl 就需要重新创建Client
* 所以,就根据类型来从map中取出对应的client
*/
fun <T> getDefault(interfaceServer: Class<T>?, hostType: Int): T {
val retrofitManager = sRetrofitManager[hostType]
return if (retrofitManager == null) {
create(interfaceServer, hostType)
} else retrofitManager.create(interfaceServer!!)
}
- ApiAddress: 地址管理类
class ApiAddress {
companion object{
/**
* 登录
*/
const val LOGIN = "api/login"
}
}
- ApiException: 异常封包处理类
class ApiException : Throwable {
//这些字段根据后台定义去修改
private var code = 0
private var displayMessage: String? = null
constructor(code: Int, displayMessage: String?) {
this.code = code
this.displayMessage = displayMessage
}
constructor(code: Int, message: String?, displayMessage: String?) : super(message) {
this.code = code
this.displayMessage = displayMessage
}
fun getCode(): Int {
return code
}
fun setCode(code: Int) {
this.code = code
}
fun getDisplayMessage(): String? {
return displayMessage
}
fun setDisplayMessage(displayMessage: String?) {
this.displayMessage = displayMessage
}
fun getUMessage(): String? {
return displayMessage
}
}
- ApiService: Retrofit 接口类
interface ApiService {
/**
* get
*/
@GET(ApiAddress.LOGIN)
suspend fun login(
@Query("account") account: String,
@Query("password") password: String
): BaseResponse<Any>
/**
* post body
*/
@POST(ApiAddress.LOGIN)
suspend fun loginBody(@Body requestBody: RequestBody): BaseResponse<Any>
}
- BaseResponse: 数据封装基类
基类字段都是根据后台定义来更改
class BaseResponse<T>:Serializable {
private var message: String? = null
private var code: Int? = null
private var data: T? = null
private var result = false
fun isResult(): Boolean {
return result
}
fun setResult(result: Boolean) {
this.result = result
}
fun getMessage(): String? {
return message
}
fun setMessage(message: String?) {
this.message = message
}
fun getData(): T? {
return data
}
fun setData(data: T) {
this.data = data
}
fun getErrCode(): Int? {
return code
}
fun setErroCode(erroCode: Int?) {
this.code = erroCode
}
override fun toString(): String {
return "BaseResponse{" +
", message='" + message + '\'' +
", code=" + code +
", data=" + data +
", result=" + result +
'}'
}
}
- BaseUrlConstants: BaseUrl 管理类
companion object {
private const val baseUrl1: String = "http://test1/"
private const val baseUrl2: String = "http://test2/"
fun getHost(host: Int): String {
when (host) {
1 -> return baseUrl1
2 -> return baseUrl2
}
return baseUrl1;
}
}
- ExceptionUtil: 异常处理工具类
object ExceptionUtil {
/**
* 未知错误
*/
const val UNKNOWN = 1000
/**
* 解析错误
*/
const val PARSE_ERROR = 1001
/**
* 网络错误
*/
const val NETWORK_ERROR = 1002
/**
* 协议错误
*/
const val HTTP_ERROR = 1003
/**
* 处理异常,toast提示错误信息
*/
fun catchException(e: Throwable) {
e.printStackTrace()
when (e) {
is HttpException -> {
catchHttpException(e.code())
}
is SocketTimeoutException -> {
// showToast(R.string.common_error_net_time_out)
}
is UnknownHostException, is NetworkErrorException -> {
// showToast(R.string.common_error_net)
}
is MalformedJsonException, is JsonSyntaxException -> {
// showToast(R.string.common_error_server_json)
}
is InterruptedIOException -> {
showToast("服务器连接失败,请稍后重试")
}
// 自定义接口异常
is ApiException -> {
showToast(e.message?:"", e.getCode())
}
is ConnectException -> {
showToast( "连接服务器失败" )
}
else -> {
// showToast("${MyApplication.instance.getString(
// R.string.common_error_do_something_fail
// )}:${e::class.java.name}")
}
}
}
/**
* 服务器异常 或 网络通道异常
*
* @param e
* @return
*/
private fun handleException(e: Throwable): ApiException {
val ex: ApiException
return if (e is JsonParseException
|| e is JSONException
|| e is ParseException
) {
//解析错误
ex = ApiException(PARSE_ERROR, e.message)
ex
} else if (e is ConnectException) {
//网络错误
ex = ApiException(
NETWORK_ERROR,
e.message
)
ex
} else if (e is UnknownHostException || e is SocketTimeoutException) {
//连接错误
ex = ApiException(
NETWORK_ERROR,
e.message
)
ex
} else {
//未知错误
ex = ApiException(
UNKNOWN,
e.message
)
ex
}
}
/**
* 处理网络异常
*/
private fun catchHttpException(errorCode: Int) {
if (errorCode in 200 until 300) return// 成功code则不处理
// showToast(
// catchHttpExceptionCode(
// errorCode
// ), errorCode
// )
}
/**
* toast提示
*/
private fun showToast(@StringRes errorMsg: Int, errorCode: Int = -1) {
// showToast(MyApplication.instance.getString(
// errorMsg
// ), errorCode
// )
}
/**
* toast提示
*/
private fun showToast(errorMsg: String, errorCode: Int = -1) {
// if (errorCode == -1) {
// ToastUtils.showShort(errorMsg)
// } else {
// ToastUtils.showShort("$errorCode:$errorMsg")
// }
}
/**
* 处理网络异常
*/
// private fun catchHttpExceptionCode(errorCode: Int): Int = when (errorCode) {
// in 500..600 -> R.string.common_error_server
// in 400 until 500 -> R.string.common_error_request
// else -> R.string.common_error_request
// }
}
- TrustAllCerts: 签名证书校验,这里默认都通过
class TrustAllCerts : X509TrustManager {
override fun checkClientTrusted(p0: Array<out X509Certificate>?, p1: String?) {
}
override fun checkServerTrusted(chain: Array<out X509Certificate>?, p1: String?) {
requireNotNull(chain) { " Check Server x509Certificates is null" }
}
override fun getAcceptedIssuers(): Array<X509Certificate?> {
return arrayOfNulls(0)
}
companion object {
fun createSSLSocketFactory(): SSLSocketFactory? {
var ssfFactory: SSLSocketFactory? = null
try {
val sc = SSLContext.getInstance("TLS")
sc.init(
null, arrayOf(TrustAllCerts()), SecureRandom()
)
ssfFactory = sc.socketFactory
} catch (e: Exception) {
}
return ssfFactory
}
}
class TrustAllHostnameVerifier : HostnameVerifier {
override fun verify(hostname: String, session: SSLSession): Boolean {
return true
}
}
}
以上就是整个结构的封装代码,很多场景可以根据业务情况去调整。下面看看在ViewModel中如何使用
通过协程请求实例
两种情况:1.在viewmodel 中,2.其他地方
一.在viewmodel 中
- 1.首先在BaseViewModel 中使用viewModelScope 封装好请求体。
fun ViewModel.launch(
block: suspend CoroutineScope.() -> Unit,
onError: (e: Throwable) -> Unit = { _: Throwable -> },
onComplete: () -> Unit = {}
) {
viewModelScope.launch(
CoroutineExceptionHandler { _, throwable ->
run {
// 这里统一处理错误
ExceptionUtil.catchException(throwable)
onError(throwable)
}
}
) {
try {
block.invoke(this)
} finally {
onComplete()
}
}
}
- 2.在子viewmodel 也就是业务层中调用如下:
/**
* 登录测试
*/
open fun login() {
launch({
val login = DataService.login(1, "admin", "admin")
if (login.getErrCode() == 200) {
var data = login.getData()
} else {
ApiException(-1, "返回结果出错")
}
}, onError = {
Log.d(TAG, "the error is" + it.message)
})
}
二、在任意地方调用
- 1 在一个类中封装好请求体
companion object {
fun launch(
block: suspend CoroutineScope.() -> Unit,
onError: (e: Throwable) -> Unit = { _: Throwable -> },
onComplete: () -> Unit = {}
) {
GlobalScope.launch(
CoroutineExceptionHandler { _, throwable ->
run {
// 这里统一处理错误
ExceptionUtil.catchException(throwable)
onError(throwable)
}
}
) {
try {
block.invoke(this)
} finally {
onComplete()
}
}
}
}
- 2 调用示例
/**
* 任意地方调用
*/
open fun notViewModelLogin() {
DataService.launch({
val login = DataService.login(1, "admin", "admin")
if (login.getErrCode() == 200) {
var data = login.getData()
} else {
ApiException(-1, "返回结果出错")
}
}, onError = {
Log.d(TAG, "the error is" + it.message)
})
}
以上就是全部的封装过程以及调用示例,Retrofit 的接口通过suspend 后,在协程调用就无需手动切换线程,所以会比Rx订阅方式方便很多。配合整个mvvm 架构去做的话,是非常简洁和方便维护的。代码以及上传到github
https://github.com/ljlstudio/KtMvvm/tree/master/ktmvvm/src/main/java/com/kt/ktmvvm/net