KMM(Kotlin Multiplatform Mobile)入门(三)平台差异化实现

平台差异化代码的使用场景

由于 KMM 运行在各平台时,实际上是翻译成了各平台专用的库,如:Android 上就会将共享模块编译成 Dalvik Bytecode 然后打包成 AAR 文件,而 iOS 上会打包成 Apple Framework,所以,一些平台相关的、不可共享的具体实现代码,就必须利用各平台的 API 来实现

举个简单的例子,公共模块有一个统一的业务逻辑——获取手机型号,控制逻辑可以在 KMM 的 common 代码库中实现,且它并不关系具体的实现逻辑,而实际需要获取手机型号字符串的方法,Android 需要调用 android.os.Build.MODEL 获取,而 iOS 需要通过 UIDevice.current.model 来获取

类似的平台强相关功能,就需要在 KMM 中利用平台差异化代码实现

差异化代码的基本实现

这里需要再次引用 Kotlin 官方的一张图

如图所示,在 KMM 中,Common 库中可以将需要差异化实现的 classfun 使用 expect 关键字修饰,expect 字面意思是:期望,这里代表需要各平台具体实现,之后在 Android、iOS 对应的库中,再使用 actual 关键字来创建对应的 classfun ,即可完成差异化代码的构建,在 App 中调用 Common 中使用 expect 修饰的内容时,将自动执行不同平台的 actual 内容

Demo 及注意点

expect & actual 实现方式

这种方式是官方推荐的一种平台差异化功能实现方式,实际操作时也比较简单

在 Common 中建立一个 expect 类或 Top-Level 方法

如图所示,创建 expect 类或方法,有点类似 Kotlin 和 Java 中的 interface

在完成基本的代码以后,此时可以看到 IDE 已经报错了,提示没有找到给 Android 使用的 JVM 实现,以及给 iOS 使用的 Native 实现

完成 actual 实现

此时不要使用 Option + Enter(Windows:Alt + Enter) 来靠 IDE 自动创建, 需要分别在 androidMain 和 iosMain 目录中,创建expect 内容包名、类名、方法签名(包括方法名、参数类型及名称)完全一致的 actual 内容,否则会报错

Android 示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// AndroidImpl.kt

package com.coderyuan.myfirstkmm

import android.util.Log

actual class PlatformSpecific {

actual fun method1(): String {
return "Android-Method1"
}

actual fun method2() {
println("Android-Method2")
}
}

actual fun platformTestFunc(v1: Int, v2: String) {
Log.i("Android-Func", "v1: $v1, v2: $v2")
}

iOS 示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// iOSImpl.kt

package com.coderyuan.myfirstkmm

import platform.Foundation.NSLog

actual class PlatformSpecific {

actual fun method1(): String {
return "iOS-Method1"
}

actual fun method2() {
println("iOS-Method2")
}
}

actual fun platformTestFunc(v1: Int, v2: String) {
NSLog("iOS-Func: v1: %d, v2: %s", v1, v2)
}

完成上面的双端功能实现以后,如果在 Android 或 iOS 主 App 中调用 Common 中的方法,将会执行各自实现的 actual 方法

注入式实现

除了官方的 expect & actual 实现模式,还可以使用类似 IoC 的注入式实现,我们可以利用 Interface 或 Block 的形式来让双端实现需要的差异化功能

如果使用过 Spring Framework 或 Dagger,此实现则不难理解

定义

首先,在 Common 中定义好需要注入的方法,可以使用接口或闭包,并使用单例来承载,如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// PlatformSpecificMgr.kt

package com.coderyuan.myfirstkmm

import kotlin.native.concurrent.ThreadLocal

// 单例注意使用 @ThreadLocal 注解,以便给 var 变量赋值
@ThreadLocal
object PlatformSpecificMgr {

var ifuncImpl: IFunc? = null

var printLogBlockImpl: MyPrintLogBlock? = null
}

// 由于 iOS 端并不能为 object 生成类方法,所以需要单独写个 Top-Level 变量
val sharedInstance = PlatformSpecificMgr

interface IFunc {

fun printLogMethod(tag: String, content: String)

fun printLogMethod(tag: String, ts: Long, content: String)
}

typealias MyPrintLogBlock = ((tag: String, type: Int, content: String) -> Unit)

注入实现

此时,我们需要在 Android 及 iOS App 主工程的代码中注入具体的实现

根据需要,选择合适的生命周期,也可以使用懒加载形式的注入,比如,在 Android App 的 Application 类中注入,在 iOS App 的 AppDelegate 类中注入

Android 示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Android App 主工程中的 MyApplication.kt
package com.coderyuan.myfirstkmm.android

import android.app.Application
import android.util.Log
import com.coderyuan.myfirstkmm.IFunc
import com.coderyuan.myfirstkmm.PlatformSpecificMgr

class MyApplication: Application() {

override fun onCreate() {
super.onCreate()
PlatformSpecificMgr.ifuncImpl = object : IFunc {
override fun printLogMethod(tag: String, content: String) {
Log.i(tag, content)
}

override fun printLogMethod(tag: String, ts: Long, content: String) {
Log.i(tag, "time: ${ts}, $content")
}
}

PlatformSpecificMgr.printLogBlockImpl = { tag: String, type: Int, content: String ->
Log.i(tag, "type: ${type}, $content")
}
}
}

iOS 示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//
// AppDelegate.swift
// iosApp
//
// Created by yuanguozheng on 2021/5/28.
// Copyright © 2021 orgName. All rights reserved.
//

import Foundation
import UIKit
import shared
import os.log

class MyAppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {

let mgr = PlatformSpecificMgr.init()
mgr.ifuncImpl = LogImpl.init()

mgr.printLogBlockImpl = { (tag: String, type: KotlinInt, content: String) -> Void in
let log = String(format: "%@: %@, type: %d", tag, content, type.intValue)
print(log)
}

return true
}
}

class LogImpl : IFunc {
func printLogMethod(tag: String, ts: Int64, content: String) {
let log = String(format: "%@: %@, time: %l", tag, content, ts)
print(log)
}

func printLogMethod(tag: String, content: String) {
let log = String(format: "%@: %@", tag, content)
print(log)
}
}

那么,在使用时,可以按照如下的代码进行调用

1
2
3
4
5
6
7
8
9
10
// Android 代码 或 Common 中调用

// print a simple log
PlatformSpecificMgr?.ifuncImpl?.printLogMethod("MyLog", "test")

// print a simple log
PlatformSpecificMgr?.ifuncImpl?.printLogMethod("MyLog", 1234567890 ,"test")

// print a log with types by block
PlatformSpecificMgr?.printLogBlockImpl?.invoke("LogBlock", 1, "testBlock")

iOS 调用示例:

1
2
3
4
5
// print a simple log
PlatformSpecificMgrKt.sharedInstance.ifuncImpl?.printLogMethod(tag: "AAA", content: "BBB")

// print a log with types by block
PlatformSpecificMgrKt.sharedInstance.printLogBlockImpl?("CCC", 1, "ZZZ")

在 iOS 模拟器中运行一下,可以看到打出的 Log