【FCon上海】与行业领袖共话AI大模型、数字化风控等前沿技术。 了解详情
写点什么

iOS 跨平台开发:Kotlin Multiplatform 入门

作者:Ben Whitley

  • 2019-05-09
  • 本文字数:9415 字

    阅读完需:约 31 分钟

iOS跨平台开发:Kotlin Multiplatform入门

AI 大模型超全落地场景&金融应用实践,8 月 16 - 19 日 FCon x AICon 大会联诀来袭、干货翻倍!

本文要点

  • Kotlin Multiplatform 让开发者在开发跨平台应用时省去很多重复编写代码逻辑的麻烦。

  • 用 KMP 并不能做出在各个平台完全共享的代码,因为很多时候 UI 逻辑需要原生编程,各平台的逻辑很难通用。

  • Swift 和 Kotlin 语法有很高的相似度,这极大降低了学习使用 KMP 业务逻辑的难度。

  • 可以使用 Android Studio 创建可复用的 KMP 组件,然后将其作为框架导入 Xcode 项目。

iOS 开发者的福音:Kotlin Multiplatform

DRY(避免重复劳动)是编程的基本原则之一,但开发跨平台应用时经常需要重复编写许多代码逻辑。出色的编程工作就是要尽量减少重复劳动,而应用使用的跨平台共享代码越多,重复劳动越少,代码质量也就越高。


想想你正在开发的 iOS 项目:它是否支持 Android 或 web 等平台?如果答案是肯定的,那么你的 iOS 应用与其它平台的版本共享了多少代码逻辑? 如果答案是否定的,但你准备开发支持其它平台的版本,那么跨平台开发会带来多少重复劳动?不管是哪个问题,答案都很可能是”很多“。


了解一下Kotlin Multiplatform(KMP)吧。Kotlin 是一种静态类型的编程语言,与 Swift 有着惊人的相似度,并可与 Java 完全互操作。从某些角度来看,你可以把它当作是 Android 上的 Swift。KMP 是 Kotlin 的一项功能组件,用它可以在应用的各平台版本之间共享代码,这样各个平台的原生 UI 就可以调用公共的底层代码了。KMP 并不能实现跨平台版本之间完全共享代码,但它也向这一目标迈出了重要的一步


KMP 使用 Kotlin 来编写跨平台应用中通用的业务逻辑。接下来各个平台的原生UI会调用这些公共逻辑。很多时候 UI 逻辑还是要原生开发的,因为各个平台的差异太大了。在 iOS 中,这意味着要将用 KMP 编写的.frameworkfile 像其它外部库一样导入到 Xcode 项目中。在 iOS 上使用 KMP 也要用到 Swift 语言,所以 KMP没法取代Swift


KMP 也可以迭代式引入项目,所以实现 KMP 不需要中断当前项目,不需要替换现有的 Swift 代码。你只要在下次为跨平台应用加入新功能的时候使用 KMP 编写业务逻辑,然后将其部署到各个平台,并开发原生 UI 就行了。在 iOS 这边,就是用 Kotlin 开发业务逻辑并用 Swift 开发 UI 逻辑。


Swift 和 Kotlin 的语法高度相似,所以学习用 KMP 编写业务逻辑会非常容易。需要下功夫学习的就只有一个 IDE 了:那就是 Android Studio。


Kotlin Multiplatform 项目仍然是 Kotlin 的实验性功能组件,所以每次更新后其 API 都可能有改动。

入门

本教程适用于几乎没接触过 Android Studio 或 Kotlin 的 iOS 开发者。如果你还没有安装 Android Studio,请按照安装指南操作。


把我们制作的入门项目克隆下来。它带有一个 KMP 样板项目,其中有一个空的 KMP 库。这个入门项目还有一个 iOS 应用和一个 Android 应用,两个应用都是用来显示一个 URL 列表的 GIF 图片的,列表中有 25 个硬编码的“meh”URL。


这两个应用是为你准备的,但那个库不是,因为本文的重点就是如何制作一个 KMP 库,不是教你应用该如何使用 KMP 库。但请放心,如果你想深入了解 Android 开发的话,这篇文章教你的知识也会很有用。


你要用 KMP 编写网络逻辑,在 Giphy 的公共 API 中搜索短语“whoa”,获取 25 个 URL,替换掉两个平台中原来的硬编码地址。


现在打开 Android Studio,选择 Open an existing Android Studio project。在弹出的 Finder 窗口中选择之前克隆来的启动项目的顶级 GifGetter/目录。


如果弹出这个对话框,请按“Update”。


Android Studio

如果你之前没有深入研究过 Android Studio,可能就要花些时间先来熟悉它了。这一章会讲解本教程用到的 Android Studio 的一些功能。如果你想深入了解这个 IDE,请点击此处


首先是基础操作:左边是项目导航器,应该会显示项目的文件结构。如果没看到文件结构,请点击左上角的“Projcet”选项卡,弹出项目导航器。在项目导航器顶部,你很可能会看到标有 Android 的下拉菜单。点一下,然后在下拉菜单中选择“Project”选项。



  1. Android Studio 顶部的按钮行是工具栏。这一栏有许多功能,如构建、运行、调试运行、应用新更改以及启动 Android 虚拟设备(AVD)管理器。在它下面是导航栏,显示项目文件夹结构中当前打开文件的位置,在本例中为:


/GifGetter/GifLibrary/commonMain/kotlin/platform/common.kt


  • 以下是工具栏中的一些工具介绍:

  • 锤子图形是 Build 按钮

  • 旁边的下拉列表是 configurations

  • 它旁边是 Run 按钮,就像在 Xcode 中一样


  1. Android 的官方文档把它叫做工具窗口之一,但人们一般叫它项目结构、文件结构、文件夹结构或类似术语。它和 Xcode 中的项目导航器是一个用途。


  • 请注意顶部的 Project 下拉菜单。这里它应该指的是本教程使用的 Project。如果它显示的是 Android 或别的什么内容,请重新选择 Project,否则你的文件和文件夹可能会在下面的结构视图里显示不全。


  1. 这里是编辑器窗口,写代码用的。注意顶部的文件:android.ktcommon.ktios.kt GifLibrary,这些都是当前打开的后台选项卡。右键单击其中一个选项卡就有选项可以在右侧、左侧、顶部或底部打开更多的编辑器窗口。

  2. 包在外面一圈的叫工具窗口栏,这里只用到黄圈中的选项卡,尤其是 Terminal Build 这俩。

项目结构


下面介绍 KMP 的项目结构:


  1. **.gradle/.idea/**文件夹:这里存放的是本地文件,不要把它们提交到多数 Android Studio 项目会使用的源码管理平台里。它们包括 IDE、项目和依赖项的本地设置,因此与本教程无关。

  2. androidApp/文件夹:这是整个 KMP 项目中 Android 应用项目的根目录,其中有 Android 端的所有 UI 逻辑。

  3. build/文件夹:存放 KMP 构建的输出。它也不要提交到源代码管理平台上。

  4. iosApp/文件夹:整个 KMP 项目中 Xcode 项目的根目录。


GifLibrary/文件夹:KMP 逻辑的主目录。它将作为 Android 项目的业务逻辑,然后 KMP 将它生成为 iOS 项目使用的 GifLibrary.framework。其中 src/文件夹包含以下内容:


  • androidLibMain:Android 平台专用的 KMP 逻辑

  • commonMain:不需要任何平台专属代码的 KMP 业务逻辑

  • iosMain:iOS 平台专用的 KMP 逻辑,能够与 Apple API 交互(如 UIKit、GCD 等)

  • main:包含一个 AndroidManifest.xml 文件,该文件将 GifLibrary/定义为 Android 库。很大程度上它就像一个 Xcode.plist。


  1. build.gradle:这可能是一个新概念,因为 iOS 开发中没有直接对应的概念。某种层次上它是一个构建脚本或 makefile,用于定义项目的依赖项、脚本和其它设置。这样来看,它也有点像.xcodeproj 文件。

  2. External Libraries:项目的所有依赖项。Android 和 KMP 项目与 iOS 项目的不同之处在于前者需要的外部依赖项要多很多。


项目顶层的其它文件(如 local.properties 和所有.iml 文件)由 Android Studio 生成,与我们在 KMP 中的操作无关。另外,settings.gradle 是应提交到源代码控制中的配置文件。与 build.gradle 文件不同,settings.gradle 不是构建脚本,而是 gradle 的配置文件。


现在你已经了解了 Android Studio 的基础知识,该来研究 KMP 项目了!

为 KMP 配置 Gradle

  • 注意:如前所述,KMP 仍然是实验性组件,因此这些 gradle 配置很容易发生变化。


打开 GifLibrary/build.gradle,然后看 kotlin {}块:


kotlin {   targets {       fromPreset(presets.android, 'androidLib')

def buildForDevice = project.findProperty("device")?.toBoolean() ?: false def iosPreset = (buildForDevice) ? presets.iosArm64 : presets.iosX64 def iOSTarget = System.getenv('SDK_NAME')?.startsWith('iphoneos') \ ? presets.iosArm64 : presets.iosX64 fromPreset(iosPreset, 'ios') { binaries { framework { // Disable bitcode embedding for the simulator build. if (!buildForDevice) { embedBitcode("disable") } } } }

fromPreset(iOSTarget, 'ios') { compilations.main.outputKinds('FRAMEWORK') }}

sourceSets { commonMain { dependencies { implementation "io.ktor:ktor-client-json:$ktor_version" } } }
复制代码


这是新建项目常见的 KMP gradle 配置。首先看一下 kotlin {}块内的 targets {}块。简单来说,fromPreset() {}为 gradle 提供了 KMP 的预设配置。iOS 的预设需要额外的代码,因为设备和模拟器构建所需的预设不一样。


然后看一下 sourceSets {}块。现在只有 commonMain 是有其它依赖项的 sourceSet,但另外两个 sourceSet 也需要其它依赖项。在 commonMain {}之后将以下内容添加到 sourceSets {}:


androidLibMain {   dependencies {       implementation "io.ktor:ktor-client-json-jvm:$ktor_version"   }}

iosMain { dependencies { implementation "io.ktor:ktor-client-ios:$ktor_version" implementation "io.ktor:ktor-client-json-native:$ktor_version" }}
复制代码


添加成功后应该会弹出一个消息框说:Gradle files have changed since the last project sync. A project sync may be necessary for the IDE to work properly(自上次项目同步以来 Gradle 文件已更改。IDE 可能需要项目同步才能正常工作)。暂时忽略此消息,因为这个 gradle 文件还要做很多改动。


现在三个必需的依赖项都已包含在 sourceSets 中了,但还需要最后一次更改才能让这个 gradle 准备就绪。在 kotlin {}块下面有一个名为 copyFramework {}的 task,它负责将 KMP 项目作为 iOS 框架复制到相应的文件夹中,这样 iosApp/ project 就能找到它了。


为了确保在项目构建时执行这些 task,请在 copyFramework {}之后插入下面这行:


tasks.build.dependsOn copyFramework
复制代码


现在点击 Sync Now 以更新 gradle 文件。需要下载新的依赖项时可能要花些时间。你可以在 Android Studio 底部的标签栏中查看 Gradle 同步的进度。

探索 Giphy 的 API

先去查阅 Giphy 的API并创建一个 Giphy 开发者帐户。如果你已经有帐户就登录进去。然后点击“Create an App”并在出现提示时输入“GifGetter”和描述信息。然后复制新生成的 Api 密钥。


现在返回 Android Studio 并打开 GifLibrary/src/commonMain/kotlin/GiphyAPI 并找到以下类:


class GiphyAPI {    val apiKey:String =“” }
复制代码


将你的 Giphy API 密钥粘贴到这个 val 语句的值中。这就是在 Kotlin 中声明类和字符串常量的方式。注意到目前为止,所有这些代码都可以用在 Swift 里,只有 val 关键字是例外,在 Kotlin 中它等同于 Swift 的 let。

平台专属代码

针对 iOS 和 Android 平台的差异部分,KMP 会告诉公共代码在各个平台会得到怎样的反馈。先转到 GifLibrary/src/commonMain/kotlin/platform/。点击 FileNewKotlin File/Class,然后创建一个新的 Kotlin 文件/类并将其命名为 common。然后在下面的 Kinddropdown 菜单中点击 File。


这些 iOS 和 Android 应用需要做网络调用,因此他们需要运行异步代码。iOS 使用 Grand Central Dispatch(GCD)处理此类并发操作,但由于 Android 不使用 GCD 所以这部分代码不能进入公共部分。相对应的,你要告诉公共代码它们能从各个平台得到的期望 expect。本例中,公共代码需要期望获得以下调度器 dispatcher:


import kotlinx.coroutines.CoroutineDispatcher

internal expect val dispatcher: CoroutineDispatcher
复制代码


这里将出现错误,因为 iosMan/androidLibMain/组件现在期望声明一个类型为 CoroutineDispatcher 的常量命名 dispatcher。在 iOS 这边,转到 GifLibrary/src/iosMain/platform/并创建一个名为 iOS 的新文件粘贴到下面的代码里:


internal actual val dispatcher: CoroutineDispatcher = NsQueueDispatcher(dispatch_get_main_queue())

internal class NsQueueDispatcher(private val dispatchQueue: dispatch_queue_t) : CoroutineDispatcher() { override fun dispatch(context: CoroutineContext, block: Runnable) { dispatch_async(dispatchQueue.freeze()) { block.run() } }}
复制代码


这个逻辑是说,对于 iOS 平台,KMP 将使用 GCD 获取主队列来运行异步代码。Android 这边,转到 Android 模块中的对应文件夹:GifLibrary/src/androidLibMain/platform/。创建另一个 Kotlin 文件并将其命名为 Android。所幸 Android 平台上可以轻松获取主调度器:


internal actual val dispatcher: CoroutineDispatcher = Dispatchers.Main
复制代码


这样应该就不会出现编译错误了,因为 commonMain 对两个平台各自期望的调度器都已经获得了对应的实现。

准备数据

现在我们需要能够代表我们从 Giphy API 获得的 JSON 数据的类。去 GifLibrary/src/commonMain/kotlin/中创建一个新的 Kotlin 文件,将其命名为 Data。将以下代码粘贴到新文件中:


import kotlinx.serialization.Serializable

@Serializabledata class GifResult( val `data`: List<Data>

)

@Serializabledata class Data( val images: Images)

@Serializabledata class Images( val original: Original)

@Serializabledata class Original( val url: Stri
复制代码


在这个文件顶部必须导入 Serializable 以识别数据类前面的 @Serializable 语句。数据类可以视为 Swift 中的结构。它们不是值类型对象,但它们的行为很像后者。从 Giphy API 获得的 JSON 将映射到这些数据类上。现在,commonMain 已经为大部分业务逻辑做好了准备。

编写 KMP 业务逻辑

现在回到 GiphyAPI 文件并将以下代码粘贴到 apiKey 下面:


private val client: HttpClient = HttpClient { // 1   install(JsonFeature) { // 2       serializer = KotlinxSerializer(Json.nonstrict).apply { // 3           setMapper(GifResult::class, GifResult.serializer()) // 4       }   }}

private fun HttpRequestBuilder.apiUrl(path: String) { // 5 url { // 6 takeFrom("https://api.giphy.com/") // 7 encodedPath = path // 8 }}
复制代码


下面逐条解释这段代码的效果:


  1. 声明一个类型为 HttpClient 的常量。这一行代码与 Swift 中写法的唯一区别还是关键字 val,不是 Swift 中的 let。

  2. 将 JsonFeature 安装到 client 对象中,使其能够序列化和反序列化 JSON。

  3. 实例化并配置 serializer,这是 HttpClient 对象的一个属性,默认值为 null。Json.nonstrict 格式表示响应的 JSON 将包含与下一行设置的数据类无关的字段。

  4. 为 serializer 提供顶级数据类,然后它会将响应的 JSON 序列化为 GifResult 对象。JSON 中的“data”字段将填充对应的 GifResult.data 属性。这些字段将继续填充嵌套数据类的层次结构。

  5. 将函数 apiUrl(path:String)添加到 HttpRequestBuilder 类。

  6. 构造一个 URL。

  7. 提供基础 URL。

  8. 使用指定的路径扩展 URL。


现在有了网络逻辑模版,就该对 Giphy 的端点做联网调用以获取“whoa”的搜索结果了。在你刚粘贴的网络代码下面添加以下代码:


suspend fun callGiphyAPI(): GifResult = client.get {   apiUrl(path = "v1/gifs/trending?api_key=$apiKey&limit=25&rating=G")}
复制代码


等一下,suspend 关键字是哪里来的?有什么用?其实这是通过协程在 Kotlin 中执行异步代码的一种方法。现在你只要知道这里只能从一个协程或其它挂起函数调用该函数就行了,例如 HttpClient 上的 get {}函数。


下一步是设置可用于 iOS 和 Android 应用的函数。这里的语法又很接近 Swift:


fun getGifUrls(callback: (List<String>) -> Unit) { // 1   GlobalScope.apply { // 2       launch(dispatcher) { // 3           val result: GifResult = callGiphyAPI() // 4           val urls = result.data.map { // 5               it.images.original.url // 6           }           callback(urls) // 7       }   }}
复制代码


  • 声明函数 getGifUrls(callback: (List) → Unit)。Android 和 iOS 代码将会调用这个函数。返回的 Unit 类型相当于 Swift 里的 void。

  • 在 GlobalScope 的上下文中运行后面的代码,其中包括它自己的调度器和作业取消逻辑。

  • 启动我们自己的 dispatcher,这里不用默认的调度器。记住,调度器的实现在各个平台是不一样的。

  • 声明一个值,等于 callGiphyApi()函数返回的值。

  • 映射 GifResult 对象的数据列表;

  • 映射完毕后就能获得每个 url 的属性了。url 是一个字符串,因此它将映射到 List 对象。Kotlin 中的 it 关键字表示 lambda 的参数,这里只有一个参数。

  • 将这个 URL 列表传递给作为函数参数提供的回调。

在 Android 平台上实现 KMP

Android 应用的最后一步是使用从 Giphy API 获取的 URL 替换原来的硬编码 URL 列表。其实这也没那么难啦。


首先打开 androidApp/src/main/kotlin/MainActivity 文件。Android 的 Activity 与 ViewController 密切相关,这里 MainActivity 中实现的第一个函数是 onCreate(savedInstanceState:Bundle?),这与 viewDidLoad()有点相似:


override fun onCreate(savedInstanceState: Bundle?) {   super.onCreate(savedInstanceState)   setContentView(R.layout.activity_main)

val gifList: RecyclerView = findViewById(R.id.gif_list) val layoutManager = LinearLayoutManager(this) gifList.layoutManager = layoutManager

adapter = GifAdapter(this) gifList.adapter = adapter adapter.setResults(urls)
复制代码


删掉这个函数的最后一行(原来这行是用 fakedata 设定输出的),换成:


getGifUrls {   adapter.setResults(it)}
复制代码


setResults(results: List)这个函数会将获取的 gif URL 设置为新的数据源并重新加载 RecyclerView,后者实际上是 UITableView 和 UICollectionView 的 Android 版本,不过它们还是有一些关键区别的。现在 Android 这边的所有逻辑就完成了!


如果它没能自动导入,你还遇到了未解决的引用错误,则可能需要将以下内容添加到文件顶部的导入列表中:


import org.gifLibrary.GiphyAPI
复制代码

在 Android 模拟器上运行

点击 Android Studio 顶部工具栏中的“Run”按钮。它的图标与 Xcode 的“运行”按钮很像。之后 Android 虚拟设备(AVD)管理器将显示一个窗口,类似 Xcode 管理模拟器的那个窗口。不同之处在于 Xcode 里的模拟器是现成的,无需再下载。


在新窗口中,如果你在列表中看到有什么可用的 Android 设备就选择它运行应用。否则就点击“Create New Virtual Device”。在设备列表中选择 Phone 类别和 Pixel 2 XL,然后点击右下方的 Next



接下来选择 Android 最新版本旁边的 Download,这样会下载 API 级别最高的系统版本。然后会出现另一个窗口来安装所选的 Android 版本。要是没看到下载按钮说明你已经下好了,那就选择该版本,然后再点击 Next。等系统下载完以后点击 Finish 就行了。


Android 这边就搞定了!好好庆祝一下,体验自己的成果吧。接下来回到熟悉的领域。

Xcode 设置

打开 Xcode 项目并转到 iosApp 目标的 Build Phases 并点击**+**,就是下图的红圈部分:



从弹出的列表中选择“New Run Script Phase”选项。你会在主窗口的列表末尾看到一个新的 Run Script。将其向上拖动到列表中,使其位于 Compile Sources 之上。单击它旁边的箭头并粘贴以下脚本:


cd "$SRCROOT/../"./gradlew GifLibrary:copyFramework \-Pconfiguration.build.dir="$CONFIGURATION_BUILD_DIR"          \-Pkotlin.build.type="$KOTLIN_BUILD_TYPE"                      \-Pdevice="$KOTLIN_DEVICE"
复制代码


这个运行脚本会读取 Xcode 项目的 Build Settings 中的定义,以便在 copyFramework gradle 任务中使用,回到 GifGetter/GifLibrary/build.gradle 中就能看到这个任务。但 Xcode 项目还没有这些设置。为了创建这些设置,请转到目标的 Build Settings 选项卡,然后点击**+**按钮,如下所示:



在弹出的小下拉菜单中选择 Add User-Defined Setting。将新设置命名为“KOTLIN_BUILD_TYPE”。单击它旁边的箭头以显示 Debug Release 环境。为 Debug 赋值“DEBUG”,为 Release 赋值“RELEASE”。


添加另一个用户定义的设置,并将其命名为“KOTLIN_DEVICE”。然后单击 Debug 右侧的**+按钮。这将在 Debug 下创建一个字段,表示 Any Architecture** | Any SDK。单击它以查看架构选项列表,然后选择 Any iOS Simulator SDK。为该字段赋值 false。对 Debug 做相同操作,但这次添加的是 Any iOS SDK 并将其赋值为 true


在“KOTLIN_DEVICE”下的 Release 中重复这些步骤。最后,你的 User-Defined Settings 应该变成这样:



然后在右上角的搜索栏中搜索“Framework Search Paths”:



将以下内容添加到 Debug Release Framework Search Paths


"$(SRCROOT)/../GifLibrary/build"
复制代码


KMP 会把**.framework 文件输出到这里,这个文件包含 iOS 应用所需的业务逻辑。在 Finder 中访问 GifGetter/GifLibrary/build/,你会看到 GifLibrary.framework**文件已准备好供 Xcode 使用了。


然后转到 iOSApp 目标的“General”选项卡,滚动到底部,然后 Embedded 点击 Embedded Binaries 下的**+按钮。在弹出的窗口中点击底部的“Add Other”;在弹出的 Finder 窗口中选择 GifGetter/GifLibrary/build/并选中 GifLibrary.framework 文件,然后按点击“Open”。这样就会把框架添加到 Linked Binaries and Frameworks**了。


打开 GifRetriever.swift,你会看到一个包含 25 个 URL 字符串的硬编码数组。在它下面是 requestGifs(_closure: @escaping StringsClosure)函数,负责传回硬编码 URL。在文件顶部的 import Foundation 语句下添加 import GifLibrary。


现在替换 requestGifs(_closure: @escaping StringsClosure) 函数的主体,像这样:


func requestGifs(_ closure: @escaping StringsClosure) {   GiphyAPI().getGifUrls { gifs -> KotlinUnit in       closure(gifs)       return KotlinUnit()   }}
复制代码


iOS 应用也可以进模拟器运行了! 现在你已经在两个平台上部署了同一个应用,两个版本通过 KMP 共享网络逻辑!

结语

可以在 GitHub 上访问已完成的项目以供参考。请记住,KMP 仍然是实验性组件,很容易发生变化,而且它在迅速发展和进化。不仅如此,已经有App Store中的现实应用成功应用KMP了。对于想要做跨平台开发的 iOS 开发者来说 KMP 是首选。


KMP 不仅是 iOS 工程师成为跨平台移动开发者的捷径,你还可以用它创建业务逻辑库,然后将其部署到其它平台。如果你希望将应用部署到 Javascript 环境,这个业务逻辑库就能派上用场了。


如果你想深入了解 KMP 或 Kotlin,请查看这些资源,其中包括 Kotlin reddit 版块和 Slack 频道的链接。美国东部时间 2019 年 6 月 4 日下午 1 点到 2 点,Touchlab 将举办一次网络研讨会,主题与本文相同。最后,下面是一些关于 KMP 的文章介绍以供查阅:


作者介绍

Ben Whitley 是纽约的一名 iOS 开发者,拥有 4 年的 Xcode、Swift 和 Objective-C 工作经验。在过去的一年半中,他一直是 Touchlab 的主力 iOS 开发人员,在此期间他与多家知名客户密切合作过。他目前致力于将 Kotlin Multiplatform 发展为 iOS 开发者的原生平台,并帮助 Android 开发者将他们的 iOS 和 Xcode 相关问题转化到 KMP 上解决。


查看英文原文Kotlin Multiplatform for iOS Developers


2019-05-09 08:008766

评论

发布
暂无评论
发现更多内容

玩转服务器之Docker篇:10分钟学会搭建 Docker 环境

京东科技开发者

云计算 容器 Docker 镜像 企业号 4 月 PK 榜

Selenium测试用例如何编写

测吧(北京)科技有限公司

测试

详解数据结构中栈的定义和操作

华为云开发者联盟

数据结构 开发 华为云 华为云开发者联盟 企业号 4 月 PK 榜

Gartner发布中国容器管理平台供应商识别指南,灵雀云实力入选

York

容器 云原生 系统架构 研究报告 平台选型

瓴羊quickbi工具免费体验30天,零基础上手企业数据更直观

对不起该用户已成仙‖

算法刷题-单词接龙、矩阵中的最长递增路径、Z 字形变换

共饮一杯无

数据结构 算法 三周年连更

户外led电子屏未来发展趋势

Dylan

技术 LED显示屏 户外LED显示屏

深度学习基础入门篇[六]:模型调优,学习率设置(Warm Up、loss自适应衰减等),batch size调优技巧,基于方差放缩初始化方法。

汀丶人工智能

人工智能 深度学习 学习率 warmup batchsize

企业级无代码平台,「重塑」软件生产关系

ToB行业头条

小技巧:如何让 Windows 应用程序在 Parallels Desktop 中启动得更快

互联网搬砖工作者

数字化转型框架如何搭建?

优秀

数字化转型

重新学习Java线程原语

码语者

Java 线程

从零学习SDK(8)SDK的集成和部署

MobTech袤博科技

OpenKruise V1.4 版本解读:新增 Job Sidecar Terminator 能力

阿里巴巴云原生

阿里云 开源 云原生 OpenKruise

从 Dev 和 Ops 视角出发,聊聊 DevSecOps 的 What / Why / How

极狐GitLab

DevOps 安全 DevSecOps 安全左移 安全合规

轻松玩转小程序,这样做让你拥有2亿用户

加入高科技仿生人

小程序 低代码 小程序制作 小程序开发

华为阅读发布最新进展,月活用户超1亿,大力发展精品阅读

最新动态

如何搭建自己的ChatGPT网站,它来了

派大星

ChatGPT

软件测试/测试开发丨Pytest 自动化测试框架(二)

测试人

软件测试 自动化测试 测试开发 pytest

Backgrounds——为所有人准备的mac动态壁纸,让桌面更生动

互联网搬砖工作者

Kubernetes网络策略之详解

乌龟哥哥

三周年连更

软件测试/测试开发丨Pytest 自动化测试框架(一)

测试人

软件测试 自动化测试 测试开发 pytest

中国边缘云公有云服务市场 Top2,百度智能云让智算无处不在

百度开发者中心

云计算 #百度智能云# 边缘云

电子元器件“切开后”,原来是这样子的!

元器件秋姐

科普 三极管 元器件 二极管 电感

如何从1到99做好产品 | 得物技术

得物技术

常用测试策略与测试手段

测吧(北京)科技有限公司

测试发开

今晚直播 | 思码逸陆春蕊:面对研发效能度量落地难点,如何让数据说话?

思码逸研发效能

研发效能

“亮相”欧洲!TDengine 在 KubeCon 与开发者探讨云原生与数据库的技术结合

TDengine

tdengine 时序数据库 KubeCON

白盒的测试方法

测吧(北京)科技有限公司

测试

带你掌握数仓的作业级监控TopSQL

华为云开发者联盟

数据库 后端 华为云 华为云开发者联盟 企业号 4 月 PK 榜

IPv6常见安全问题

穿过生命散发芬芳

ipv6 三周年连更

iOS跨平台开发:Kotlin Multiplatform入门_移动_InfoQ精选文章