项目概述

在现代应用开发中,网络通信是必不可少的功能。无论是获取用户数据、上传文件还是同步状态,所有这些操作都需要通过网络进行。在传统的多平台开发中,开发者需要在 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 平台上实现这些接口,以及编译后的代码在不同平台上的样子。最后,我们讨论了如何在鸿蒙应用中使用这些共享代码,以及如何通过缓存和重试机制来提高应用的可靠性和性能。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐