ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

KMM网络层实战:Android共享网络请求的工程化落地

KMM网络层实战:Android共享网络请求的工程化落地 1. 项目概述为什么在 Android 上用 KMP 做网络请求不是“炫技”而是工程必然KMP——这里指 Kotlin Multiplatform Mobile不是字符串匹配的 KMP 算法。这点必须第一时间划清界限因为搜索热词里混进了大量“kmp算法”“next数组”“鸿蒙适配”等完全无关的干扰项。真正要解决的问题是如何让 Android 工程中网络请求这一核心能力从 Java/Kotlin 单平台逻辑升级为可跨 iOS、桌面甚至未来 Web 共享的稳定基础设施。这不是为了写个 Demo 展示语法糖而是应对真实团队痛点业务模块复用率低、iOS 和 Android 同一接口要各自维护两套 Retrofit ViewModel Repository联调周期长、字段变更漏同步、错误码处理不一致——我带过的三个中型 App 团队平均每年因此多投入 280 人日的重复开发与回归测试。KMP 的价值不在“能不能跑”而在“能不能稳、能不能扩、能不能省”。它把网络层抽象成纯 Kotlin 的expect/actual接口让业务侧只面向NetworkClient编程底层实际调用 Android 的 OkHttp 或 iOS 的 URLSession上层完全无感。更关键的是它天然支持协程、结构化并发、类型安全的序列化通过 kotlinx.serialization避免了 Gson 反射带来的泛型擦除陷阱和 ProGuard 混淆风险。比如一个UserResponse数据类在 KMP 共享层定义一次Android 和 iOS 都能直接解析不用再各自写一遍SerializedName(user_name)或CodingKeys。实测下来新功能上线时网络层相关 Bug 下降 63%尤其在字段名变更、嵌套结构调整这类高频场景下效果立竿见影。适合谁看如果你正面临这些情况团队开始做 iOS 版本但不想重写全部网络逻辑App 架构在向 Clean Architecture 迁移需要清晰的分层边界或者你已经用 Compose 写 UI却还在用传统 MVP 模式管理网络回调——那这篇就是为你写的。它不讲“KMP 是什么”只讲“怎么用 KMP 把网络请求这件事做得比纯 Android 方案更可靠、更少 bug、更易维护”。接下来所有内容都基于一个真实落地的电商 App 改造项目从零开始每一步都有取舍依据和踩坑记录。2. 整体架构设计为什么选择 KMM 而非其他跨平台方案2.1 三种主流路径的硬性对比KMM、Flutter、React Native 的网络层成本很多人一提跨平台就默认选 Flutter但网络请求这个特定场景KMM 的优势是结构性的。我们做过横向对比同样是实现“获取商品列表 分页加载 错误重试”三者在 Android 侧的网络层投入差异巨大方案Android 网络层代码量LoCiOS 侧是否需重写网络逻辑类型安全保障调试体验包体积增量纯 AndroidRetrofit~1200 行含 Model、Service、Interceptor不适用✅Kotlin⚡️原生 Logcat0KMMKotlin Multiplatform~850 行共享层 ~200 行Android actual❌ 共享层直接复用✅✅kotlinx.serialization 编译期检查⚡️ Android Studio 支持断点进共享代码1.2MBokio serializationFlutterDio~0Dart 侧统一实现✅ iOS 也用 Dart但需桥接 Platform Channel 获取设备信息⚠️Dart 动态类型依赖 JSON 序列化库需在 Dart VM 和 Android/iOS 原生层间切换4.7MBlibflutter.so Dart runtimeReact NativeAxios~0JS 侧实现✅ 同上但 JS 弱类型更易出错❌TypeScript 仅编译期检查运行时仍可能崩溃Chrome DevTools 原生日志分离3.9MBJSCore JS bundle关键结论KMM 不是取代 Android 原生开发而是把最易出错、最需一致性、最常变更的网络层提前锁定在共享域。Flutter/RN 的网络层看似“写一次”实则因平台差异如证书校验、Cookie 存储机制、后台任务限制不得不写大量条件分支最终维护成本反超。而 KMM 的actual实现是 Kotlin 对 Kotlin 的精准对接——Android 用 OkHttpiOS 用 URLSession双方都用自己最熟悉的工具只是接口契约由共享层约定。2.2 KMM 网络层的核心分层Expect/Actual 不是魔法是契约KMM 的expect/actual机制常被误解为“黑盒”其实它本质是一份强制执行的接口契约。我们定义的网络层结构如下shared/src/commonMain/kotlin/com/example/network/ ├── NetworkClient.kt // expect interface定义 get/post/delete 方法签名 ├── HttpResponse.kt // expect sealed class统一错误分类NetworkError, ApiError, ParseError ├── HttpConfig.kt // expect object配置 baseUrl、timeout、headers └── serializer/ // expect module定义序列化器 └── JsonSerializer.kt对应 Android 平台的actual实现位于androidApp/src/main/kotlin/com/example/network/ ├── AndroidNetworkClient.kt // actual class封装 OkHttp Interceptor ├── AndroidHttpResponse.kt // actual implementation of HttpResponse └── AndroidJsonSerializer.kt // actual JsonSerializer using kotlinx.serialization重点在于expect层必须足够抽象但又不能过度设计。早期我们曾把OkHttpClient.Builder的所有参数都暴露为expect函数结果 iOS 侧根本无法实现connectionPool()或sslSocketFactory()。后来重构为只暴露业务强相关参数connectTimeoutMs: Int,readTimeoutMs: Int,authToken: String?。iOS 侧用URLSessionConfiguration的timeoutIntervalForRequest和httpAdditionalHeaders等效实现既满足需求又不强加平台特有概念。提示expect接口中的 suspend 函数其异常抛出必须明确。Kotlin 协程中throw的异常会被try/catch捕获但actual实现若在回调中抛出未捕获异常如 OkHttp 的IOException会直接 crash。因此我们在AndroidNetworkClient中统一用ResultT封装响应并在actual层将所有平台异常转为HttpResponse.Failure确保上层业务代码无需关心底层异常类型。2.3 为什么放弃 RetrofitKMM 生态下的 OkHttp 直接封装更可控Retrofit 是 Android 网络请求的事实标准但在 KMM 中它并非最优解。原因有三版本碎片化严重Retrofit 官方未提供commonMain支持社区方案如multiplatform-retrofit依赖 Kotlin 1.7且对kotlinx.serialization的适配常滞后于官方发布。我们曾因kotlinx.serialization升级到 1.6.0导致multiplatform-retrofit解析嵌套泛型失败排查耗时 17 小时。拦截器链不可控Retrofit 的Interceptor在 KMM 中需分别实现 Android/iOS但NetworkInterceptor如日志、重试的逻辑高度通用。若用 Retrofit这部分代码无法共享而直封装 OkHttp可将重试逻辑写在expect层actual层只负责发起请求。调试链路过长Retrofit → Converter → OkHttp → Socket任意一层出问题都需跳转多层源码。而直封装 OkHttp请求流程扁平化NetworkClient.get()→OkHttpClient.newCall().execute()→HttpResponse断点调试一目了然。我们的AndroidNetworkClient核心逻辑仅 230 行却覆盖了全部需求自动添加Authorizationheader从HttpConfig.authToken读取统一处理 401 Unauthorized触发登出流程并清除 token网络错误自动重试3 次指数退避响应体自动解析为T通过JsonSerializer.deserialize()请求/响应日志仅 Debug 模式启用这比引入 Retrofit Converter CallAdapter 的 500 行配置更轻量、更透明。3. 核心细节解析从协议设计到错误处理的全链路把控3.1 API 契约设计为什么用 sealed class 而非 Result Kotlin 的ResultT很诱人但用于网络层会带来两个隐性成本类型擦除陷阱ResultUser在 JVM 上实际是Result泛型信息丢失。当需要区分ResultUser和ResultListProduct的错误处理逻辑时只能靠is判断丧失编译期类型安全。扩展性差Result只有success和failure两种状态但真实网络场景需细分NetworkError无网络、超时、SSL 异常ApiErrorHTTP 状态码非 2xx如 400 参数错误、403 权限不足、500 服务端异常ParseErrorJSON 格式错误、字段缺失CancellationError用户主动取消请求因此我们定义expect sealed class HttpResponseout Texpect sealed class HttpResponseout T { data class SuccessT(val data: T) : HttpResponseT() sealed class Failure : HttpResponseNothing() { data class NetworkError(val cause: Throwable) : Failure() data class ApiError(val code: Int, val message: String, val details: MapString, Any?) : Failure() data class ParseError(val cause: Throwable) : Failure() object Cancellation : Failure() } }iOS 侧actual实现时ApiError的details字段用NSDictionary传递Android 侧用MapString, Any?双方都能无损解析。业务层处理时可精准匹配when (response) { is HttpResponse.Success - handleUser(response.data) is HttpResponse.Failure.ApiError - { when (response.code) { 401 - logoutAndRedirect() 400 - showValidationError(response.details[field] as? String) else - showErrorDialog(response.message) } } is HttpResponse.Failure.NetworkError - showNetworkOfflineToast() }注意sealed class的子类必须在同一文件或同一包内声明否则when会提示“exhaustive”这是 Kotlin 的类型安全保护。我们强制要求所有Failure子类在HttpResponse.kt中定义杜绝遗漏处理。3.2 序列化方案kotlinx.serialization 是唯一选择Gson/Jackson 在 KMM 中不可行它们依赖 JVM 反射而 iOS 是原生环境。kotlinx.serialization是 Kotlin 官方方案通过注解生成序列化器无反射开销且commonMain支持完美。关键配置如下// shared/src/commonMain/kotlin/com/example/serializer/JsonSerializer.kt expect object JsonSerializer { val json: Json } // androidApp/src/main/kotlin/com/example/serializer/AndroidJsonSerializer.kt actual object JsonSerializer { actual val json: Json Json { encodeDefaults true explicitNulls false ignoreUnknownKeys true // 关键启用 polymorphic支持 sealed class 的反序列化 serializersModule SerializersModule { polymorphic(HttpResponse::class) { subclass(HttpResponse.Success::class, HttpResponseSuccessSerializer()) subclass(HttpResponse.Failure::class, HttpResponseFailureSerializer()) } } } }HttpResponseSuccessSerializer和HttpResponseFailureSerializer需手动实现因为kotlinx.serialization默认不支持 sealed class 的多态序列化。我们采用“type 字段标识法”服务端返回时增加type: success或type: failure字段客户端据此选择解析器。这比尝试用SerialName注解sealed class更稳定避免因字段名冲突导致解析失败。3.3 认证与 Token 管理如何让登录态在 KMM 层统一刷新Token 过期是网络请求最常见问题。若在每个 API 调用前手动检查authToken是否过期代码冗余且易漏。我们采用“请求拦截 自动刷新”双机制Android 侧actual实现中OkHttp Interceptor 拦截 401 响应val authInterceptor Interceptor { chain - val request chain.request() val response chain.proceed(request) if (response.code 401 !request.url.toString().contains(/refresh)) { // 触发刷新 Token 流程 val newToken refreshTokenSync() // 同步刷新阻塞当前请求 if (newToken ! null) { // 用新 Token 重放原请求 val newRequest request.newBuilder() .header(Authorization, Bearer $newToken) .build() chain.proceed(newRequest) } else { response // 返回原 401交由上层处理登出 } } else { response } }刷新逻辑放在expect层但具体实现由平台决定expect suspend fun refreshToken(): String?Android 侧调用OkHttpClient发起/auth/refresh请求iOS 侧调用URLSession。这样业务代码只需调用NetworkClient.getUser(/profile)Token 刷新对上层完全透明。实操心得refreshTokenSync()必须是同步函数否则 OkHttp Interceptor 无法等待协程完成。我们用runBlocking包裹但限定在Dispatchers.IO避免阻塞主线程。同时设置超时30 秒防止刷新接口卡死导致整个请求挂起。4. 实操过程从项目初始化到真机调试的完整链路4.1 环境准备Android Studio 与 KMM 插件的最低兼容版本KMM 对工具链要求严格版本不匹配会导致commonMain无法识别、expect/actual报红。经实测稳定组合为Android StudioFlamingo | 2022.2.1 Patch 2或更高理由低版本对 Kotlin 1.8 的commonMain支持不全Gradle 同步常卡死Kotlin Plugin1.8.22与 AS Flamingo 捆绑勿手动升级理由Kotlin 1.9 的kotlinx.serialization与 KMM Gradle 插件存在兼容问题Gradle Wrappergradle-8.0-bin.zip理由Gradle 7.x 对kotlin-multiplatform插件的依赖解析有缺陷KMM Plugin已内置无需额外安装初始化步骤命令行# 1. 创建新项目推荐使用官方模板 curl -s https://raw.githubusercontent.com/JetBrains/kmm-template/main/kmm-template.sh | bash -s myapp # 2. 导入 Android Studio选择 Import project (Gradle)指向 myapp 目录 # 3. 等待 Gradle 同步完成检查以下目录是否存在 # - shared/src/commonMain/kotlin/ # - androidApp/src/main/kotlin/ # - iosApp/src/iosMain/kotlin/注意首次同步可能耗时 5-10 分钟因需下载kotlinx-serialization、okio等 KMM 专用依赖。若卡在Resolving dependencies检查~/.gradle/caches/是否有磁盘空间不足至少 2GB 空闲。4.2 共享模块构建Gradle 配置的关键参数与避坑点shared/build.gradle.kts是 KMM 网络层的基石配置错误会导致commonMain无法编译。核心配置如下plugins { kotlin(multiplatform) version 1.8.22 apply true id(com.android.library) version 8.0.2 apply true kotlin(plugin.serialization) version 1.8.22 apply true // 必须启用序列化插件 } kotlin { androidTarget { publishAllLibraryVariants() // 发布所有变体供 Android App 依赖 // 关键指定 Android SDK 版本避免与主 App 冲突 compilations.all { kotlinOptions { jvmTarget 1.8 } } } iosX64() iosArm64() iosSimulatorArm64() sourceSets { val commonMain by getting { dependencies { implementation(io.ktor:ktor-client-core:2.3.2) // KMM 官方 HTTP 客户端但我们不用它 implementation(org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0) implementation(com.squareup.okio:okio:3.4.0) // KMM 兼容的 Okio } } val androidMain by getting { dependencies { implementation(com.squareup.okhttp3:okhttp:4.11.3) // Android 专用 OkHttp implementation(com.squareup.okhttp3:logging-interceptor:4.11.3) } } } }致命避坑点publishAllLibraryVariants()必须开启否则androidApp无法通过implementation(project(:shared))依赖共享模块。kotlinOptions.jvmTarget 1.8必须显式指定否则 Android Gradle Plugin 会默认用11导致commonMain中的suspend函数在 Android 侧编译失败。kotlinx-serialization-json版本必须与 Kotlin 插件版本严格匹配1.8.22 对应 1.5.0否则Serializable注解报红。4.3 Android 侧 actual 实现OkHttp 封装的 7 个关键细节AndroidNetworkClient不是简单包装 OkHttp而是针对 Android 生态做了深度适配。以下是 7 个必须处理的细节主线程安全OkHttp 的execute()是同步阻塞调用必须在Dispatchers.IO执行否则 ANR。我们用withContext(Dispatchers.IO)包裹override suspend fun T : Any get( path: String, serializer: KSerializerT ): HttpResponseT withContext(Dispatchers.IO) { try { val request Request.Builder() .url(${HttpConfig.baseUrl}$path) .addHeader(Authorization, Bearer ${HttpConfig.authToken ?: }) .build() val response client.newCall(request).execute() // ... 解析逻辑 } catch (e: Exception) { HttpResponse.Failure.NetworkError(e) } }Cookie 同步Android 的CookieJar需与 WebView、系统 Cookie 同步。我们实现PersistentCookieJar将 Cookie 存入SharedPreferencesval cookieJar PersistentCookieJar( SharedPreferencesCookieStore(context.getSharedPreferences(cookies, Context.MODE_PRIVATE)), FileCookieStore(context.cacheDir) )HTTPS 证书校验生产环境必须启用严格校验但测试环境常需信任自签名证书。我们通过BuildConfig.DEBUG控制if (BuildConfig.DEBUG) { val trustAllCerts arrayOfTrustManager(object : X509TrustManager { override fun checkClientTrusted(chain: ArrayX509Certificate, authType: String) {} override fun checkServerTrusted(chain: ArrayX509Certificate, authType: String) {} override fun getAcceptedIssuers(): ArrayX509Certificate arrayOf() }) sslContext.init(null, trustAllCerts, SecureRandom()) builder.sslSocketFactory(sslContext.socketFactory, trustAllCerts[0] as X509TrustManager) }请求取消OkHttpClient的cancel()方法需与 Kotlin 协程的Job关联。我们在get()函数中创建Job并在finally块中取消val job Job() try { withContext(Dispatchers.IO job) { // 执行请求 } } finally { job.cancel() // 确保请求被取消 }日志拦截器仅在 Debug 模式启用避免 Release 包泄露敏感信息if (BuildConfig.DEBUG) { client.interceptors().add(HttpLoggingInterceptor().apply { level HttpLoggingInterceptor.Level.BODY }) }连接池复用OkHttp 默认连接池大小为 5对高并发电商 App 不足。我们设为 20client.connectionPool(ConnectionPool(20, 5, TimeUnit.MINUTES))Gzip 压缩服务端返回的 JSON 默认启用 GzipOkHttp 自动解压但需确认服务端 Header 正确// OkHttp 自动处理无需额外代码但需确保服务端返回 Content-Encoding: gzip4.4 真机调试为什么模拟器无法替代真机验证KMM 网络层在模拟器上能跑通不代表真机能用。我们遇到的真机特有问题华为/小米手机的“省电模式”强制冻结后台网络请求。解决方案在AndroidManifest.xml中添加权限并引导用户关闭优化uses-permission android:nameandroid.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS /并在首次启动时调用PowerManager.isIgnoringBatteryOptimizations()检查。Android 10 的 Scoped StorageFileProvider的external_path路径变更如content://com.tencent.wework.fileprovider/...。网络请求本身不受影响但若请求中包含文件上传需用ContentResolver读取Uri而非直接File路径。Vivo/Oppo 的“后台冻结”应用切到后台后OkHttp 的ConnectionPool被系统回收。解决方案在Application.onCreate()中初始化OkHttpClient单例并设置keepAliveDurationclient.connectionPool(ConnectionPool(20, 5, TimeUnit.MINUTES))真机调试必备步骤在androidApp/src/main/AndroidManifest.xml中添加android:usesCleartextTraffictrue仅 DebugRelease 必须用 HTTPS使用adb logcat -s NetworkClient过滤日志避免被海量系统日志淹没在AndroidNetworkClient的catch块中打印e.stackTraceToString()而非仅e.message5. 常见问题与排查技巧实录那些文档不会写的实战经验5.1 典型问题速查表从编译失败到运行时崩溃问题现象根本原因解决方案重现概率Cannot find expect declaration for NetworkClientshared模块未被androidApp正确依赖或build.gradle.kts中publishAllLibraryVariants()未开启检查androidApp/build.gradle.kts是否有implementation(project(:shared))确认shared/build.gradle.kts中publishAllLibraryVariants()已启用⚠️ 高新手必踩Unresolved reference: kotlinxkotlinx-serialization版本与 Kotlin 插件不匹配查看 Kotlin 插件版本AS → Help → About → Kotlin Plugin安装对应kotlinx-serialization版本如 Kotlin 1.8.22 → serialization 1.5.0⚠️ 高java.lang.NoClassDefFoundError: kotlinx/serialization/json/JsonandroidApp的minSdkVersion 21而kotlinx-serialization最低要求 21将androidApp/build.gradle.kts中minSdkVersion设为21⚠️ 中老项目迁移常见Network request failed: java.net.UnknownHostException真机未开启网络或AndroidManifest.xml缺少INTERNET权限检查uses-permission android:nameandroid.permission.INTERNET /是否存在用ConnectivityManager检测网络状态⚠️ 中Caused by: java.lang.ClassNotFoundException: okhttp3.internal.platform.Platformokhttp3依赖未正确添加到androidMain确认shared/build.gradle.kts中androidMain.dependencies包含implementation(com.squareup.okhttp3:okhttp:4.11.3)⚠️ 中SerializationException: Serializer for class User is not foundUser数据类未加Serializable注解或kotlinx-serialization插件未启用检查User.kt是否有Serializable确认shared/build.gradle.kts中kotlin(plugin.serialization)已应用⚠️ 高E/AndroidRuntime: FATAL EXCEPTION: DefaultDispatcher-worker-1NetworkClient.get()中未用withContext(Dispatchers.IO)导致在主线程执行 OkHttp 同步请求在actual实现中所有OkHttpClient.execute()必须包裹withContext(Dispatchers.IO)⚠️ 高ANR 根源5.2 独家避坑技巧提升 3 倍调试效率的 3 个方法技巧 1用Logcat过滤器精准定位网络日志不要用adb logcat默认输出而是创建专属过滤器adb logcat -s NetworkClient:I OkHttp:W kotlinx.serialization:WNetworkClient:I显示我们自定义的日志Info 级别OkHttp:W显示 OkHttp 的警告如连接超时、重定向kotlinx.serialization:W显示序列化失败详情如Missing field id技巧 2Mock Server 本地化绕过服务端依赖开发阶段用MockWebServer启动本地 HTTP 服务返回预设 JSONval mockServer MockWebServer() mockServer.enqueue(MockResponse().setBody({id:1,name:test})) mockServer.start(8080) HttpConfig.baseUrl http://localhost:8080/这样即使后端接口未就绪也能验证NetworkClient的解析逻辑是否正确且避免污染真实测试环境。技巧 3Gradle 依赖树分析秒杀冲突问题当出现NoSuchMethodError或IncompatibleClassChangeError一定是依赖冲突。用此命令查看shared模块的依赖树./gradlew :shared:dependencies --configuration androidMainCompileClasspath重点关注okhttp、kotlinx-serialization的版本是否被其他库如retrofit、coil降级。若发现冲突用force强制指定版本configurations.all { resolutionStrategy { force(com.squareup.okhttp3:okhttp:4.11.3) force(org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0) } }5.3 性能监控如何证明 KMM 网络层比纯 Android 方案更快性能不能靠感觉必须量化。我们在NetworkClient中加入毫秒级耗时统计override suspend fun T : Any get( path: String, serializer: KSerializerT ): HttpResponseT { val startTime System.currentTimeMillis() val result try { // 执行请求... response } finally { val duration System.currentTimeMillis() - startTime Timber.d(GET $path: ${duration}ms, status${response.code()}) } return result }实测数据100 次请求平均值Wi-Fi 环境场景纯 Android (Retrofit)KMM (OkHttp 封装)差异首次请求冷启动284ms276ms-2.8%后续请求连接复用42ms38ms-9.5%大 JSON 解析50KB112ms98ms-12.5%优势来源连接复用KMM 封装的OkHttpClient连接池复用率 92%Retrofit 因CallAdapter层级更多复用率仅 85%。序列化解析kotlinx.serialization的二进制序列化Json.encodeToByteArray()比 Gson 快 3.2 倍虽此处用 JSON但解析器更精简。我个人在实际操作中的体会是KMM 网络层的价值80% 在稳定性减少跨平台 Bug15% 在开发效率共享逻辑5% 在性能。不要把它当成性能优化工具而要当作工程质量的基础设施。当你第 3 次因为 iOS 和 Android 字段名不一致导致订单提交失败时你会明白为网络层多花 2 天搭建 KMM换来的是一整年不再为这类问题加班。
返回列表