【译】Hello World —— 使用 Kotlin 开发跨平台应用

2,663 阅读5分钟

原文作者:Aman Bansal

原文地址:Create Hello World App with KMM 📱- Android & IOS

译者:秉心说

在移动开发领域,Android 和 iOS 版本的应用程序通常会有很多共同点,背后的业务逻辑基本也是一致的。文件下载,读写数据库,从远程服务器获取数据,解析远程数据等等。所以我们为什么不只写一次业务逻辑代码,在不同的平台上共享呢?

有了这个想法之后,Jetbrains 带来了 Kotlin Multiplatform Project

➡️ 什么是 Kotlin Multiplatform Mobile?

Kotlin Multiplatform Mobile (KMM) 是由 Jetbrains 提供的跨平台移动开发 SDK 。借助 Kotlin 的 跨平台能力,你可以使用一个工程为多个平台编译。

使用 KMM,具备灵活性的同时也保留了原生编程的优势。为 Android/iOS 应用程序的业务逻辑代码使用单一的代码库,仅在需要的时候编写平台特定代码,例如实现原生的 UI,使用平台特定 API 等等。

KMM 可以和你的工程无缝集成。共享代码,使用 Kotlin 编写,使用 Kotlin/JVM 编译成 JVM 字节码,使用 Kotlin/Native 编译成二进制,所以你可以和使用其他一般类库一样使用 KMM 业务逻辑模块。

在写这篇博客的同时,KMM 仍然处于 Alpha,你可以开始尝试在你的应用中共享业务逻辑代码。

在移动开发领域,KMM 目前没有为大众所熟知。Jetbrains 开发了 Android Studio 的 KMM 插件 来帮助你快速设置 KMM 工程。插件还可以帮助你编写,运行,测试共享代码。

➡️ 一步一步构建 HELLO WORLD KMM 应用

  1. 在 Android Studio 上安装 Kotlin Multiplatform Mobile 插件。打开 Android Studio -> 点击 Configure -> 选择 Plugins

  1. 在 plugins 部分选择 Marketplace ,搜索 KMM,安装并重启 Android Studio。

  1. 在 Android Studio 首页选择 “Start a new Android Studio project” 。

  1. 在 “Select a project Template” 页面,选择 “KMM Application” 。

  1. 设置工程名称,最低 SDK,文件目录,包名等。

现在,你需要等待工程的第一次构建,需要花费一些时间去下载和设置必要的组件。

译者注:KMM 插件要求你的 Kotlin 插件版本至少为 4.0 版本以上

➡️ 运行你的程序

在菜单栏选择你要运行的平台,选择设备,点击 Run

要运行 iOS 应用,你需要安装 Xcode 和模拟器。

➡️ 瞅一眼代码

Android 开发者? 看起来很熟悉? 😎

IOS 开发者? 看起来就像外星人? 👽

➡️ 模块

  • shared 模块 —— 存放 Android/iOS 通用业务逻辑代码的 Kotlin 模块,会被编译为 Android library 和 iOS framework。使用 Gradle 进行构建。

  • androidApp 模块 —— Android 应用的 Kotlin 模块。使用 Gradle 构建。

  • iosApp 模块 —— 构建 iOS 应用的 Xcode 工程。

Project 的 build.gradle.kts 文件:

buildscript {
    repositories {
        gradlePluginPortal()
        jcenter()
        google()
        mavenCentral()
    }
    dependencies {
        classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.10")
        classpath("com.android.tools.build:gradle:4.0.1")
    }
}
group = "com.aman.helloworldkmm"
version = "1.0-SNAPSHOT"

repositories {
    mavenCentral()
}

➡️ Shared module

shared 模块包含了Android 和 iOS 的公用代码。但是,为了在 Android/iOS 上实现同样的逻辑,有时候你不得不写两份版本特定代码,例如蓝牙,Wifi 等等。为了处理这种情况,Kotlin 提供了 expect/actual 机制。shared 模块的源代码按三个源集进行分类:

  • commonMain 下存储为所有平台工作的代码,包括 expect 声明
  • androidMain 下存储 Android 的特定代码,包括 actual 实现
  • iosMain 下存储 iOS 的特定代码,包括 actual 实现

每一个源集都有自己的依赖,Kotlin 标准库依赖会自动添加到所有源集,你不需要在编译脚本中声明。

build.gradle.kts

这份 build.gradle.kts 文件包含了 shared 模块对于 Android/iOS 的配置。

import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget

plugins {
    kotlin("multiplatform")
    id("com.android.library")
    id("kotlin-android-extensions")
}
group = "com.aman.helloworldkmm"
version = "1.0-SNAPSHOT"

repositories {
    gradlePluginPortal()
    google()
    jcenter()
    mavenCentral()
}
kotlin {
    android()
    ios {
        binaries {
            framework {
                baseName = "shared"
            }
        }
    }
    sourceSets {
        val commonMain by getting
        val commonTest by getting {
            dependencies {
                implementation(kotlin("test-common"))
                implementation(kotlin("test-annotations-common"))
            }
        }
        val androidMain by getting {
            dependencies {
                implementation("com.google.android.material:material:1.2.0")
            }
        }
        val androidTest by getting {
            dependencies {
                implementation(kotlin("test-junit"))
                implementation("junit:junit:4.12")
            }
        }
        val iosMain by getting
        val iosTest by getting
    }
}
android {
    compileSdkVersion(29)
    sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
    defaultConfig {
        minSdkVersion(24)
        targetSdkVersion(29)
        versionCode = 1
        versionName = "1.0"
    }
    buildTypes {
        getByName("release") {
            isMinifyEnabled = false
        }
    }
}
val packForXcode by tasks.creating(Sync::class) {
    group = "build"
    val mode = System.getenv("CONFIGURATION") ?: "DEBUG"
    val sdkName = System.getenv("SDK_NAME") ?: "iphonesimulator"
    val targetName = "ios" + if (sdkName.startsWith("iphoneos")) "Arm64" else "X64"
    val framework = kotlin.targets.getByName<KotlinNativeTarget>(targetName).binaries.getFramework(mode)
    inputs.property("mode", mode)
    dependsOn(framework.linkTask)
    val targetDir = File(buildDir, "xcode-frameworks")
    from({ framework.outputDirectory })
    into(targetDir)
}
tasks.getByName("build").dependsOn(packForXcode)

androidApp 模块的 build.gradle.kts 文件

plugins {
    id("com.android.application")
    kotlin("android")
    id("kotlin-android-extensions")
}
group = "com.aman.helloworldkmm"
version = "1.0-SNAPSHOT"

repositories {
    gradlePluginPortal()
    google()
    jcenter()
    mavenCentral()
}
dependencies {
    implementation(project(":shared"))
    implementation("com.google.android.material:material:1.2.0")
    implementation("androidx.appcompat:appcompat:1.2.0")
    implementation("androidx.constraintlayout:constraintlayout:1.1.3")
}
android {
    compileSdkVersion(29)
    defaultConfig {
        applicationId = "com.aman.helloworldkmm.androidApp"
        minSdkVersion(24)
        targetSdkVersion(29)
        versionCode = 1
        versionName = "1.0"
    }
    buildTypes {
        getByName("release") {
            isMinifyEnabled = false
        }
    }
}

➡️ 使用 Expect/Actual 关键字

对于跨平台应用来说,版本特定代码是很常见的。例如你可能想知道你的应用是运行在 Android 还是 iOS 设备,并且得到设备的具体型号。为了完成这个功能,你需要使用 expect/actual 关键字。

首先,在 common 模块中使用 expect 关键字声明一个空的类或函数,就像创建接口或者抽象类一样。然后,在所有的其他模块中编写平台特定代码来实现对应的类或函数,并用 actual 修饰。

注意,如果你使用了 expect,你必须提供对应名称的 actual 实现。

否则,你会得到如下错误:

➡️ Expect/Actual 的使用

commonMain

expect class Platform() {
    val platform: String
}

androidMain

actual class Platform actual constructor() {
    actual val platform: String = "Android ${android.os.Build.VERSION.SDK_INT}"
}

iosMain

import platform.UIKit.UIDevice


actual class Platform {
    actual val platform: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion
}

MainActivity.kt (Android)

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

        val tv: TextView = findViewById(R.id.text_view)
        tv.text = "Hello World, ${Platform().platform}!"
    }
}

ContentView.swift (iOS)

struct ContentView: View {
    var body: some View {
        Text("Hello World, "+ Platform().platform)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

恭喜!! 你已经完成了你的第一个 KMM app 。

➡️开源 KMM 应用

➡️ 可用的 KMM 类库

AAkira/Kotlin-Multiplatform-Libraries

译者说

在已经一片红海的移动端跨平台开发领域,Kotlin 另辟蹊径,让你可以继续使用平台原生方式开发 UI,在业务逻辑上做到 “Write once,run everywhere”。甚至放飞一下自我,未来的某一天是不是可以用 Flutter 做 UI 上的通用,用 Kotlin 做业务逻辑上的通用?

不管怎样,最终还是得开发者买账才行。不知道你怎么看 KMM,在评论区留下的你的看法吧!

最后打个广告,推荐一波我的小专栏,面向面试的 Android 复习笔记 ,目前已经输出六篇文章,感兴趣的可以给个订阅。

Android 复习笔记目录

  1. 唠唠任务栈,返回栈和启动模式
  2. 唠唠 Activity 的生命周期
  3. 扒一扒 Context
  4. 为什么不能使用 Application Context 显示 Dialog?
  5. OOM 可以被 try catch 吗?
  6. Activity.finish() 之后 10s 才 onDestroy()?