Hello World —— 使用 Kotlin 開發跨平臺應用

原文作者: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 應用

  • JetBrains/kotlinconf-app
  • saket/press
  • jarroyoesp/KotlinMultiPlatform

➡️ 可用的 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()?


本文分享自微信公衆號 - 秉心說TM(gh_c6504b1af5ae)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章