因为项目都是用的是kotlin了,那么之前那些通过java 线程切换到网络请求就没有在考虑范围内了,那么我们就基于Okhttp3 进行网络访问,因为这个砖主要是使用post请求。所以我们就以post 请求为例。
导入权限声明
ini
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
配置http访问
我们在res 的xml 中新建一个xml。
xml
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true" />
</network-security-config>
然后再application 中使用:
ini
android:networkSecurityConfig="@xml/network_security_config"
至于为啥要写这个,因为7.0以上默认不支持http 明文访问。
导入okhttp 包
kotlin
// okhttp3网络库 https://square.github.io/okhttp/
const val okhttp3 = "com.squareup.okhttp3:okhttp:4.9.3"
const val okhttp3LoggingInterceptor = "com.squareup.okhttp3:logging-interceptor:4.9.3"
const val okhttp3UrlConnection = "com.squareup.okhttp3:okhttp-urlconnection:4.9.3"
const val cronetOkhttp = "com.google.net.cronet:cronet-okhttp:0.1.0"
信任所有的证书及其连接对象的创建
这个其实不建议写信任所有的https 证书来着,但是不得不写。
首先我们创建一个类。
kotlin
class TrustAllCertsManager: X509TrustManager {
override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) {
// 不做任何检查,因此所有客户端证书都会被信任
}
override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?) {
// 不做任何检查,因此所有服务器证书都会被信任
}
override fun getAcceptedIssuers(): Array<X509Certificate> {
return arrayOf()
}
}
在连接对象中设置:
scss
val okHttpClient: OkHttpClient = OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.writeTimeout(15, TimeUnit.SECONDS)
.sslSocketFactory(SSLContext.getInstance("TLS").apply {
init(null, arrayOf<TrustManager>(TrustAllCertsManager()), SecureRandom())
}.socketFactory, TrustAllCertsManager())
.build()
log拦截器
kotlin
class LoggerInterceptor(private val logger: (String) -> Unit) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val logStr = StringBuffer()
logStr.append(" \n")
// 请求方法
logStr.append(request.method)
logStr.append(": ")
// 请求链接
logStr.append(request.url)
logStr.append("\n")
// 请求头
logStr.append("请求头:\n")
val requestHeaderMap = mutableMapOf<String, String>()
request.headers.names().forEach {
requestHeaderMap[it] = request.header(it).toString()
}
val requestHeadersStr = requestHeaderMap.toJsonString()
logStr.append(requestHeadersStr)
logStr.append("\n")
// 请求体
logStr.append("请求体:\n")
val requestBodyStr = when (val requestBody = request.body) {
is ProgressRequestBody -> {
parseMultipartBody(requestBody.multipartBody)
}
else -> {
val requestBodyBuffer = Buffer().also { requestBody?.writeTo(it) }
val requestBodyContent = requestBodyBuffer.readByteArray()
val requestCharset = requestBody?.contentType()?.charset() ?: Charsets.UTF_8
if (requestBodyBuffer.isProbablyUtf8()) {
requestBodyContent.toString(requestCharset)
} else {
"二进制请求体, 忽略"
}
}
}
logStr.append(requestBodyStr)
logStr.append("\n")
val response = try {
chain.proceed(request)
} catch (e: Exception) {
logStr.append("网络请求失败: ${e.message} ")
logger.invoke(logStr.toString())
throw e
}
// 响应头
logStr.append("响应头:\n")
val responseHeaderMap = mutableMapOf<String, String>()
response.headers.names().forEach {
responseHeaderMap[it] = response.header(it).toString()
}
val responseHeadersStr = responseHeaderMap.toJsonString()
logStr.append(responseHeadersStr)
logStr.append("\n")
val code = response.code
val message = response.message
if (response.isSuccessful.not()) {
// 失败
val duration = response.receivedResponseAtMillis - response.sentRequestAtMillis
logStr.append("网络请求失败: $code > $message 耗时: ${duration}ms")
} else {
// 成功
// 响应体
logStr.append("响应体:\n")
val responseBody = response.body!!
val contentType = responseBody.contentType()
val subtype = contentType?.subtype
val responseCharset = contentType?.charset() ?: Charsets.UTF_8
val isFileDownload = subtype != "json"
val responseBodyStr = if (isFileDownload) {
"${subtype}文件响应体,忽略"
} else {
val responseBodySource = responseBody.source()
responseBodySource.request(Long.MAX_VALUE)
var responseBuffer = responseBodySource.buffer
if ("gzip".equals(response.header("Content-Encoding"), true)) {
GzipSource(responseBuffer.clone()).use { gzippedResponseBody ->
responseBuffer = Buffer()
responseBuffer.writeAll(gzippedResponseBody)
}
}
when {
responseBuffer.isProbablyUtf8().not() -> {
"二进制响应体, 忽略"
}
else -> {
responseBuffer.clone().readString(responseCharset)
}
}
}
logStr.append(responseBodyStr)
logStr.append("\n")
// 完成
val duration = response.receivedResponseAtMillis - response.sentRequestAtMillis
logStr.append("网络请求完成: 状态码: $code > $message 耗时: ${duration}ms")
}
logger.invoke(logStr.toString())
return response
}
private fun parseMultipartBody(body: MultipartBody): String {
return body.parts.fold("") { acc, part ->
acc + parsePartHeaders(part.headers) + "; " + parsePartBody(part.body) + "\n"
}.trimEnd()
}
private fun parsePartHeaders(headers: Headers?): String {
return "headers: " + headers?.toMap().toString()
}
private fun parsePartBody(body: RequestBody): String {
val contentType = body.contentType()
val contentLength = body.contentLength()
val mediaType = "${contentType?.type}/${contentType?.subtype}"
val size = getFormatSize(contentLength)
return "body: " + if (contentType == null) {
val bodyBuffer = Buffer().also { body.writeTo(it) }
bodyBuffer.readByteArray().toString(Charsets.UTF_8)
} else {
"文件二进制数据流(mediaType:$mediaType, size:$size)"
}
}
private fun Buffer.isProbablyUtf8(): Boolean {
try {
val prefix = Buffer()
val byteCount = size.coerceAtMost(64)
copyTo(prefix, 0, byteCount)
for (i in 0 until 16) {
if (prefix.exhausted()) {
break
}
val codePoint = prefix.readUtf8CodePoint()
if (Character.isISOControl(codePoint) && !Character.isWhitespace(codePoint)) {
return false
}
}
return true
} catch (_: EOFException) {
return false // Truncated UTF-8 sequence.
}
}
}
然后使用的时候,配合log工具类,在合适的时候把log打印关闭了即可。
使用Chucker 做APP网络访问的检查
我们通过log拦截器其实已经实现便于调试的功能。但是这里还是推荐接入类似于Chucker 的工具。
chucker网络检查 里面可以输出网络的入参和返回参数啥的。
导入maven
scss
// https://github.com/ChuckerTeam/chucker 用于记录网络层的交互。
debugImplementation("com.github.chuckerteam.chucker:library:4.0.0")
releaseImplementation("com.github.chuckerteam.chucker:library-no-op:4.0.0")
设置拦截器:
scss
val okHttpClient: OkHttpClient = OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.writeTimeout(15, TimeUnit.SECONDS)
.addInterceptor(ChuckerInterceptor(Utils.getApp()))
.sslSocketFactory(SSLContext.getInstance("TLS").apply {
init(null, arrayOf<TrustManager>(TrustAllCertsManager()), SecureRandom())
}.socketFactory, TrustAllCertsManager())
.build()
发送一个post 请求
kotlin
suspend inline fun postSting(
url: String,
params: Map<String, Any?> = mapOf(),
headers: Map<String, String> = emptyMap()
): String {
val allParams = params
val body =
(allParams)
.filterValues { value -> value != null }
.toJsonString()
.toRequestBody(MEDIA_TYPE_JSON)
// 发起请求
val request = Request.Builder()
.url(url)
.post(body)
.headers(headers.toHeaders())
.build()
val response = okHttpClient.newCall(request).execute()
if (response.isSuccessful) {
response.body?.let {
return it.string()
}
throw HttpException(response)
} else {
throw HttpException(response)
}
}
可以看到,我们这是同步请求,当response.body 是空的时候就抛出一个异常。这里还是不建议用inline 关键字标记函数,因为我们网络层只是做了数据的同步获取。
Android viewModel中进行网络请求
kotlin
inline fun <reified T : Parcelable> getNetLiveData(
methodPath: String, params: Map<String, Any>,
liveData: MutableLiveData<Result<T>>,
) {
// 拦截异常。
val liveHandler = CoroutineExceptionHandler { _, e ->
// 处理异常
liveData.postValue(Result.failure(e))
}
viewModelScope.launch(Dispatchers.IO + liveHandler) {
val json = netService().post(appConfig().appHost() + methodPath, params)
val data = // json 转换为数据模型
liveData.postValue(Result.success(data))
}
}
我们使用了 CoroutineExceptionHandler 处理协程中的异常,而没有采用try... 这种方式。我们直接AI对于两种方式的看法:
CoroutineExceptionHandler
和 try
处理异常的优缺点
在 Kotlin 中,使用 CoroutineExceptionHandler
和 try
处理异常都有各自的优缺点。
使用 CoroutineExceptionHandler
的优点:
- 简洁性:使用
CoroutineExceptionHandler
可以使代码更加简洁,不需要在每个异步代码块中都使用try-catch
语句。 - 可读性:通过使用
CoroutineExceptionHandler
,可以将异常处理逻辑集中在一个地方,而不是分散在多个try-catch
语句中,提高了代码的可读性。 - 异常处理灵活性:
CoroutineExceptionHandler
允许你注册自定义的异常处理逻辑,可以在特定的情况下捕获和处理异常。
使用 CoroutineExceptionHandler
的缺点:
- 控制流复杂性:如果使用了多个协程,并且每个协程都有自己的异常处理逻辑,这可能会导致控制流变得复杂,难以管理。
- 无法捕获所有异常:
CoroutineExceptionHandler
只能捕获那些被协程挂起并传递给它的异常。对于那些没有挂起或未被传递给协程的异常,可能无法被捕获和处理。
使用 try
处理异常的优点:
- 全面性:使用
try
可以捕获到尽可能多的异常,包括那些未被协程挂起或未被传递给协程的异常。 - 传统性:对于那些习惯了使用
try-catch
语句进行异常处理的开发者来说,这种方式可能更加直观和熟悉。
使用 try
处理异常的缺点:
- 冗余代码:在每个异步代码块中都需要编写
try-catch
语句,可能导致代码重复且冗余。 - 可读性和可维护性:大量的
try-catch
语句可能会使代码变得难以阅读和维护。 - 异常处理逻辑分散:每个
try-catch
语句都可能有自己的异常处理逻辑,这可能会导致整个应用程序的异常处理逻辑变得分散。
综上所述,选择使用 CoroutineExceptionHandler
还是 try
处理异常取决于具体的应用场景和需求。如果是在协程中执行异步操作,并且希望简化代码和提高可读性,那么使用 CoroutineExceptionHandler
可能是一个更好的选择。如果需要在传统同步代码块中捕获和处理异常,或者需要确保能够捕获尽可能多的异常,那么使用 try
语句可能更适合。
如何将网络IO协程抽离
aidl
fun switchIO(
handler: CoroutineExceptionHandler,
block: suspend CoroutineScope.() -> Unit
): Job {
return viewModelScope.launch(Dispatchers.IO + handler) {
block()
}
}
json 解析
目前在kotlin 中是推荐使用 moshi。
总结
整体上的思路来说,就是网络层只是处理网络请求,包括拦截器、一些必须要的参数的封装。而参数的解析就全在viewModel 中通过IO 协程中进行处理。 个人感觉这种写法的好处是便于后期网络请求库的更换,因为他解析缓存协程调度是没有在一起的,当我们使用 viewModelScope 创建协程的时候,当viewModel 被销毁的时候,他的协程及其子协程都会被取消。 所以,这么写感觉还行,当然还有一个问题,如果说一个网络请求不依托于界面呢?那么我们将真正请求、解析、缓存抽离出来之后,我们其实更好的拼出来吧。
缺点,估计在于自己对于协程错误的认知上面。同时因为分散的厉害,所以说,自己组合还是蛮麻烦的。
后期发现问题了,再更正吧。