使用 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/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"
)
}
现在,您只需使用即可saveValueLocally
,Kotlin 知道它应该NSUserDefaults
在 iOS 和SharedPreferences
Android 上使用。
您可以对平台上任何可能产生差异的事情执行这些操作,例如Date
。
我们可以分享什么?
为了最大限度地实现 Android 和 iOS 之间的代码共享,我们尽可能地共享所有代码。这些代码包括用于网络和缓存的数据层、用于业务逻辑的领域层,以及包含应用逻辑的表示层的一部分。我们将表示层保留为不同的版本,以使其与平台保持一致。这意味着在 Android 和iOS 上分别使用/ 。这是我们无法共享的,而且在不同平台上也存在很大差异。Activity
Fragment
ViewController
设置 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")
}
该plugins
代码块表明这是一个 Android 库和一个多平台项目。这样我们就可以同时配置多平台和 Android 了。
在代码块内部kotlin
,我们可以指定目标。这里我们指定了ios
和android
。这将创建我们可以进一步配置的目标。
我们还可以向目标添加依赖项。在代码片段中,我们刚刚添加了 Kotlin 库。我们将它们添加到所有目标中,以便 Kotlin 知道如何在每个目标上进行编译。
注意这个android
块。这里我们只是将其配置为将默认文件夹重命名main
为androidMain
just,以便文件夹更有意义。
此配置将使项目具有以下结构。
SharedCode
├── build.gradle.kts
├── src
| ├── androidMain
| | ├── AndroidManifest.xml
| | ├── res
| | └── kotlin
| ├── iosMain
| | └── kotlin
| └── commonMain
| └── kotlin
└── etc
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")
}
只需进行一些设置,我们需要HttpClientEngine
为每个平台指定
// commonMain
expect val engine: HttpClientEngine
// androidMain
actual val engine by lazy { Android.create() }
// iosMain
actual val engine by lazy { Ios.create() }
现在,我们可以创建一个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")
}
该client
变量根据要使用的引擎(Android/iOS)初始化 HttpClient。在这里,我们还初始化它以便能够使用 解析 JSON,KotlinxSerializer
并为我们的 Item 注册序列化器(稍后您将看到 item)。这会告诉 Ktor 如何从 JSON 字符串解析 Item。
完成该设置后,我们就可以使用客户端并通过它执行请求了。client.get
,,client.post
等等……
好了,共享网络代码就完成了。我们已经可以在 Android 和 iOS 上使用了。
领域层共享
这里我们将业务逻辑放入应用中。在本例中,我们可以将实体模型放在这里。
@Serializable
data class Item(val value: String)
这里我们只共享实体的数据模型。另外,请注意@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() }
}
}
}
Android 已经提供了通过架构组件构建的实用程序,因此我们可以充分利用它们。iOS 没有,所以我们必须自己创建。幸运的是,所需的组件并不多。
为了BaseViewModel
能够将数据变化传播到视图,我们可以使用协程的Flow
。
挂起函数未编译为 ObjC 语言,因此我们无法在 iOS 上使用它们,但得益于CFlow
KotlinConf 的支持,我们可以这样做。源代码请见此处。
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()
}
}
}
}
基本上,CFlow
它只是包装Flow
并暴露了一个常规watch
函数,这样我们就可以观察它们并传递一个 lambda 表达式,而不是使用带有 的挂起函数Flow
。此外,还有一些辅助函数可以将Flow
和转换ConflatedBroadcastChannel
为CFlow
。继续获取FlowUtils.kt
文件并将其添加到您的项目中。
watch
使用泛型,为了在 Swift 代码中启用泛型,我们必须进行一些配置。
ios() {
compilations {
val main by getting {
kotlinOptions.freeCompilerArgs = listOf("-Xobjc-generics")
}
}
}
private val _dataToPropagate = ConflatedBroadcastChannel<String>()
val dataToPropagate = _dataToPropagate.wrap()
fun someFunction() {
_dataToPropagate.offer("The Data")
}
上面是使用ConflatedBroadcastChannel
并向Flow
视图模型的消费者提供数据的代码片段。我们使用ConflatedBroadcastChannel
just 是为了让它只保存视图所需的最新值。
对于 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())
}
}
我们还添加了一个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
}
}
现在,我们有了一个共享视图模型。剩下的就是如何在 Android 和 iOS 项目中使用它。
在 Android 中使用共享代码
幸运的是,由于 Android 本身也是一个 Gradle 项目,因此在 Android 中使用共享代码非常容易。只需将其作为依赖项添加到 Android 项目的 Gradle 配置中即可。
dependencies {
…
implementation(project(":SharedCode"))
}
添加它,我们可以在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
}
}
}
如果你检查过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"
}
}
添加此配置将添加一个podspec
任务,该任务将生成一个podspec
可在 iOS 项目中引用的文件。要在 iOS 中使用 CocoaPods,请按照以下步骤操作。
通过运行 podspec 任务./gradlew SharedCode:podspec
来获取文件。
在iOS项目中,可以通过如下方式引用podspec文件:
pod "SharedCode", :path => 'path-to-shared-code/SharedCode.podspec'
然后运行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
}
}
}
瞧!您刚刚在 Android 和 iOS 中使用了 SharedCode。
以下是我们刚刚所做工作的直观摘要:
我们使用 Kotlin Multiplatform 来实现 Android 和 iOS 代码共享。我们还使用了多平台库,例如用于网络的 Ktor、
用于 JSON 解析的 Serialization 以及用于异步任务的 Coroutines。
Kotlin Multiplatform 非常有前景,随着 Kotlin 1.4 的推出,这项技术还有更多值得关注的地方。
要查看更具体的示例,您可以查看显示笑话列表的这个示例项目。
就是这样!这就是我在 Kotlin Multiplatform 之旅中总结的点子。希望有人能从中有所收获,或者能引导别人尝试一下。
感谢阅读!希望你喜欢!
资源
- Kotlin Multiplatform 的清洁架构示例
- Kotlin Multiplatform 实现清洁架构
- 使用 Kotlin Multiplatform 瞄准 iOS 和 Android
- 适用于 Android 和 iOS 的 Kotlin 多平台项目:入门
- 如何开始使用 Kotlin Multiplatform 进行移动开发
- KotlinConf 2019:分享即关爱 - 面向 Android 开发者的 Kotlin 多平台,作者:Britt Barak
- KotlinConf 2019:在 iOS 和 Android 上发布移动多平台项目(作者:Ben Asher 和 Alec Strong)
- KotlinConf 2019:Kotlin Multiplatform 实践,作者:Alexandr Pogrebnyak
- KotlinConf 2019:1.3.X 及更高版本的 MPP,作者:Dmitry Savvinov 和 Liliia Abdulina
- KotlinConf 2019:你的多平台 Kaptain 已到来,作者:Ahmed El-Helw
- 实用 Kotlin 原生并发
- 陌生线索
- KotlinConf 2019:Kevin Galligan 讲解 Kotlin 原生并发