使用 Kotlin Multiplatform 最大化 Android 和 iOS 之间的代码共享 Kotlin Multiplatform 是如何工作的?预期/实际 我们可以共享什么?设置 Kotlin Multiplatform 数据层共享 领域层共享 表示层共享 在 Android 中使用共享代码 在 iOS 中使用共享代码 资源

2025-06-11

使用 Kotlin Multiplatform 最大化 Android 和 iOS 之间的代码共享

Kotlin 多平台

这是如何运作的?

预期/实际

我们可以分享什么?

设置 Kotlin Multiplatform

数据层共享

领域层共享

表现层共享

在 Android 中使用共享代码

在 iOS 中使用共享代码

资源

本文旨在探讨如何使用 Kotlin Multiplatform 在 Android 和 iOS 之间共享代码。您可能具备 Android 和 iOS 开发的相关知识,但具备这些知识将有助于您理解本文的主题。


Kotlin 多平台

Android 和 iOS 应用的功能通常相同,但我们最终还是会用不同的语言和工具来编写它们,以便能够在一个平台上运行。为了解决这些问题,我们开发了不同的跨平台技术,例如 React Native 和 Flutter,截至本文撰写时,它们是两个最值得关注的跨平台框架。而一个并不那么新鲜的技术正在逐渐受到关注。

现有的框架很好。毫无疑问,它们可以完成它的工作,但是,这需要你重写所有现有代码,并迁移到它们的框架中。这需要你重新培训你的工程师,让他们适应新的框架。此外,新框架只是通往原生世界的桥梁。它们只是替你完成了工作。如果你想在原生层面上做一些事情,你将无法做到,因为你被束缚在了它们的框架所能提供的功能上。这时,Kotlin Multiplatform 就派上用场了。

Kotlin Multiplatform 是 Jetbrain 对跨平台世界的全新诠释。您无需迁移至其他框架,只需共享所需内容,并始终忠于您正在构建的平台。您的工程师仍使用自己的技术栈。他们可能需要学习一些知识,但无需从头开始。您可以根据需要共享网络逻辑、缓存逻辑、业务逻辑和应用程序逻辑。有些平台只共享网络层。您可以根据用例进行配置,但在本文中,我们将介绍如何共享所有这些逻辑。

这是如何运作的?

Kotlin 编译目标

Kotlin 编译为不同的目标,这允许它为每个平台编译为不同的输出。 

Kotlin/JVM输出 JAR/AAR 文件,可供 Android 和 Spring Boot 等 Java 项目使用。

Kotlin/JS会从 Kotlin 生成 JS 文件,供您在其他 JS 文件中使用。这使得 Kotlin 可以在 React 和 Node 等框架上使用。

Kotlin/Native随后会输出二进制文件,供原生平台使用。它可以输出 Apple 框架,使其适用于 iOS 和 macOS 等 Apple 平台,也可以输出适用于 Windows 和 Linux 等其他原生平台的可执行文件。

通过将 Kotlin 编译为这些目标,我们可以编写一次 Kotlin 代码,Kotlin 会将该代码编译为您需要的特定目标,并生成可在该目标上使用的正确输出。

预期/实际

预期/实际

大多数情况下,你只需编写代码,然后让 Kotlin 将其编译成你想要的目标代码即可。但如果 Kotlin 不知道某些东西怎么办?假​​设你想在应用中存储一些值。在 Android 上,你可以使用SharedPreferences;在 iOS 上,有NSUserDefaults。默认情况下,Kotlin 并不知道这一点。它只知道如何将 Kotlin 代码编译成不同的目标代码,但你可以使用 expect/actual 机制让 Kotlin 知道这一点。 

expect会告诉 Kotlin 它可以做某件事,但它不知道怎么做,但目标平台知道怎么做。接下来actual就是平台声明如何去做。代码如下:

// Common Code
expect fun saveValueLocally(value: String)

// Android Code
actual fun saveValueLocally(value: String) {
    val sharedPreferences = 
    sharedPreferences.edit { putString("MyString", value) }
}

// iOS Code
actual fun saveValueLocally(value: String) {
    NSUserDefaults.standardUserDefaults.setValue(
        value, 
        forKey = "MyString"
    )
}
Enter fullscreen mode Exit fullscreen mode

现在,您只需使用即可saveValueLocally,Kotlin 知道它应该NSUserDefaults在 iOS 和SharedPreferencesAndroid 上使用。

您可以对平台上任何可能产生差异的事情执行这些操作,例如Date

我们可以分享什么?

在 Android 和 iOS 中共享

为了最大限度地实现 Android 和 iOS 之间的代码共享,我们尽可能地共享所有代码。这些代码包括用于网络缓存的数据层用于业务逻辑的领域层,以及包含应用逻辑的表示层的一部分。我们将表示层保留为不同的版本,以使其与平台保持一致。这意味着在 Android 和iOS 上分别使用/ 。这是我们无法共享的,而且在不同平台上也存在很大差异。ActivityFragmentViewController

设置 Kotlin Multiplatform

我们要做的第一件事是设置代码共享。我们创建一个 Gradle 模块,名称随意(本例中为 SharedCode),并需要告诉 Kotlin 它的目标。以下是共享代码模块的基本配置:

plugins {
    id("com.android.library")
    id("org.jetbrains.kotlin.multiplatform")
}

kotlin {
    ios()
    android()

    sourceSets["commonMain"].dependencies {
        implementation("org.jetbrains.kotlin:kotlin-stdlib-common")
    }
    sourceSets["iosMain"].dependencies {
        implementation("org.jetbrains.kotlin:kotlin-stdlib")
    }
}

android {
    sourceSets {
        getByName("main") {
            manifest.srcFile("src/androidMain/AndroidManifest.xml")
            java.srcDirs("src/androidMain/kotlin")
            res.srcDirs("src/androidMain/res")
        }
    }
}

dependencies {
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
}
Enter fullscreen mode Exit fullscreen mode

plugins代码块表明这是一个 Android 库和一个多平台项目。这样我们就可以同时配置多平台和 Android 了。

在代码块内部kotlin,我们可以指定目标。这里我们指定了iosandroid。这将创建我们可以进一步配置的目标。

我们还可以向目标添加依赖项。在代码片段中,我们刚刚添加了 Kotlin 库。我们将它们添加到所有目标中,以便 Kotlin 知道如何在每个目标上进行编译。

注意这个android块。这里我们只是将其配置为将默认文件夹重命名mainandroidMainjust,以便文件夹更有意义。

此配置将使项目具有以下结构。

SharedCode
├── build.gradle.kts
├── src
|   ├── androidMain
|   |   ├── AndroidManifest.xml
|   |   ├── res
|   |   └── kotlin
|   ├── iosMain
|   |   └── kotlin
|   └── commonMain
|       └── kotlin
└── etc
Enter fullscreen mode Exit fullscreen mode

commonMain是您放置共享代码的地方,如果您需要的话,androidMain也是iosMain放置平台代码的地方。

现在我们可以开始编写代码了。

数据层共享

这一层包含与数据相关的所有内容。这是我们为应用程序获取或存储数据的地方。为了简化本文,我们只讨论从远程源获取数据。

联网

幸运的是,目前已经有跨平台的网络库,因此我们可以使用Ktor作为 HTTP 客户端,使用Kotlin 序列化进行 JSON 解析,并使用Kotlin 协程处理异步任务。请阅读相关内容,以进一步了解这些库。

首先,我们需要在 Gradle 配置中添加依赖项。

kotlin { 
    
    sourceSets["commonMain"].dependencies {
        
        implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-common:1.3.3")
        implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime-common:0.14.0")
        implementation("io.ktor:ktor-client-core:1.2.6")
        implementation("io.ktor:ktor-client-json:1.2.6")
        implementation("io.ktor:ktor-client-serialization:1.2.6")
        implementation("io.ktor:ktor-client-ios:1.2.6")
    }

    sourceSets["iosMain"].dependencies {
        
        implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-native:1.3.3")
        implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime-native:0.14.0")
        implementation("io.ktor:ktor-client-ios:1.2.6")
        implementation("io.ktor:ktor-client-json-native:1.2.6")
        implementation("io.ktor:ktor-client-serialization-native:1.2.6")
    }
}

dependencies {
    
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3")
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.14.0")
    implementation("io.ktor:ktor-client-android:1.2.6")
    implementation("io.ktor:ktor-client-json-jvm:1.2.6")
    implementation("io.ktor:ktor-client-serialization-vm:1.2.6")
}
Enter fullscreen mode Exit fullscreen mode

只需进行一些设置,我们需要HttpClientEngine为每个平台指定

// commonMain
expect val engine: HttpClientEngine

// androidMain
actual val engine by lazy { Android.create() }

// iosMain 
actual val engine by lazy { Ios.create() }
Enter fullscreen mode Exit fullscreen mode

现在,我们可以创建一个ItemRepository使用 Ktor 执行网络请求来获取一些数据。

class ItemRepository {
    private val client = HttpClient(engine) {
        install(JsonFeature) {
            serializer = KotlinxSerializer().apply {
                register(Item.serializer().list)
            }
        }
    }

    suspend fun getItems(): List<Item> =
        client.get("https://url.only.fortest/items")
}
Enter fullscreen mode Exit fullscreen mode

client变量根据要使用的引擎(Android/iOS)初始化 HttpClient。在这里,我们还初始化它以便能够使用 解析 JSON,KotlinxSerializer并为我们的 Item 注册序列化器(稍后您将看到 item)。这会告诉 Ktor 如何从 JSON 字符串解析 Item。

完成该设置后,我们就可以使用客户端并通过它执行请求了。client.get,,client.post等等……

好了,共享网络代码就完成了。我们已经可以在 Android 和 iOS 上使用了。

领域层共享

这里我们将业务逻辑放入应用中。在本例中,我们可以将实体模型放在这里。

@Serializable
data class Item(val value: String)
Enter fullscreen mode Exit fullscreen mode

这里我们只共享实体的数据模型。另外,请注意@Serializable注解。这使得该类能够被序列化/反序列化为 JSON。

表现层共享

现在,我们在这里控制应用逻辑。控制哪些内容会被呈现,以及如何处理用户输入/交互。我们可以在这里共享 ViewModel。

首先,我们可以创建一个BaseViewModel在 Android 上使用架构组件、在 iOS 上使用原始 ViewModel 的组件。

// commonMain
expect open class BaseViewModel() {
    val clientScope: CoroutineScope
    protected open fun onCleared()
}

// androidMain
actual open class BaseViewModel actual constructor(): ViewModel() {
    actual val clientScope: CoroutineScope = viewModelScope
    actual override fun onCleared() {
        super.onCleared()
    }
}

// iosMain
actual open class BaseViewModel actual constructor() {
    private val viewModelJob = SupervisorJob()
    val viewModelScope: CoroutineScope = CoroutineScope(IosMainDispatcher + viewModelJob)

    actual val clientScope: CoroutineScope = viewModelScope

    protected actual open fun onCleared() {
        viewModelJob.cancelChildren()
    }

    object IosMainDispatcher : CoroutineDispatcher() {
        override fun dispatch(context: CoroutineContext, block: Runnable) {
            dispatch_async(dispatch_get_main_queue()) { block.run() }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Android 已经提供了通过架构组件构建的实用程序,因此我们可以充分利用它们。iOS 没有,所以我们必须自己创建。幸运的是,所需的组件并不多。

为了BaseViewModel能够将数据变化传播到视图,我们可以使用协程的Flow

挂起函数未编译为 ObjC 语言,因此我们无法在 iOS 上使用它们,但得益于CFlowKotlinConf 的支持,我们可以这样做。源代码请见此处。

fun <T> ConflatedBroadcastChannel<T>.wrap(): CFlow<T> = CFlow(asFlow())

fun <T> Flow<T>.wrap(): CFlow<T> = CFlow(this)

class CFlow<T>(private val origin: Flow<T>) : Flow<T> by origin {
    fun watch(block: (T) -> Unit): Closeable {
        val job = Job(/*ConferenceService.coroutineContext[Job]*/)

        onEach {
            block(it)
        }.launchIn(CoroutineScope(dispatcher() + job))

        return object : Closeable {
            override fun close() {
                job.cancel()
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

基本上,CFlow它只是包装Flow并暴露了一个常规watch函数,这样我们就可以观察它们并传递一个 lambda 表达式,而不是使用带有 的挂起函数Flow。此外,还有一些辅助函数可以将Flow和转换ConflatedBroadcastChannelCFlow。继续获取FlowUtils.kt文件并将其添加到您的项目中。

watch使用泛型,为了在 Swift 代码中启用泛型,我们必须进行一些配置。

ios() {
    compilations {
        val main by getting {
            kotlinOptions.freeCompilerArgs = listOf("-Xobjc-generics")
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
private val _dataToPropagate = ConflatedBroadcastChannel<String>()
val dataToPropagate = _dataToPropagate.wrap()

fun someFunction() {
    _dataToPropagate.offer("The Data")
}
Enter fullscreen mode Exit fullscreen mode

上面是使用ConflatedBroadcastChannel并向Flow视图模型的消费者提供数据的代码片段。我们使用ConflatedBroadcastChanneljust 是为了让它只保存视图所需的最新值。

对于 Android 开发者:
ConflatedBroadcastChannel= MutableLiveData
Flow=LiveData

有了这些实用程序,我们就可以开始制作视图模型功能了。
假设我们要查看一个项目列表。

class ViewItemsViewModel(
    private val itemsRepository: ItemsRepository
) : BaseViewModel() {
    private val _items = ConflatedBroadcastChannel<String>()
    val items = _items.wrap()

    init {
        clientScope.launch {
            _items.offer(itemsRepository.getItems())
        }
    }

    @ThreadLocal
    companion object {
        fun create() = ViewItemsViewModel(ItemsRepository())
    }
}
Enter fullscreen mode Exit fullscreen mode

我们还添加了一个create辅助函数来创建 ViewModel。ThreadLocal它有助于 Kotlin/Native 的并发模型。这是 K/N 的一个基础主题,我强烈推荐阅读 Kevin Galligan 的相关资料。

Android 还需要为其 ViewModel 创建一个工厂,以便缓存部分能够正常工作,因此我们创建了一个

// androidMain

class ViewItemsViewModelFactory : ViewModelProvider.Factory {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        return ViewItemsViewModel.create() as T
    }
}
Enter fullscreen mode Exit fullscreen mode

现在,我们有了一个共享视图模型。剩下的就是如何在 Android 和 iOS 项目中使用它。

在 Android 中使用共享代码

幸运的是,由于 Android 本身也是一个 Gradle 项目,因此在 Android 中使用共享代码非常容易。只需将其作为依赖项添加到 Android 项目的 Gradle 配置中即可。

dependencies {
    
    implementation(project(":SharedCode"))
}

Enter fullscreen mode Exit fullscreen mode

添加它,我们可以在Fragment

class ViewItemsFragment : Fragment(R.layout.fragment_view_items) {
   private val factory = ViewItemsViewModelFactory()
   private val viewModel by viewModels<ViewItemsViewModel> { factory }

    override fun onCreate() {
        viewModel.items.watch {
            // Refresh the RecyclerView contents
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

如果你检查过CFlow代码,watch会返回一个Closeable。稍后你需要清除它以防止内存泄漏。类似于 RxJava 的Disposable。你可以保留每个 的引用,close稍后再处理,或者创建一些东西来帮助你解决这个问题。

在 iOS 中使用共享代码

对于 iOS,还需要做更多工作。我们需要生成一个框架作为输出,并在 Xcode 上使用它。

为了方便起见,我们将使用 cocoapods 来处理设置。在SharedCode模块的 Gradle 配置中:

plugins {
    
    id("org.jetbrains.kotlin.native.cocoapods")
}

version = "1.0.0"

kotlin {
    cocoapods {
        summary = "Shared Code for Android and iOS"
        homepage = "Link to a Kotlin/Native module homepage"
    }
}
Enter fullscreen mode Exit fullscreen mode

添加此配置将添加一个podspec任务,该任务将生成一个podspec可在 iOS 项目中引用的文件。要在 iOS 中使用 CocoaPods,请按照以下步骤操作。

通过运行 podspec 任务./gradlew SharedCode:podspec来获取文件。

在iOS项目中,可以通过如下方式引用podspec文件:

pod "SharedCode", :path => 'path-to-shared-code/SharedCode.podspec'
Enter fullscreen mode Exit fullscreen mode

然后运行pod install

这将钩住配置,以便您可以在 iOS 中使用 SharedCode。这只会生成框架并引用它,但所有工作都由 CocoaPods 完成。

完成后,我们现在可以将其导入ViewController

import SharedCode

class ViewItemsViewController: UIViewController {
    let viewModel = ViewItemsViewModel.init().create()

    func viewDidAppear() {
        viewModel.items.watch { items in
            // Reload TableViewController 
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

瞧!您刚刚在 Android 和 iOS 中使用了 SharedCode。

以下是我们刚刚所做工作的直观摘要:

概括

我们使用 Kotlin Multiplatform 来实现 Android 和 iOS 代码共享。我们还使用了多平台库,例如用于网络的 Ktor、
用于 JSON 解析的 Serialization 以及用于异步任务的 Coroutines。

Kotlin Multiplatform 非常有前景,随着 Kotlin 1.4 的推出,这项技术还有更多值得关注的地方。

要查看更具体的示例,您可以查看显示笑话列表的这个示例项目。

GitHub 徽标 kuuuurt /笑话应用程序多平台

在 Android 和 iOS 中使用 Kotlin 代码的 Kotlin 多平台示例项目

就是这样!这就是我在 Kotlin Multiplatform 之旅中总结的点子。希望有人能从中有所收获,或者能引导别人尝试一下。

感谢阅读!希望你喜欢!

资源

链接链接 https://dev.to/kuuurt/maximizing-code-sharing-between-android-and-ios-with-kotlin-multiplatform-54h8
PREV
我想成为哪种开发人员?结论
NEXT
F# 中的纯函数式编程 组成部分 交互 未采用的路径 结论