Kuikly多端框架(KMP)实战:KMP中的 Ktor 网络库的多端适配指南

在当今追求高效开发的移动端领域,Kotlin Multiplatform (KMP) 已成为一套不可忽视的"代码共享"解决方案。 它允许我们在 iOS、Android、JS、Desktop 等多个平台之间共享业务逻辑,同时保留与原生 UI 和 API 的无缝交互能力。网络请求作为任何现代应用的命脉, 自然是 KMP 中最需要优先实现共享的核心模块之一。

本文将带你从零到一,深入探索如何在 KMP 项目中,利用 JetBrains 官方出品的网络库 Ktor,构建一个优雅、可维护且高性能的跨平台网络层。 我们将覆盖从项目配置、 核心机制 expect/actual 的应用,到为 Android、iOS、JS 各平台提供最佳引擎适配的全过程。

该项目案例的开源地址https://gitcode.com/qq8864/kuiklytest

一、为什么选择 Ktor?

在现代网络编程中,Ktor是一个高性能且易于使用的框架,它提供了对异步编程、WebSockets、HTTP客户端和服务器等特性的原生支持。Ktor是使用Kotlin语言编写的,充分利用了Kotlin的协程特性来简化异步编程。

ktor官网https://ktor.io/
ktor网络库的github仓https://github.com/ktorio/ktor

在 KMP 的世界里,选择一个原生支持多平台的网络库至关重要。 Ktor (Kotlin HTTP Client) 无疑是最佳选择,原因如下:

  • 官方出品,原生支持:由 JetBrains 亲自打造,为 Kotlin 而生,与 Kotlin 协程无缝集成,完美契合 KMP 的开发哲学。
  • 轻量且可扩展:Ktor 遵循"插件化"设计。核心库非常小,你可以按需安装 Logging(日志)、ContentNegotiation(内容协商/JSON解析)、HttpTimeout(超时)等功能插件。
  • 平台引擎适配:Ktor 的一大亮点是其可插拔的引擎(Engine) 机制。 它允许我们在共享代码中编写统一的网络请求逻辑, 同时在不同平台上使用该平台最高效的底层实现( 如 Android 的 OkHttp,iOS 的 URLSession/NSURLSession, JS 的 fetch API)。

二、KMP 的核心魔法:expect 与 actual

在深入 Ktor 之前,我们必须理解 KMP 实现平台差异化的核心机制:expect 和 actual 关键字。

你可以将其理解为一种编译时多态:

  • expect (期望):在 commonMain(共享代码源集)中,我们使用 expect 关键字来声明一个契约。这可以是一个类、一个对象、一个函数或一个属性。 它像一个抽象的" 插座" , 告诉编译器: " 我期望在最终的平台代码中, 有人会提供这个功能的具体实现。 "
  • actual (实际):在平台特定的源集(如 androidMain, iosMain, jsMain)中,我们使用 actual 关键字来提供这个契约的具体实现。它就像一个针对特定平台的"插头",会被编译器在编译该平台时自动" 插入" 到 expect 定义的"插座"中。

这个机制让我们的共享代码可以依赖于一个抽象的接口, 而将具体的、 依赖于平台 SDK 的实现细节隔离在各自的平台模块中。

三、实战:构建跨平台的 HttpClient

我们的目标是创建一个 HttpClientFactory 对象,它在任何平台都能被调用,但内部会根据当前平台自动使用最佳的网络引擎。

第 1 步:在 commonMain 中定义期望 (expect)

首先,在 commonMain 中定义我们的"插座"。文件: src/commonMain/kotlin/com/example/myapp/base/HttpClientFactory.kt

kotlin 复制代码
package com.example.myapp.base

import io.ktor.client.*

// 定义一个期望的工厂对象
expect object HttpClientFactory {
    fun create(): HttpClient
}

这行代码非常简洁,它只做了一件事:承诺在未来的某个地方, 会有一个名为 HttpClientFactory 的对象,并且它有一个返回 HttpClient 的 create 方法。

第 2 步:配置 Gradle 依赖 (build.gradle.kts)

接下来,我们需要为 Ktor 核心库以及各个平台的引擎添加依赖。

bash 复制代码
// build.gradle.kts

kotlin {
    // 1. 定义你的编译目标
    androidTarget("android")
    iosX64("ios")
    js(IR) { browser() }

    sourceSets {
        // 2. 在 commonMain 中添加核心依赖
        val commonMain by getting {
            dependencies {
                // Ktor 核心库
                implementation("io.ktor:ktor-client-core:2.3.9")
                // Ktor 内容协商插件
                implementation("io.ktor:ktor-client-content-negotiation:2.3.9")
                // Kotlinx Serialization 用于 JSON 解析
                implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.9")
                // Ktor 日志插件
                implementation("io.ktor:ktor-client-logging:2.3.9")
            }
        }

        // 3. 在 androidMain 中添加 Android 引擎 (OkHttp)
        val androidMain by getting {
            dependencies {
                implementation("io.ktor:ktor-client-okhttp:2.3.9")
            }
        }

        // 4. 在 iosMain 中添加 iOS 引擎 (Darwin)
        val iosMain by getting {
            dependencies {
                implementation("io.ktor:ktor-client-darwin:2.3.9")
            }
        }

        // 5. 在 jsMain 中添加 JS 引擎 (Js)
        val jsMain by getting {
            dependencies {
                implementation("io.ktor:ktor-client-js:2.3.9")
            }
        }
    }
}

注意:请确保所有 io.ktor:* 依赖的版本号保持完全一致,以避免版本冲突。

第 3 步:在各平台提供实际 (actual) 实现

现在,我们来为每个平台制作专属的"插头" 。

Android 实现 (androidMain) 文件: src/androidMain/kotlin/com/example/myapp/base/HttpClientFactory.kt

kotlin 复制代码
package com.example.kuiklytest.base

import io.ktor.client.HttpClient

// 定义一个期望的工厂对象

import io.ktor.client.*
import io.ktor.client.engine.okhttp.OkHttp // 导入 Android 平台的引擎
import io.ktor.client.plugins.HttpTimeout
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.logging.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json

// `actual` 关键字表示这是对 `expect` 的具体实现
actual object HttpClientFactory {
    actual fun create(): HttpClient {
        return HttpClient(OkHttp) { // 使用 OkHttp 引擎
            // 所有通用的插件配置都放在这里
            install(Logging) {
                level = LogLevel.ALL
            }
            install(ContentNegotiation) {
                json(Json {
                    prettyPrint = true
                    isLenient = true
                    ignoreUnknownKeys = true
                })
            }
            install(HttpTimeout) {
                requestTimeoutMillis = 15000
                connectTimeoutMillis = 15000
            }
        }
    }
}

iOS 实现 (iosMain) 文件: src/iosMain/kotlin/com/example/myapp/base/HttpClientFactory.kt

kotlin 复制代码
package com.example.kuiklytest.base

import io.ktor.client.*
import io.ktor.client.engine.darwin.Darwin // 导入 iOS 平台的引擎
import io.ktor.client.plugins.HttpTimeout
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.logging.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json

// iOS 平台的 `actual` 实现
actual object HttpClientFactory {
    actual fun create(): HttpClient {
        return HttpClient(Darwin) { // 使用 Darwin 引擎
            // 这里的配置和 Android 的完全一样,可以考虑抽取成一个通用函数
            install(Logging) {
                level = LogLevel.ALL
            }
            install(ContentNegotiation) {
                json(Json {
                    prettyPrint = true
                    isLenient = true
                    ignoreUnknownKeys = true
                })
            }
            install(HttpTimeout) {
                requestTimeoutMillis = 15000
                connectTimeoutMillis = 15000
            }
        }
    }
}

JavaScript 实现 (jsMain) 文件: src/jsMain/kotlin/com/example/myapp/base/HttpClientFactory.kt

kotlin 复制代码
package com.example.kuiklytest.base

import io.ktor.client.*
// 1. 导入 JS 平台的引擎
import io.ktor.client.engine.js.*
import io.ktor.client.plugins.HttpTimeout
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.logging.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json

// JS 平台的 `actual` 实现
actual object HttpClientFactory {
    actual fun create(): HttpClient {
        // 2. 在构造函数中传入 Js 引擎
        return HttpClient(Js) {
            // 所有通用的插件配置都和 Android/iOS 完全一样
            install(Logging) {
                level = LogLevel.ALL
            }
            install(ContentNegotiation) {
                json(Json {
                    prettyPrint = true
                    isLenient = true
                    ignoreUnknownKeys = true
                })
            }
            install(HttpTimeout) {
                requestTimeoutMillis = 15000
                connectTimeoutMillis = 15000
            }

            // 3. JS 引擎特有的配置 (可选)
            engine {
                // 例如,可以配置 fetch API 的凭证策略
                // cors.credentials = true
            }
        }
    }
}

第 4 步:抽取通用配置,拒绝重复代码 (DRY)

你会发现,所有平台的 HttpClient 配置(如日志、JSON、超时)都是重复的。 我们可以利用 Kotlin 的扩展函数,在 commonMain 中创建一个通用配置函数。

文件: src/commonMain/kotlin/com/example/myapp/base/HttpClientFactory.kt

kotlin 复制代码
// ... (expect object 定义之后)

import io.ktor.client.plugins.HttpTimeout
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.logging.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json

// 定义一个 internal 的通用配置函数
internal fun HttpClientConfig<*>.applyCommonConfig() {
    // 安装日志插件
    install(Logging) {
        level = LogLevel.ALL // 打印所有日志,便于调试
    }
    // 安装内容协商插件,用于自动序列化/反序列化 JSON
    install(ContentNegotiation) {
        json(Json {
            prettyPrint = true      // 格式化输出 JSON
            isLenient = true        // 宽松模式
            ignoreUnknownKeys = true // 忽略 JSON 中未知字段
        })
    }
    // 安装超时插件
    install(HttpTimeout) {
        requestTimeoutMillis = 15000
        connectTimeoutMillis = 15000
    }
}

现在,我们的 actual 实现可以变得极其简洁:

kotlin 复制代码
// 例如,在 androidMain 中
actual object HttpClientFactory {
    actual fun create(): HttpClient {
        return HttpClient(OkHttp) {
            applyCommonConfig() // 一行代码,搞定所有通用配置!
        }
    }
}

四、在共享代码中使用

我们已经搭建好了跨平台的网络客户端,现在来看看如何在共享代码中消费 API。假设我们需要获取一个轮播图数据列表。

  1. 定义数据模型 (Data Class)
    首先,在 commonMain 中定义与 API 响应对应的 Kotlin 数据类,并使用 @Serializable 注解标记它们。 文件: src/commonMain/kotlin/com/example/myapp/common/types/Models.kt
kotlin 复制代码
import kotlinx.serialization.Serializable

@Serializable
data class ApiResponse<T>(
    val code: Int,
    val message: String,
    val data: T
)

@Serializable
data class SwiperItem(
    val id: String,
    val imageUrl: String,
    val title: String
)
  1. 封装 API 调用
    创建一个专门的 Api 类来封装所有与特定功能相关的网络请求。这是一种良好的实践, 能让代码结构更清晰。 文件: src/commonMain/kotlin/com/example/myapp/common/api/SwiperApi.kt
kotlin 复制代码
package com.example.myapp.common.api

import com.example.myapp.base.HttpClientFactory
import com.example.myapp.common.types.ApiResponse
import com.example.myapp.common.types.SwiperItem
import io.ktor.client.call.*
import io.ktor.client.request.*

class SwiperApi {

    // 通过我们创建的工厂来获取 HttpClient 实例
    private val httpClient = HttpClientFactory.create()
    private val baseUrl = "https://api.example.com" // 你的 API 基地址

    /**
     * 获取轮播图数据。
     * 这是一个挂起函数 (suspend fun),必须在协程中调用。
     */
    suspend fun getSwiperData(): ApiResponse<List<SwiperItem>> {
        // 使用 Ktor 发起 GET 请求
        val response = httpClient.get("$baseUrl/swiperdata")
        
        // Ktor 会根据 Content-Type 和 ContentNegotiation 配置,
        // 自动将响应体 JSON 解析为你指定的泛型类型。
        return response.body()
    }
}
  1. 在 ViewModel 或 Repository 中调用
    最后,在业务逻辑层(如 ViewModel)中,注入并调用 SwiperApi。
kotlin 复制代码
class HomeViewModel(private val swiperApi: SwiperApi) : ViewModel() {

    fun loadData() {
        viewModelScope.launch { // 在 ViewModel 的协程作用域中启动一个新协程
            try {
                val swiperResponse = swiperApi.getSwiperData()
                if (swiperResponse.code == 0) {
                    // 更新 UI 状态为成功
                    _uiState.value = HomeUiState.Success(swiperResponse.data)
                } else {
                    // 更新 UI 状态为失败
                    _uiState.value = HomeUiState.Error(swiperResponse.message)
                }
            } catch (e: Exception) {
                // 处理网络异常或解析异常
                _uiState.value = HomeUiState.Error("网络错误: ${e.message}")
            }
        }
    }
}

编译器会像施展魔法一样,在编译时自动将 HttpClientFactory.create() 替换为对应平台的正确实现。

总结

通过 expect/actual 机制与 Ktor 的引擎适配能力,我们成功构建了一个真正意义上的跨平台网络层。 这套实践不仅让我们的代码库保持了极高的共享率和简洁性, 还确保了在每个平台上都能发挥出最佳的原生性能。 从定义数据模型,到封装 API,再到在 ViewModel 中调用,整个流程都在 commonMain 中完成,实现了 100% 的逻辑共享。这正是 KMP 开发的魅力所在。掌握它,你便掌握了开启 KMP 高效开发大门的钥匙。

参考链接

https://cloud.tencent.com/developer/article/2425085

https://article.juejin.cn/post/7142887007465766925

https://cloud.tencent.com/developer/article/1670592

相关推荐
那就回到过去2 小时前
拥塞管理和拥塞避免
运维·服务器·网络·网络协议·tcp/ip·ensp
乐悲蔚蓝湖2 小时前
华三做流量统计
网络
sdff113962 小时前
【HarmonyOS】Flutter适配鸿蒙多屏异构UI开发实战指南
flutter·ui·harmonyos
米羊1212 小时前
风险评估文档记录
开发语言·网络·php
2301_796512523 小时前
【精通篇】打造React Native鸿蒙跨平台开发高级复合组件库开发系列:Swipe 轮播(用于循环播放一组图片或内容)
javascript·react native·react.js·ecmascript·harmonyos
切糕师学AI3 小时前
NFS(网络文件系统)详解
开发语言·网络·php
学习3人组3 小时前
_cdecl_stdcall_fastcall 三种函数调用约定
网络
熊猫钓鱼>_>3 小时前
【开源鸿蒙跨平台开发先锋训练营】React Native 工程化实践:Hooks 封装与跨端 API 归一化
react native·react.js·华为·开源·harmonyos·鸿蒙·openharmony
星空22234 小时前
【HarmonyOS】day28:React Native 实战:精准控制 Popover 弹出位置
react native·华为·harmonyos