KMP 结构适配鸿蒙:网络通信与 HTTP 客户端
KMP项目通过共享代码实现跨平台网络通信,减少重复开发。核心设计包括:1)定义统一的HttpClient接口,使用suspend函数实现异步请求;2)在共享层实现业务逻辑,通过依赖注入使用平台特定实现;3)各平台分别实现接口:Android使用OkHttp,鸿蒙使用HttpURLConnection,Web使用Fetch API。这种方式使业务逻辑只需编写一次,便于维护和测试,同时保持各平台特性。
项目概述
在现代应用开发中,网络通信是必不可少的功能。无论是获取用户数据、上传文件还是同步状态,所有这些操作都需要通过网络进行。在传统的多平台开发中,开发者需要在 Android、iOS 和鸿蒙等不同平台上分别实现网络通信逻辑,这导致了大量的代码重复和维护困难。
KMP 提供了一个优雅的解决方案:我们可以在共享代码中定义网络通信的核心逻辑,然后在各个平台上提供特定的 HTTP 客户端实现。这样做的好处是显而易见的。首先,业务逻辑只需要编写一次,所有平台都能使用,大大减少了代码重复。其次,当需要修改业务逻辑时,只需要修改共享代码,所有平台都会自动获得更新。最后,这种架构使得代码更容易测试,因为我们可以在共享代码中进行单元测试,而不需要依赖特定平台的实现。
本文将详细介绍如何在 KMP 项目中实现网络通信。我们将从设计共享的网络接口开始,然后展示如何在 Android、JVM(鸿蒙)和 JavaScript 平台上实现这些接口。最后,我们将展示编译后的代码在不同平台上的样子,以及如何在鸿蒙应用中使用这些共享代码。
第一部分:共享网络层设计的核心概念
为什么需要抽象网络层
在开始编写代码之前,我们需要理解为什么要抽象网络层。想象一个场景:你的应用需要从服务器获取用户信息。在没有抽象的情况下,你可能需要在 Android 代码中使用 OkHttp,在 iOS 中使用 URLSession,在鸿蒙中使用 HttpURLConnection。这样做会导致三套不同的代码,每套代码都有自己的错误处理、重试逻辑和缓存机制。当你需要修改重试策略时,你必须在三个地方都进行修改,这很容易出错。
通过抽象网络层,我们可以将这些平台特定的实现隐藏起来。应用代码只需要知道"我需要获取用户信息",而不需要知道具体如何进行网络请求。这种分离使得代码更加模块化、可维护和可测试。
定义网络请求接口
在 KMP 中,我们使用接口来定义网络请求的行为。这个接口定义了所有平台都需要支持的操作,例如 GET、POST、PUT 和 DELETE。
// commonMain/kotlin/com/example/kmp/network/HttpClient.kt
interface HttpClientInterface {
suspend fun get(url: String): String
suspend fun post(url: String, body: String): String
suspend fun put(url: String, body: String): String
suspend fun delete(url: String): String
}
这个接口非常简洁,但功能强大。每个方法都使用了 suspend 关键字,这表示这些是异步操作。在 Kotlin 中,suspend 函数是协程的基础,它允许我们以同步的方式编写异步代码,这大大提高了代码的可读性。
定义数据模型
接下来,我们需要定义数据模型来表示网络请求和响应。
@Serializable
data class UserDto(
val id: String,
val name: String,
val email: String
)
@Serializable
data class ApiResponse<T>(
val code: Int,
val message: String,
val data: T?
)
使用 @Serializable 注解使得这些数据类可以自动序列化和反序列化为 JSON。这是 Kotlin 序列化库提供的功能,它为我们节省了大量的手动编写序列化代码的工作。
实现业务逻辑层
现在我们创建一个 NetworkManager 类,它使用 HttpClientInterface 来实现具体的业务逻辑。
class NetworkManager(private val httpClient: HttpClientInterface) {
suspend fun fetchUser(userId: String): UserDto? {
return try {
val response = httpClient.get("https://api.example.com/users/$userId")
parseResponse<UserDto>(response)
} catch (e: Exception) {
null
}
}
}
这个类的关键特点是它不依赖于任何具体的 HTTP 客户端实现。它只知道 HttpClientInterface 接口。这意味着我们可以在不修改 NetworkManager 的情况下,为不同的平台提供不同的 HTTP 客户端实现。这就是依赖注入的力量。
第二部分:平台特定实现的设计
Android 平台的网络实现
在 Android 平台上,我们使用 OkHttp 库来实现 HTTP 客户端。OkHttp 是 Android 开发中最流行的 HTTP 客户端库,它提供了许多高级功能,如连接池、请求重试和拦截器。
// androidMain/kotlin/com/example/kmp/network/AndroidHttpClient.kt
actual class AndroidHttpClient : HttpClientInterface {
private val client = OkHttpClient()
actual override suspend fun get(url: String): String =
withContext(Dispatchers.IO) {
// 使用 OkHttp 发送 GET 请求
val request = Request.Builder().url(url).get().build()
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) throw Exception("HTTP ${response.code}")
response.body?.string() ?: ""
}
}
}
这个实现的关键点是使用 withContext(Dispatchers.IO)。这确保了网络请求在 IO 线程中执行,而不是在主线程上。这是 Android 开发中的最佳实践,因为在主线程上执行网络请求会导致应用卡顿。
JVM 平台的网络实现(鸿蒙)
在鸿蒙系统上,我们使用 Java 标准库中的 HttpURLConnection 来实现 HTTP 客户端。虽然 HttpURLConnection 不如 OkHttp 功能丰富,但它是 Java 标准库的一部分,不需要额外的依赖。
// jvmMain/kotlin/com/example/kmp/network/JvmHttpClient.kt
actual class JvmHttpClient : HttpClientInterface {
actual override suspend fun get(url: String): String =
withContext(Dispatchers.IO) {
// 使用 HttpURLConnection 发送 GET 请求
val connection = URL(url).openConnection() as HttpURLConnection
connection.requestMethod = "GET"
try {
if (connection.responseCode == HttpURLConnection.HTTP_OK) {
connection.inputStream.bufferedReader().use { it.readText() }
} else {
throw Exception("HTTP ${connection.responseCode}")
}
} finally {
connection.disconnect()
}
}
}
这个实现展示了如何使用 Java 标准库来实现网络请求。虽然代码看起来更复杂(需要手动管理连接的生命周期),但它完全兼容鸿蒙系统。
第三部分:编译后的代码形式
Kotlin 如何编译为 JavaScript
当我们将 Kotlin 代码编译为 JavaScript 时,编译器会将 Kotlin 的语法和语义转换为 JavaScript 的等价形式。这个过程涉及到许多复杂的转换,但最终的结果是一个可以在浏览器或 Node.js 中运行的 JavaScript 文件。
// 编译后的 JavaScript (简化版)
var NetworkManager = function(httpClient) {
this.httpClient = httpClient;
};
NetworkManager.prototype.fetchUser = function(userId) {
var self = this;
return Promise.resolve()
.then(function() {
return self.httpClient.get("https://api.example.com/users/" + userId);
})
.then(function(response) {
return JSON.parse(response);
})
.catch(function(e) {
console.error("Error:", e);
return null;
});
};
注意 Kotlin 中的 suspend 函数被编译为返回 Promise 的 JavaScript 函数。这是因为 JavaScript 中没有原生的协程支持,所以 Kotlin 编译器使用 Promise 来模拟协程的行为。
Web 平台的 HTTP 实现
在 Web 平台上,我们使用浏览器提供的 Fetch API 来实现 HTTP 客户端。
// Web 平台的 HTTP 客户端
var WebHttpClient = function() {};
WebHttpClient.prototype.get = function(url) {
return fetch(url, {
method: 'GET',
headers: { 'Content-Type': 'application/json' }
}).then(function(response) {
if (!response.ok) throw new Error("HTTP " + response.status);
return response.text();
});
};
这个实现使用了浏览器的 Fetch API,它是现代 Web 开发中进行网络请求的标准方式。
第四部分:鸿蒙系统中的实际应用
在鸿蒙应用中使用网络管理器
在鸿蒙应用中,我们可以直接使用编译后的 KMP 代码。由于鸿蒙系统支持 Java 虚拟机,我们可以直接使用 JVM 编译目标的代码。
// HarmonyOS 应用代码
class HarmonyUserService {
private val httpClient = JvmHttpClient()
private val networkManager = NetworkManager(httpClient)
fun loadUserData(userId: String, onSuccess: (UserDto?) -> Unit) {
CoroutineScope(Dispatchers.Main).launch {
val user = networkManager.fetchUser(userId)
onSuccess(user)
}
}
}
这个代码展示了如何在鸿蒙应用中使用 KMP 编译的代码。我们创建了一个 JvmHttpClient 实例,然后将其传递给 NetworkManager。当需要加载用户数据时,我们在协程中调用 fetchUser 方法,并在回调中处理结果。
处理网络错误和重试
在实际应用中,网络请求经常会失败。我们需要实现错误处理和重试逻辑。
// 错误处理和重试
suspend fun getUserWithRetry(userId: String, maxRetries: Int = 3): UserDto? {
repeat(maxRetries) { attempt ->
try {
return networkManager.fetchUser(userId)
} catch (e: Exception) {
if (attempt < maxRetries - 1) {
Thread.sleep(1000 * (attempt + 1)) // 指数退避
}
}
}
return null
}
这个函数实现了一个简单的重试机制。如果网络请求失败,它会等待一段时间后重试。等待时间随着重试次数的增加而增加(指数退避),这有助于避免在服务器过载时继续发送请求。
第五部分:缓存策略与性能优化
为什么需要缓存
网络请求是应用中最耗时的操作之一。每次用户打开应用或刷新数据时,都需要等待网络请求完成。通过实现缓存机制,我们可以大大改善用户体验。当用户再次请求相同的数据时,我们可以直接从缓存中返回,而不需要进行网络请求。
实现缓存管理器
// commonMain/kotlin/com/example/kmp/network/CacheManager.kt
interface CacheManager {
suspend fun get(key: String): String?
suspend fun put(key: String, value: String)
suspend fun remove(key: String)
suspend fun clear()
}
class CachedNetworkManager(
private val networkManager: NetworkManager,
private val cacheManager: CacheManager
) {
suspend fun fetchUserWithCache(userId: String): UserDto? {
val cacheKey = "user_$userId"
// 先检查缓存
val cached = cacheManager.get(cacheKey)
if (cached != null) {
return parseUserDto(cached)
}
// 从网络获取
val user = networkManager.fetchUser(userId)
if (user != null) {
cacheManager.put(cacheKey, serializeUserDto(user))
}
return user
}
}
这个实现展示了如何在网络请求前检查缓存。如果缓存中存在数据,我们直接返回;否则,我们进行网络请求,并将结果存储在缓存中。

总结
通过本文的学习,我们理解了如何在 KMP 项目中实现跨平台的网络通信。关键的设计原则是使用接口来抽象平台特定的实现,这样业务逻辑就可以独立于具体的平台。我们展示了如何在 Android、JVM(鸿蒙)和 JavaScript 平台上实现这些接口,以及编译后的代码在不同平台上的样子。最后,我们讨论了如何在鸿蒙应用中使用这些共享代码,以及如何通过缓存和重试机制来提高应用的可靠性和性能。
更多推荐


所有评论(0)