从0开始搭建一个APP:(3)应用的网络层与解析

因为项目都是用的是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对于两种方式的看法:

CoroutineExceptionHandlertry 处理异常的优缺点

在 Kotlin 中,使用 CoroutineExceptionHandlertry 处理异常都有各自的优缺点。

使用 CoroutineExceptionHandler 的优点:

  1. 简洁性:使用 CoroutineExceptionHandler 可以使代码更加简洁,不需要在每个异步代码块中都使用 try-catch 语句。
  2. 可读性:通过使用 CoroutineExceptionHandler,可以将异常处理逻辑集中在一个地方,而不是分散在多个 try-catch 语句中,提高了代码的可读性。
  3. 异常处理灵活性:CoroutineExceptionHandler 允许你注册自定义的异常处理逻辑,可以在特定的情况下捕获和处理异常。

使用 CoroutineExceptionHandler 的缺点:

  1. 控制流复杂性:如果使用了多个协程,并且每个协程都有自己的异常处理逻辑,这可能会导致控制流变得复杂,难以管理。
  2. 无法捕获所有异常:CoroutineExceptionHandler 只能捕获那些被协程挂起并传递给它的异常。对于那些没有挂起或未被传递给协程的异常,可能无法被捕获和处理。

使用 try 处理异常的优点:

  1. 全面性:使用 try 可以捕获到尽可能多的异常,包括那些未被协程挂起或未被传递给协程的异常。
  2. 传统性:对于那些习惯了使用 try-catch 语句进行异常处理的开发者来说,这种方式可能更加直观和熟悉。

使用 try 处理异常的缺点:

  1. 冗余代码:在每个异步代码块中都需要编写 try-catch 语句,可能导致代码重复且冗余。
  2. 可读性和可维护性:大量的 try-catch 语句可能会使代码变得难以阅读和维护。
  3. 异常处理逻辑分散:每个 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 被销毁的时候,他的协程及其子协程都会被取消。 所以,这么写感觉还行,当然还有一个问题,如果说一个网络请求不依托于界面呢?那么我们将真正请求、解析、缓存抽离出来之后,我们其实更好的拼出来吧。

缺点,估计在于自己对于协程错误的认知上面。同时因为分散的厉害,所以说,自己组合还是蛮麻烦的。

后期发现问题了,再更正吧。

相关推荐
落落落sss1 小时前
MybatisPlus
android·java·开发语言·spring·tomcat·rabbitmq·mybatis
代码敲上天.2 小时前
数据库语句优化
android·数据库·adb
GEEKVIP4 小时前
手机使用技巧:8 个 Android 锁屏移除工具 [解锁 Android]
android·macos·ios·智能手机·电脑·手机·iphone
model20056 小时前
android + tflite 分类APP开发-2
android·分类·tflite
彭于晏6896 小时前
Android广播
android·java·开发语言
与衫7 小时前
掌握嵌套子查询:复杂 SQL 中 * 列的准确表列关系
android·javascript·sql
500了13 小时前
Kotlin基本知识
android·开发语言·kotlin
人工智能的苟富贵14 小时前
Android Debug Bridge(ADB)完全指南
android·adb
小雨cc5566ru19 小时前
uniapp+Android面向网络学习的时间管理工具软件 微信小程序
android·微信小程序·uni-app
bianshaopeng20 小时前
android 原生加载pdf
android·pdf