KMM(Kotlin Multiplatform Mobile)入门(七)处理 HTTP 网络请求
背景
与 Server 的数据交互已经成为 App 必不可少的一个重要部分,常用的方式即 HTTP(S),当然也有 WebSocket、TCP、UDP 等等
在 KMM 模块中,为保证双端逻辑一致,且对 JVM、Native 进行统一兼容,可以使用官方推荐的 Ktor 进行网络通信,Kotlinx.Serialization 来进行数据解析
这篇文章就来介绍在 KMM 中如何发起并处理网络请求,后面的文章再详细介绍 kotlinx.serialization 的使用
Ktor 是什么?
Ktor 是由 JetBrains 开发的一套用于解决各类应用中网络连接的框架,不仅可以用在发起请求的各类客户端(不是所谓的 App),还可以构建微服务
针对客户端能力,通过一系列插件,可以支持 HTTP 的各类特性,如:Cookies、重定向、代理、UA、WebSocket 等,在一定程度上,还可以支持一些简单的 TCP 或 UDP 通信
另外,Ktor 还支持为不同的平台配置不同的 HTTP 引擎,如:为 Android 配置 OkHttp 或 HttpURLConnection,为 iOS 配置 NSURLSession,或者为 JVM 配置 Apache HttpClient、为 JavaScript (Node.js) 配置 node-fetch,以便使用同一套代码逻辑处理网络请求
由于现在的 RESTful API 通常会以 JSON 作为通信数据格式,在 JVM 平台上,Ktor 还支持与 Gson、Jackson 协同工作,而对于 Kotlin Multiplatform(当然包括 KMM)可以与 kotlinx.serialization 进行协作
由于 Ktor 适用的平台广泛,本文只对 KMM 平台上的使用进行说明
为 KMM 模块配置 Ktor
如果你使用的 IDE 是 IntelliJ IDEA Ultimate 版本,可以考虑安装 Ktor 插件,但基于 Community 版本的 Android Studio 等 IDE 并不支持该插件,当然它对实际使用影响不大
对于 KMM 模块,首先需要在 Common 的依赖中加入 Ktor 的核心依赖
由于 Ktor 底层依赖协程一些核心功能,同时 Ktor 需要使用基于 Kotlin Native 且实现多线程版本的协程库,所以还需要加入对协程的依赖
1 | // build.gradle.kts |
Android 模块中加入 Ktor Android 端默认引擎(使用 HttpURLConnection)的依赖
1 | // build.gradle.kts |
如果需要使用 OkHttp 来作为 HTTP 能力的引擎,可以使用如下的依赖
1 | // build.gradle.kts |
另外,Android 端也可以使用 CIO(Coroutine(协程) based I/O 实现)引擎,但 CIO 目前还不支持 HTTP/2
对于 iOS,则加入 iOS 的引擎依赖,由于 iOS 的 HTTP 网络请求都是使用 NSURLSession(包括著名的 AFNetworking,NSURLConnection 早已经不用了),所以也就不像 Android 上有多种选择
1 | // build.gradle.kts |
由于 Ktor 是 Kotlin 团队主要负责开发和维护,所以对 Kotlin 相关技术栈支持的比较友好,且部分技术应用的也比较激进,比如 Kotlin Native 的 New Memory Management,所以官方建议大家使用 Kotlin 协程,这就要求在宿主 App(Android 端)中添加协程相关的依赖
1 | dependencies { |
Ktor 已经适配了 New Memory 技术,如果还需要开启 New Memory,则需要根据 New Memory 官方的文档要求,在 gradle.properties 文件中,添加以下的配置项
1 | kotlin.native.binary.memoryModel=experimental |
创建 Ktor 的 HttpClient
Ktor 中的 HttpClient 与其他 HTTP 框架类似,都是对发送和接收网络请求的一系列资源、配置的封装,请求与响应的操作方法,以 Extension 的形式表现,调用也非常简洁
在 Common 代码中,首先需要创建一个 HttpClient 的实例
1 | val httpClient by lazy { HttpClient() } |
如果不需要对 HttpClient 默认的引擎(根据 Gradle 中的依赖自动设置)进行特殊配置,以上代码足矣
为保障多平台的一致,在 Common 中的 HttpClient,对 engine 的可配置项非常有限,只有下面的 Proxy 和线程数量可配,同时可以支持一些公共的请求配置,写在 defaultRequest
闭包中即可,具体内容见下面一节
1 | HttpClient { |
如果需要针对不同的平台和不同的引擎的特性,进行一些自定义配置,则需要用到 expect/actual 的方式来实现 HttpClient
比如在 Android 代码中,针对 OkHttp 进行一些定制
1 | actual val httpClient by lazy { |
或者对 iOS 的 NSURLSession 进行一些配置
1 | actual val httpClient by lazy { |
完成 HttpClient 的创建和配置以后,我们就可以在 Common 目录中的 Kotlin 代码中发起网络请求了
发送一个简单的 HTTP 请求
代码非常简单,只需要一行,但因为 Ktor 中大量使用了协程的开发理念,所以需要符合 Kotlin 协程的基本思想和写法,可以参考:https://kotlinlang.org/docs/coroutines-basics.html
1 | // 写法 1: |
这里需要注意的是,由于 iOS 并不支持协程,所以在 iOS 代码中,如果不使用默认的 CoroutineContext
,则需要使用 GCD
单独实现一个 CoroutineDispatcher
实例并作为 launch
方法的参数传入,如下面代码所示:
1 | internal actual val ApplicationDispatcher: CoroutineDispatcher = NsQueueDispatcher(dispatch_get_main_queue()) |
最后在实际的 Android 和 iOS 工程当中,调用 sendGet()
即可发送网络请求,完成请求之后,别忘记调用 close()
来关闭和释放 HttpClient 实例,以免造成内存泄露
如果 HttpClient 的实例只做一次网络请求,也可以使用 use
语法,在结束时自动进行 close 操作
1 | val status = HttpClient().use { client -> |
由于我们还没有处理网络请求的响应,所以需要使用 Charles 或 Fiddler 抓包才能看到发送的网络请求
自定义请求
众所周知,一条 HTTP 请求报文,包含几个重要部分:Method、Host、Path 及 Query、HTTP 版本、Headers、Body(主要是 POST、PUT)
这些内容,Ktor 也都支持定义,封装在 HttpRequestBuilder
当中,并在 HttpClient
的初始化闭包中的 defaultResult
子闭包,以及 HttpClient
的各个扩展方法中,作为最后一个参数的 Block 参数返回,即:可在 HttpClient.request 或 get、post 等扩展方法调用的后的闭包中操作
如果需要添加统一的公共参数,或者 Headers(包括 Cookies、User-Agent),可以在 HttpClient
初始化时,添加 defaultRequest
闭包,并利用其 HttpRequestBuilder
类型的参数进行配置,这样就是可以使所有使用当前 HttpClient 实例的发送的网络请求,保持统一配置
1 | HttpClient { |
如果只是给某一个请求添加自定义的配置,只需要在 request
方法调用后的闭包中处理即可
1 | fun sendGet() { |
通过 Charles 抓包,就可以看到经过自定义配置后,通过 Ktor 发出的 HTTP 请求
处理响应
和常见的 HTTP 请求框架(如:OkHttp、AFNetworking)类似,Ktor 也支持获取多种类型的返回数据,具体为以下三种:
原始响应 Body:
获取原始的 HTTP 响应体内容,比如 HTML、纯文本字符串、二进制数据等
JSON 对象:
如果响应内容为纯 JSON 字符串,Ktor 可以在返回响应之前直接解析成你需要的对象,但是需要配置 JSON 插件,并结合 kotlinx.serialization 进行使用
流式数据:
如文件下载这种数据量比较大,或是异步、非阻塞式返回形式的数据,可能会用到流式的 HTTP 响应接收模式
下面使用几段示例代码,来实现以上几种响应类型的处理
获取原始类型
- 获取 String 类型(纯文本)的 Body
1 | val httpResponse: HttpResponse = client.get("https://ktor.io/") |
- 获取 ByteArray 类型(二进制)的 Body
1 | val httpResponse: HttpResponse = client.get("https://ktor.io/") |
进行类型自动转换
如果你配置了 Kotlinx.serialization 插件,并且声明了对应数据结构的实体类,则 Ktor 可以自动进行 JSON 解析
首先需要添加 Ktor 用于进行类型转换的依赖,也被称为 ContentNegotiation
1 | implementation("io.ktor:ktor-client-content-negotiation:$ktor_version") |
其次需要添加 Kotlinx.serialization 依赖
1 | implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor_version") |
注意:这里添加以后,会将 Kotlinx.Serialization 相关的依赖传递进来,建议显式指定版本
然后在 HttpClient 初始化的时候,install 这个类型插件 ContentNegotiation
,并把 JSON 插件配置在里面
1 | val client = HttpClient() { |
熟悉 Kotlinx.Serialization 的同学可以使用 Json {}
语法来和直接使用 Kotlin.Serialization 一样进行全局解析配置
这里定义一个和请求结果 JSON 结构一致的 data class,并配置好解析规则
1 |
|
1 | val httpResponse: HttpResponse = client.get("https://api.xxx.com/student?id=xxx") |
流式数据
如果需要下载文件,择需要用到流式数据的形式,来处理 HTTP 响应
1 | val client = HttpClient(CIO) |
Ktor 的其他功能
Server 能力
Ktor 是个很强大的网络库,不但提供了 HTTP 客户端所需要的各种常见功能,也提供了 HTTP Server 的能力,虽不能与 Nginx 这种专业的 HTTP Server 相提并论,但用作测试还是不错的
文档:https://ktor.io/docs/intellij-idea.html
WebSocket
除了常见的 HTTP API,Ktor 对 WebSocket 的支持相当友好,Chat Server,Chat Client
以上关于 Ktor 的介绍就不再详细展开了,有需要的话,可以参考 Ktor 官网的文档:https://ktor.io/docs/welcome.html,内容也十分详细!
KMM 网络能力建设
直接使用 Ktor 建设网络能力,所带来的影响
- 主要优点
- 整体性好,API 统一
- 可借助 Ktor 的所有新增能力
- 友好支持协程、Kotlinx.Serialization 等 Kotlin 工具链
- 没有历史包袱
- 部分缺点
- 无法再利用 App 已有网络组件的能力
- 公共参数、Headers 等需要从 0 开始重新建设
- 在一定程度上导致包体积增大(尤其是 iOS)
- 存在一些不稳定因素(New Memory、协程等)
综合 Ktor 在 KMM 项目中集成的一些优点和缺点,个人认为如果你需要使用 KMM 从零开始开发一个 App,且不太过分在意 iOS 平台的包体积影响,可以优先考虑使用 Ktor,这样 API 和各种网络请求流程会更加统一,也能够结合 Ktor 的各类插件,在一定程度上提升开发效率。
但是如果你需要在原有已经非常成熟的 App 中应用 KMM 技术,重构或新开发某些功能,使用 Ktor 往往不会带来更多的收益。这些 App 大多已经拥有非常完善的网络库了,无论是业务上的公参、统计、异常处理、免流量,还是 HTTP/3、IP 直通、HTTP DNS、SSL 等技术迭代,可谓是遍地开花。所以在这种情况下,个人认为应当尽可能充分地利用现有网络库的能力,在 KMM 层进行 API 和流程的抹平!
推荐的网络能力建设方式
结合实际开发过程中的情况,个人更推荐使用 expect/actual 模式来桥接双端真正的 API。
且由于 HTTP 请求这种业务逻辑,各平台都比较接近,也不存在直接操作 UI 的需求,所以也非常适合使用 KMM 去做逻辑统一。
1 | /** |
如上面的代码片段所示,可以在 KMM 的 commonMain 目录中定义类似的 HTTP 请求接口,后续在 KMM 代码中即可使用该方法发送并处理 HTTP 请求。
但其 actual 的实现应当考虑的相对周全一些,例如:Android 端可以桥接 OkHttp,iOS 端可以桥接 AFNetworking。当然,如果项目中有基于系统或第三方库 API 进行二次开发的网络能力,应当桥接二次开发后的 API。
例如,淘宝客户端内部的 ANetwork 网络框架等等……
以 OkHttp(4.0 以上版本)的基本使用为例,Android 端的 actual 实现可以参考下面的代码:
1 | actual fun commonHttpRequest( |
使用 URLSession 的示例代码:
1 | actual fun commonHttpRequest( |
总结
由于网络请求是业务逻辑代码中使用非常频繁的功能,所以在 KMM 中,建设一套适合项目使用的网络能力尤为重要,需要根据项目实际情况选择合理的实现方案,以便实现网络请求开发的效率最大化。