在跨平台移动开发领域,Kotlin Multiplatform(简称KMP)正越来越受到开发者的青睐。它的核心想法很简单:把业务逻辑写到一份代码里,然后在iOS和Android上各自跑起来。听起来很美,但真正动手做的时候,很多人会陷入一个困惑——共享模块写出来之后,代码复用率到底有没有真正提升?还是说只是把同一份逻辑搬来搬去,实际上重复代码还是不少?
这篇文章就来聊聊,在KMP项目中,如何通过合理的架构设计,真正把共享模块的代码复用率提上去。
一、为什么要关注KMP共享模块的代码复用率
先说一个很现实的场景。你做了一个KMP项目,shared模块写了不少代码,业务逻辑、数据模型、网络请求的抽象层,都放在commonMain里了。看起来挺整洁,但到了iOS端和Android端,你还是得写一大堆适配代码,把KMP暴露出来的接口一个个接上。这时候你就会发现,虽然业务逻辑是共享的,但胶水代码依然不少。
那什么是真正的代码复用呢?不是简单地"同一份代码跑在两个平台上",而是指你能用最少的代码量,实现最大的功能覆盖。如果一个需求变更,你只需要改一处代码,两个平台都能自动同步更新,这才叫高复用率。
KMP的优势在于,它允许你在commonMain中定义纯Kotlin的逻辑,然后在各自的expect/actual机制下为不同平台提供实现。但怎么组织这些代码,才能让复用率最大化,这才是架构设计要解决的问题。
二、KMP项目的基本架构认知
2.1 KMP的核心概念
KMP项目的代码分为几个源集(source set):commonMain是所有平台共享的代码,androidMain只给Android用,iosMain只给iOS用。你写的业务逻辑放在commonMain,平台相关的实现放在各自的源集里,通过expect/actual机制连接起来。
举个例子,你想定义一个获取设备屏幕宽度的方法:
// 技术栈:Kotlin Multiplatform + Gradle
// commonMain中声明期望的接口
expect fun getScreenWidth(): Int
然后在Android和iOS端各自实现:
// 技术栈:Kotlin Multiplatform + Gradle
// androidMain中提供实现
actual fun getScreenWidth(): Int {
return android.util.DisplayMetrics().widthPixels
}
// 技术栈:Kotlin Multiplatform + Gradle
// iosMain中提供实现
actual fun getScreenWidth(): Int {
return UIScreen.mainScreen.bounds.size.width.toInt()
}
这就是最基础的expect/actual用法。但如果你只有这种程度的使用,复用率提升有限。
2.2 共享模块的项目结构
一个典型的KMP项目结构长这样:
// 技术栈:Kotlin Multiplatform + Gradle
// 项目根目录结构
project/
├── shared/ // 共享模块
│ ├── commonMain/ // 全平台共享代码
│ │ └── kotlin/com/example/
│ │ ├── model/ // 数据模型
│ │ ├── repository/ // 数据仓库接口
│ │ ├── service/ // 业务服务
│ │ └── util/ // 工具类
│ ├── androidMain/ // Android专属实现
│ │ └── kotlin/com/example/
│ │ └── platform/ // 平台实现
│ └── iosMain/ // iOS专属实现
│ └── kotlin/com/example/
│ └── platform/ // 平台实现
├── androidApp/ // Android应用模块
├── iosApp/ // iOS应用模块
└── build.gradle.kts
这种结构看起来很正常,但问题在于:当业务复杂度上升后,所有东西都堆在一个shared模块里,复用率反而会下降,因为你没办法把通用的业务逻辑和特定平台无关的代码分开复用。
三、提升代码复用率的架构思路
3.1 分层架构设计
提升复用率的第一步是把代码分层。不是简单的三层架构(UI-业务-数据),而是要在KMP的语境下重新思考。我建议分为四层:
第一层是核心层(Core),放纯Kotlin的、跟平台完全无关的代码。比如数据模型、领域逻辑、纯算法实现。这部分代码100%复用,不存在任何平台差异。
第二层是抽象层(Abstraction),定义各种接口和expect声明。比如网络请求的接口、本地存储的接口、日志记录的接口。这一层不实现任何具体逻辑,只定义"要做什么"。
第三层是平台实现层(Platform),在androidMain和iosMain里实现抽象层定义的接口。比如用Retrofit实现网络请求,用Core Data或NSKeyedArchiver实现本地存储。
第四层是应用层(Application),组装各层组件,处理UI和业务编排。
分层的关键好处是:核心层可以独立测试,完全不依赖平台环境;抽象层让平台实现可以自由替换,不会耦合;平台层只负责适配,不包含业务逻辑。
3.2 平台期望与实现分离
很多开发者在写KMP代码时,容易把expect/actual用得太"散"。比如到处声明expect class、expect interface,导致代码看起来到处都是expect,很难看清哪些是共享逻辑,哪些是平台差异。
更好的做法是:把expect集中到少数几个"平台桥接类"中。比如:
// 技术栈:Kotlin Multiplatform + Gradle
// commonMain中定义平台桥接接口
interface PlatformBridge {
/** 获取应用版本号 */
fun getAppVersion(): String
/** 发送本地通知 */
fun sendLocalNotification(title: String, content: String)
/** 获取设备唯一标识 */
fun getDeviceId(): String
}
// 用expect object声明单例平台桥接
expect object PlatformProvider {
val bridge: PlatformBridge
}
然后在Android端实现:
// 技术栈:Kotlin Multiplatform + Gradle
// 在androidMain中实现Android平台桥接
actual object PlatformProvider {
actual val bridge: PlatformBridge = AndroidPlatformBridge()
}
class AndroidPlatformBridge : PlatformBridge {
override fun getAppVersion(): String {
// 获取Android版本号
return BuildConfig.VERSION_NAME
}
override fun sendLocalNotification(title: String, content: String) {
// 使用Android的通知渠道发送通知
val channel = NotificationChannel(
"app_channel",
"应用通知",
NotificationManager.IMPORTANCE_DEFAULT
)
val manager = getSystemService(NotificationManager::class.java)
manager.createNotificationChannel(channel)
// 构建并发送通知的逻辑...
}
override fun getDeviceId(): String {
return Settings.Secure.getString(
contentResolver,
Settings.Secure.ANDROID_ID
)
}
}
iOS端同理:
// 技术栈:Kotlin Multiplatform + Gradle
// 在iosMain中实现iOS平台桥接
actual object PlatformProvider {
actual val bridge: PlatformBridge = IOSPlatformBridge()
}
class IOSPlatformBridge : PlatformBridge {
override fun getAppVersion(): String {
// 获取iOS应用版本
return Bundle.main.infoDictionary?.get(
"CFBundleShortVersionString"
) as? String ?: ""
}
override fun sendLocalNotification(title: String, content: String) {
// 使用iOS的UNUserNotificationCenter发送通知
val center = UNUserNotificationCenter.current()
val notificationContent = UNMutableNotificationContent()
notificationContent.title = title
notificationContent.body = content
val request = UNNotificationRequest(
identifier = UUID().toString(),
content = notificationContent,
trigger = null
)
center.addNotificationRequest(request)
}
override fun getDeviceId(): String {
// 使用iOS的identifierForVendor获取设备标识
return UIDevice.current.identifierForVendor.UUIDString ?: ""
}
}
这样做的好处是:你只需要维护两个PlatformBridge实现,所有平台差异都集中在这两个类里。其他业务代码只依赖PlatformBridge接口,完全不知道底层是哪个平台。这就把复用率从"共享业务逻辑"提升到了"共享所有非平台相关代码"。
3.3 依赖注入与模块化
光有分层还不够。当你的shared模块越来越大,不同业务功能之间会产生耦合。比如用户模块的代码需要调用订单模块的代码,订单模块又需要调用支付模块的代码。这种耦合在KMP中特别危险,因为你一旦改了某个模块,两个平台都要重新编译测试。
解决方式是引入依赖注入和模块化。思路是:把shared模块拆成多个子模块,每个子模块独立编译、独立测试、独立复用。
// 技术栈:Kotlin Multiplatform + Gradle
// 项目顶层settings.gradle.kts中的模块配置
rootProject.name = "DemoApp"
include(":shared-core") // 核心层:数据模型、领域逻辑
include(":shared-network") // 网络层:网络请求接口和实现
include(":shared-storage") // 存储层:本地存储接口和实现
include(":shared-business") // 业务层:各业务模块
include(":androidApp") // Android应用
include(":iosApp") // iOS应用
// 技术栈:Kotlin Multiplatform + Gradle
// shared-core/build.gradle.kts
plugins {
kotlin("multiplatform")
}
kotlin {
androidTarget {
compilations.getByName("main") {
associateWith(defaultHierarchyRoot.compilations.getByName("commonMain"))
}
}
iosX64()
iosArm64()
iosSimulatorArm64()
sourceSets {
val commonMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
}
}
}
}
// 技术栈:Kotlin Multiplatform + Gradle
// shared-network/build.gradle.kts
plugins {
kotlin("multiplatform")
}
kotlin {
androidTarget()
iosX64()
iosArm64()
iosSimulatorArm64()
sourceSets {
val commonMain by getting {
dependencies {
implementation(project(":shared-core")) // 依赖核心层
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
}
}
val androidMain by getting {
dependencies {
implementation("com.squareup.okhttp3:okhttp:4.12.0")
}
}
val iosMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
}
}
}
}
这种模块化拆分的好处是:网络层的改动不会影响存储层,存储层的改动不会影响业务层。你可以独立测试每个子模块,独立升级每个子模块的依赖版本,也可以把某个子模块复用到另一个项目中。
四、实践示例:搭建一个完整的KMP共享模块
4.1 项目初始化
先创建一个基础的KMP项目。用Android Studio的New Project向导,选择Multiplatform Mobile,选择目标平台(iOS和Android),项目名称设为DemoApp。
生成项目后,你会看到shared模块和androidApp、iosApp两个应用模块。我们先修改shared模块的构建配置:
// 技术栈:Kotlin Multiplatform + Gradle
// shared/build.gradle.kts
plugins {
kotlin("multiplatform")
id("com.android.library")
id("org.jetbrains.kotlin.plugin.serialization") version "1.9.20"
}
kotlin {
androidTarget {
compilations.all {
kotlinOptions {
jvmTarget = "11"
}
}
}
iosX64()
iosArm64()
iosSimulatorArm64()
sourceSets {
val commonMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
}
}
val commonTest by getting {
dependencies {
implementation(kotlin("test"))
}
}
val androidMain by getting {
dependencies {
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
}
}
val iosMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
}
}
}
}
android {
namespace = "com.example.demoapp.shared"
compileSdk = 34
defaultConfig {
minSdk = 24
}
}
4.2 commonMain模块设计
在commonMain中,我们先定义核心数据模型和接口。假设我们的应用是一个简单的任务管理应用:
// 技术栈:Kotlin Multiplatform + Gradle
// commonMain/src/kotlin/com/example/demoapp/model/Task.kt
package com.example.demoapp.model
/**
* 任务数据模型
* 这个模型在所有平台共享,不需要任何平台相关的代码
*/
@Serializable
data class Task(
val id: String, // 任务唯一标识
val title: String, // 任务标题
val description: String = "", // 任务描述
val isCompleted: Boolean = false, // 是否已完成
val createdAt: Long = 0L, // 创建时间戳
val priority: TaskPriority = TaskPriority.NORMAL // 优先级
)
/**
* 任务优先级枚举
* @property LOW 低优先级
* @property NORMAL 普通优先级
* @property HIGH 高优先级
*/
@Serializable
enum class TaskPriority {
LOW, // 低优先级
NORMAL, // 普通优先级
HIGH // 高优先级
}
// 技术栈:Kotlin Multiplatform + Gradle
// commonMain/src/kotlin/com/example/demoapp/repository/TaskRepository.kt
package com.example.demoapp.repository
import com.example.demoapp.model.Task
import com.example.demoapp.model.TaskPriority
import kotlinx.coroutines.flow.Flow
/**
* 任务仓库接口
* 定义了所有对任务数据的操作,具体实现由各平台提供
* 接口放在commonMain,确保业务层只依赖接口不依赖实现
*/
interface TaskRepository {
/**
* 获取所有任务
* 返回Flow以便在UI层进行响应式监听
*/
fun getAllTasks(): Flow<List<Task>>
/**
* 根据ID获取单个任务
*/
suspend fun getTaskById(id: String): Task?
/**
* 创建新任务
* @param title 任务标题
* @param description 任务描述(可选)
* @param priority 优先级(可选,默认普通)
* @return 创建后的任务对象
*/
suspend fun createTask(
title: String,
description: String = "",
priority: TaskPriority = TaskPriority.NORMAL
): Task
/**
* 更新任务状态
* @param id 任务ID
* @param isCompleted 是否完成
* @return 是否更新成功
*/
suspend fun updateTaskStatus(id: String, isCompleted: Boolean): Boolean
/**
* 删除任务
* @param id 任务ID
* @return 是否删除成功
*/
suspend fun deleteTask(id: String): Boolean
}
// 技术栈:Kotlin Multiplatform + Gradle
// commonMain/src/kotlin/com/example/demoapp/usecase/TaskUseCase.kt
package com.example.demoapp.usecase
import com.example.demoapp.model.Task
import com.example.demoapp.model.TaskPriority
import com.example.demoapp.repository.TaskRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
/**
* 任务用例
* 用例层是纯业务逻辑,不依赖任何平台实现
* 这里可以写复杂的业务规则,比如排序、过滤、校验等
*/
class TaskUseCase(private val repository: TaskRepository) {
/**
* 获取排序后的任务列表
* 排序规则:未完成的排在前面,同状态内按优先级从高到低排序
*/
fun getSortedTasks(): Flow<List<Task>> = repository.getAllTasks().map { tasks ->
tasks.sortedWith(
compareByDescending<Task> { it.isCompleted == false }
.thenByDescending { it.priority }
)
}
/**
* 获取高优先级未完成的任务
* 这是典型的业务逻辑,完全不需要平台代码
*/
fun getUrgentTasks(): Flow<List<Task>> = repository.getAllTasks().map { tasks ->
tasks.filter { !it.isCompleted && it.priority == TaskPriority.HIGH }
}
/**
* 获取已完成的任务数量
* 用于展示"已完成X个任务"之类的统计信息
*/
fun getCompletedCount(): Flow<Int> = repository.getAllTasks().map { tasks ->
tasks.count { it.isCompleted }
}
/**
* 创建任务并做校验
* 校验逻辑放在用例层,保证业务规则的一致性
*/
suspend fun createTask(
title: String,
description: String,
priority: TaskPriority
): Result<Task> {
// 业务校验:标题不能为空
if (title.isBlank()) {
return Result.failure(IllegalArgumentException("任务标题不能为空"))
}
// 业务校验:标题长度限制
if (title.length > 100) {
return Result.failure(IllegalArgumentException("任务标题不能超过100个字符"))
}
return Result.success(
repository.createTask(title, description, priority)
)
}
/**
* 批量删除已完成的任务
* 方便用户一键清理已完成任务
*/
suspend fun deleteCompletedTasks(): Int {
val tasks = repository.getAllTasks().first()
var deletedCount = 0
tasks.filter { it.isCompleted }.forEach { task ->
if (repository.deleteTask(task.id)) {
deletedCount++
}
}
return deletedCount
}
}
注意这个usecase代码,里面没有任何平台相关的代码。排序、过滤、校验逻辑都是纯Kotlin,在iOS和Android上跑的是完全同一份代码。这就是代码复用的核心——业务逻辑只写一次。
4.3 iOS平台实现
在iosMain中,我们实现TaskRepository接口,使用内存存储作为示例(实际项目可以用Core Data或SQLite):
// 技术栈:Kotlin Multiplatform + Gradle
// iosMain/src/kotlin/com/example/demoapp/platform/IOSTaskRepository.kt
package com.example.demoapp.platform
import com.example.demoapp.model.Task
import com.example.demoapp.model.TaskPriority
import com.example.demoapp.repository.TaskRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import platform.Foundation.*
/**
* iOS平台的任务仓库实现
* 使用内存存储作为示例
* 实际项目中可以替换为Core Data或SQLite的实现
*/
class IOSTaskRepository : TaskRepository {
// 内存中的数据源,用MutableStateFlow包装以便响应式监听
private val _tasks = MutableStateFlow<List<Task>>(emptyList())
override fun getAllTasks(): Flow<List<Task>> = _tasks.asStateFlow()
override suspend fun getTaskById(id: String): Task? {
// 从内存列表中查找对应ID的任务
return _tasks.value.find { it.id == id }
}
override suspend fun createTask(
title: String,
description: String,
priority: TaskPriority
): Task {
// 创建新任务,使用UUID生成唯一标识
val newTask = Task(
id = UUID().toString(),
title = title,
description = description,
priority = priority
)
// 将新任务添加到列表
_tasks.value = _tasks.value + newTask
return newTask
}
override suspend fun updateTaskStatus(id: String, isCompleted: Boolean): Boolean {
val tasks = _tasks.value
// 找到对应任务并更新状态
val updatedTasks = tasks.map { task ->
if (task.id == id) {
task.copy(isCompleted = isCompleted)
} else {
task
}
}
_tasks.value = updatedTasks
return true
}
override suspend fun deleteTask(id: String): Boolean {
val tasks = _tasks.value
val remaining = tasks.filter { it.id != id }
// 如果数量变化了,说明删除成功
if (remaining.size != tasks.size) {
_tasks.value = remaining
return true
}
return false
}
}
4.4 Android平台实现
在androidMain中,同样实现TaskRepository接口:
// 技术栈:Kotlin Multiplatform + Gradle
// androidMain/src/kotlin/com/example/demoapp/platform/AndroidTaskRepository.kt
package com.example.demoapp.platform
import com.example.demoapp.model.Task
import com.example.demoapp.model.TaskPriority
import com.example.demoapp.repository.TaskRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import java.util.UUID
/**
* Android平台的任务仓库实现
* 使用内存存储作为示例
* 实际项目中可以替换为Room或SQLite的实现
*/
class AndroidTaskRepository : TaskRepository {
// 内存中的数据源
private val _tasks = MutableStateFlow<List<Task>>(emptyList())
override fun getAllTasks(): Flow<List<Task>> = _tasks.asStateFlow()
override suspend fun getTaskById(id: String): Task? {
return _tasks.value.find { it.id == id }
}
override suspend fun createTask(
title: String,
description: String,
priority: TaskPriority
): Task {
val newTask = Task(
id = UUID.randomUUID().toString(),
title = title,
description = description,
priority = priority
)
_tasks.value = _tasks.value + newTask
return newTask
}
override suspend fun updateTaskStatus(id: String, isCompleted: Boolean): Boolean {
val tasks = _tasks.value
val updatedTasks = tasks.map { task ->
if (task.id == id) {
task.copy(isCompleted = isCompleted)
} else {
task
}
}
_tasks.value = updatedTasks
return true
}
override suspend fun deleteTask(id: String): Boolean {
val tasks = _tasks.value
val remaining = tasks.filter { it.id != id }
if (remaining.size != tasks.size) {
_tasks.value = remaining
return true
}
return false
}
}
4.5 依赖注入方案
现在最关键的一步来了:怎么把接口和实现连起来?在KMP中没有像Hilt那样的官方依赖注入框架,但我们可以自己实现一个简单的:
// 技术栈:Kotlin Multiplatform + Gradle
// commonMain/src/kotlin/com/example/demoapp/di/ServiceLocator.kt
package com.example.demoapp.di
import com.example.demoapp.repository.TaskRepository
import com.example.demoapp.usecase.TaskUseCase
/**
* 服务定位器
* 负责管理所有服务的实例
* 这里是简化版本,实际项目可以扩展成支持作用域的完整DI容器
*/
class ServiceLocator {
// 存储服务实例的映射表,key是服务标识,value是实例
private val _services = mutableMapOf<String, Any>()
/**
* 注册服务
* @param key 服务标识字符串
* @param instance 服务实例对象
*/
fun register(key: String, instance: Any) {
_services[key] = instance
}
/**
* 获取服务
* @param key 服务标识
* @return 服务实例,如果未注册则抛出异常
*/
@Suppress("UNCHECKED_CAST")
inline fun <reified T> get(key: String): T {
return _services[key] as T
}
/**
* 检查服务是否已注册
*/
fun has(key: String): Boolean = _services.containsKey(key)
/**
* 注销服务
*/
fun unregister(key: String) {
_services.remove(key)
}
/**
* 清空所有已注册的服务
*/
fun clear() {
_services.clear()
}
}
/**
* 服务注册表
* 定义所有需要注入的服务的key常量
* 新增服务时只需在这里添加一行
*/
object ServiceRegistry {
const val TASK_REPOSITORY = "TaskRepository"
const val TASK_USE_CASE = "TaskUseCase"
const val PLATFORM_BRIDGE = "PlatformBridge"
}
// 技术栈:Kotlin Multiplatform + Gradle
// commonMain/src/kotlin/com/example/demoapp/di/expect/PlatformFactory.kt
package com.example.demoapp.di.expect
import com.example.demoapp.repository.TaskRepository
/**
* 期望的平台工厂
* 每个平台需要提供自己的平台服务创建逻辑
* 这是expect/actual机制的典型用法
*/
expect object PlatformFactory {
/**
* 创建当前平台的TaskRepository实例
* iOS平台会返回IOSTaskRepository
* Android平台会返回AndroidTaskRepository
*/
fun createTaskRepository(): TaskRepository
}
// 技术栈:Kotlin Multiplatform + Gradle
// androidMain/src/kotlin/com/example/demoapp/di/actual/AndroidPlatformFactory.kt
package com.example.demoapp.di.actual
import com.example.demoapp.di.expect.PlatformFactory
import com.example.demoapp.platform.AndroidTaskRepository
import com.example.demoapp.repository.TaskRepository
/**
* Android平台的平台工厂实现
* 这里可以注入Android专属的服务,比如带数据库的仓库实现
*/
actual object PlatformFactory {
actual fun createTaskRepository(): TaskRepository {
return AndroidTaskRepository()
}
}
// 技术栈:Kotlin Multiplatform + Gradle
// iosMain/src/kotlin/com/example/demoapp/di/actual/IOSPlatformFactory.kt
package com.example.demoapp.di.actual
import com.example.demoapp.di.expect.PlatformFactory
import com.example.demoapp.platform.IOSTaskRepository
import com.example.demoapp.repository.TaskRepository
/**
* iOS平台的平台工厂实现
* 这里可以注入iOS专属的服务,比如带Core Data的仓库实现
*/
actual object PlatformFactory {
actual fun createTaskRepository(): TaskRepository {
return IOSTaskRepository()
}
}
// 技术栈:Kotlin Multiplatform + Gradle
// commonMain/src/kotlin/com/example/demoapp/AppModule.kt
package com.example.demoapp
import com.example.demoapp.di.ServiceLocator
import com.example.demoapp.di.ServiceRegistry
import com.example.demoapp.di.expect.PlatformFactory
import com.example.demoapp.repository.TaskRepository
import com.example.demoapp.usecase.TaskUseCase
/**
* 应用模块
* 负责初始化所有服务依赖
* 这是整个应用的依赖注入入口点
*/
object AppModule {
// 全局服务定位器实例
val serviceLocator = ServiceLocator()
// 标记是否已初始化,防止重复初始化
private var isInitialized = false
/**
* 初始化应用服务
* 在应用启动时调用一次
*/
fun initialize() {
if (isInitialized) return
// 第一步:通过平台工厂创建平台相关的仓库实例
val taskRepository = PlatformFactory.createTaskRepository()
// 第二步:创建用例实例(纯业务逻辑,与平台无关)
val taskUseCase = TaskUseCase(taskRepository)
// 第三步:注册到服务定位器
serviceLocator.register(ServiceRegistry.TASK_REPOSITORY, taskRepository)
serviceLocator.register(ServiceRegistry.TASK_USE_CASE, taskUseCase)
isInitialized = true
}
/**
* 重置应用模块
* 主要用于单元测试时重置状态
*/
fun reset() {
serviceLocator.clear()
isInitialized = false
}
}
这样,你的代码架构就变成了:
// 技术栈:Kotlin Multiplatform + Gradle
// 整体架构层次图
//
// UI层(各自平台) androidApp / iosApp
// └── 调用业务层
// └── TaskUseCase(commonMain,100%复用)
// └── 调用数据层接口
// └── TaskRepository(commonMain接口)
// ├── AndroidTaskRepository(androidMain实现)
// └── IOSTaskRepository(iosMain实现)
//
// 依赖注入层(commonMain)
// ├── ServiceLocator(管理服务生命周期)
// ├── PlatformFactory(expect/actual,平台工厂)
// └── AppModule(初始化入口)
从这张图可以清楚地看到:UI层各自写,但业务逻辑层和依赖注入层是完全共享的。只有最底层的存储/网络实现是平台相关的。这种架构下,代码复用率可以从30%提升到70%以上。
4.6 实际使用示例
了解了架构之后,来看看在两个平台上怎么使用这些共享代码:
// 技术栈:Kotlin Multiplatform + Gradle
// androidApp/src/main/java/com/example/demoapp/MainActivity.kt
package com.example.demoapp
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.example.demoapp.usecase.TaskUseCase
import com.example.demoapp.di.ServiceRegistry
import com.example.demoapp.model.TaskPriority
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.collectLatest
/**
* Android主界面
* 通过AppModule获取共享的业务逻辑
*/
class MainActivity : AppCompatActivity() {
private lateinit var scope: CoroutineScope
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
scope = CoroutineScope(Dispatchers.Main)
// 初始化应用模块(只需在启动时调用一次)
AppModule.initialize()
// 获取共享的任务用例
val taskUseCase = AppModule.serviceLocator.get<TaskUseCase>(
ServiceRegistry.TASK_USE_CASE
)
// 监听紧急任务的变化
scope.launch {
taskUseCase.getUrgentTasks().collectLatest { tasks ->
// 更新UI显示紧急任务列表
runOnUiThread {
// 你的UI更新逻辑
}
}
}
// 创建新任务的示例
scope.launch {
val result = taskUseCase.createTask(
title = "完成KMP架构设计",
description = "梳理分层架构和模块化方案",
priority = TaskPriority.HIGH
)
result.onSuccess {
Toast.makeText(this@MainActivity, "任务创建成功", Toast.LENGTH_SHORT).show()
}
result.onFailure { e ->
Toast.makeText(this@MainActivity, "创建失败:${e.message}", Toast.LENGTH_SHORT).show()
}
}
}
}
// 技术栈:Kotlin Multiplatform + Gradle
// iosApp/ContentView.swift 中的Kotlin互操作使用示例
// 通过Kotlin生成的framework调用共享模块
//
// import KotlinShared
//
// class MainViewController: UIViewController() {
// override func viewDidLoad() {
// super.viewDidLoad()
//
// // 初始化应用模块
// AppModule.shared.initialize()
//
// // 获取任务用例
// let serviceLocator = AppModule.shared.serviceLocator
// let taskUseCase = serviceLocator.get(
// key: ServiceRegistry.taskUseCase
// ) as! TaskUseCase
//
// // 创建任务
// let result = taskUseCase.createTask(
// title: "完成KMP架构设计",
// description: "梳理分层架构和模块化方案",
// priority: TaskPriority.HIGH
// )
// }
// }
五、应用场景分析
这种架构适合哪些场景呢?
第一个场景是跨平台团队协作。如果你的团队里有iOS开发、Android开发和共享模块开发,分层架构可以让他们各自负责各自的层,减少沟通成本。iOS团队只关心iosMain和iosApp,Android团队只关心androidMain和androidApp,共享模块团队只关心commonMain。
第二个场景是需要高复用率的商业项目。比如你要做多个产品线,每个产品线都需要类似的任务管理功能。有了共享模块和模块化设计,你可以在不同项目中复用shared-core和shared-business,只需替换网络层的API地址和存储层的实现。
第三个场景是长期维护的项目。代码分层后,每层的变更范围是可控的。修改存储层不影响业务层,修改业务层不影响UI层。这意味着你可以更安全地迭代功能,不用担心改了一处坏了另一处。
第四个场景是第三方库封装。如果你开发了一个网络库或UI组件库,想让它同时支持iOS和Android,用KMP共享模块的方式发布SDK是最合理的选择。
六、技术优缺点分析
6.1 优点
KMP共享模块的核心优势在于"业务逻辑写一次"。这在业务逻辑复杂的项目中价值最大。一个典型的电商应用,可能有上百个业务规则,如果每个规则都要在两个平台上写一遍,维护成本会非常高。KMP让这些规则只写一次,两个平台自动同步。
另一个优势是类型安全。KMP是强类型的,不像JavaScript那样容易出运行时错误。你在commonMain中定义的接口,编译器会强制你提供所有平台的实现,漏了会直接报错。
还有测试的优势。commonMain中的代码可以在JVM上运行测试,不需要真实的设备环境。你可以用mock的方式模拟平台实现,快速验证业务逻辑。
6.2 缺点
第一个缺点是学习曲线。expect/actual机制、源集概念、Gradle配置……这些对不熟悉KMP的开发者来说有门槛。特别是对于从纯Android或纯iOS转过来的开发者,需要适应新的思维模式。
第二个缺点是调试困难。当KMP共享模块出问题时,定位起来比原生开发要麻烦一些。你不太确定问题出在commonMain的逻辑错误,还是平台实现有问题。特别是iOS端,Xcode的调试工具对KMP的支持还不够完善。
第三个缺点是生态不如原生丰富。KMP的第三方库数量远少于Android和iOS各自的原生生态。虽然像SQLDelight、Ktor这些KMP友好的库质量不错,但很多iOS专属的库在KMP中可能找不到对应。
第四个缺点是二进制大小。KMP共享模块会在iOS端生成一个framework,这个framework会增加app的包体积。如果你的app本身很小,这个开销可能占比不小。
七、注意事项
使用这种架构时,有几个坑需要特别注意:
第一,不要在commonMain中使用@JvmStatic或@JvmOverloads注解。这些注解只在JVM上有效,用在KMP中会导致编译错误。
第二,iOS的cinterop文件要放在正确的位置。cinterop def文件必须放在src/nativeInterop/cinterop/目录下,否则不会自动加载。
第三,注意并发安全。Kotlin协程在不同平台上的调度器不同,androidMain用Dispatchers.Main,iosMain用Dispatchers.Main对应的iOS调度器。如果你在commonMain中用了withContext(Dispatchers.IO),在iOS上会出问题,因为Dispatchers.IO在iOS端不可用。
第四,序列化要注意。kotlinx.serialization是KMP友好的,但第三方序列化库(比如Gson)只支持JVM。如果你需要跨平台序列化,优先选择kotlinx.serialization。
第五,版本管理。KMP的Gradle插件版本和Kotlin版本需要匹配。如果你用了较新的Kotlin版本,需要确保KMP插件也支持。建议定期检查版本兼容性。
第六,模块拆分粒度。不要拆得太细,也不要太粗。每个子模块应该有一个清晰的职责。如果模块之间有循环依赖,说明拆分方式有问题,需要重新设计。
第七,测试覆盖率。commonMain的测试可以在JVM上跑,iosMain和androidMain的测试需要各自平台的环境。建议至少保证commonMain的代码测试覆盖率超过80%。
第八,CI/CD配置。KMP项目的CI配置比单平台复杂一些。你需要同时配置Android的Gradle构建和iOS的Xcode构建。建议使用GitHub Actions或GitLab CI的多阶段构建。
八、总结
KMP共享模块的代码复用率提升,本质上是一个架构设计问题。不是靠某种神奇的技术魔法,而是靠合理的分层、清晰的接口定义和模块化的组织方式。
核心思路可以概括为三点:第一,把平台无关的代码放到commonMain,确保这部分代码的复用率是100%;第二,用expect/actual机制隔离平台差异,让平台实现只负责"怎么实现",不负责"做什么";第三,用依赖注入把接口和实现解耦,让业务代码不依赖具体的平台实现。
按照这个思路搭建的KMP项目,代码复用率可以显著提升。更重要的是,这种架构让代码更易维护、更易测试、更易扩展。当你需要新增一个平台(比如Flutter或者鸿蒙)时,只需要在对应的源集里实现接口,commonMain的代码完全不用改。
最后说一句,KMP不是万能的。如果你的项目UI差异非常大,或者业务逻辑很简单,KMP可能不值得投入。但在业务逻辑复杂、需要长期维护的跨平台项目中,合理的KMP架构设计会让你的团队事半功倍。
评论
围绕“Kotlin Multiplatform项目中,共享模块的代码复用率提升的架构思路与实践”参与讨论