【KMP跨平台】 如何优雅地调用平台API

2,136 阅读3分钟

前言

在使用 KMP进行开发时,我发现了一个令人头疼的事情。

在使用Android平台特性时,我们往往需要获取 Context 示例,与 iOS 的服务不同,Context 的实例并不是能到处能获取到的,这造成了编写平台特性的 API非常痛苦。

以下以一个简单的按键震动例子带大家优雅地调用平台 API。

原生API

以下先介绍下原生的震动 API 该怎么使用

  1. Android
    // androidMain
    fun vibrate(context: Context) {
        context.getSystemService(Vibrator::class.java)?.vibrate(
            VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK)
        )
    }
    
    
  2. iOS
    // iOSMain
    fun vibrate() {
        UIImpactFeedbackGenerator(
            UIImpactFeedbackStyle.UIImpactFeedbackStyleMedium
        ).impactOccurred()
    }
    

它们最大的区别是,Android 需要 Context 上下文,由于这一个参数的存在,我们无法在 common模块定义一个这样的 expect 函数。

// commonMain
expect fun vibrate() // we can't do this...

如何破局?

我们可以把 Context 上下文理解成一个提供震动的服务,以这种思维来思考的话,我们很容易能想到依赖注入,我们不关心服务从哪来,只需要外部注入进来供内部使用即可。

// androidMain
class Vibrator(private val context: Context) {
    fun vibrate() {
        context.getSystemService(Vibrator::class.java)?.vibrate(
            VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK)
        )
    }
}

自此我们得到了一个震动工具,同理也可以在 iOS 端声明,由于 iOS 不需要 Context 上下文,可以写出如下代码:

// iOSMain
class Vibrator {
    fun vibrate() {
        /*...*/
    }
}

而作为一个如此相似的工具,仅仅只有构造函数不同,有没有办法能够在 common 模块进行声明呢?答案是可以的,我们可以使用expect class。它就像一个接口,我们分别在不同的模块利用对应的平台特性去实现它。

// commonMain
expect class Vibrator {
    fun vibrate()
}

// androidMain
actual class Vibrator(private val context: Context) {
    fun vibrate() { /*...*/ }
}

// iOSMain
actual class Vibrator {
    fun vibrate() { /*...*/ }
}

自此我们得到了一个通用的 Vibrator,而他们的实现和构造都由各自的平台来完成。它类似一个接口,在 common模块无法构造。此时陷入了第二个困境:我们怎么构造这个通用的工具。

依赖注入

是的,Vibrator也可以通过依赖注入的方式来声明和获取。

利用 Koin ,我们声明一个模块,并在模块中声明一个构造Vibrator 的函数,而这个函数交给各个平台去实现。

val vibratorModule = module { vibrator() }

expect fun Module.vibrator()

在其他平台中我们可以使用平台的服务来构造对应的Vibrator,Context直接通过Koin依赖注入即可,这解决了一大难题。

// androidMain
actual fun Module.vibrator() {
    single { Vibrator(context = get()) }
}

// iOSMain
actual fun Module.vibrator() {
    single { Vibrator() }
}

到这里,我们就完成了所有准备工作,只需要在使用的地方通过 Koin 注入即可。

使用

这里以 Compose为例,以下声明一个获取 Vibrator 的函数。

@Composable
fun rememberVibrator(): Vibrator = koinInject()

然后我们在页面中可以直接使用了,非常简单!

@Composable
fun MainScreen() {
    val vibrator: Vibrator = rememberVibrator()
    Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
        Button(onClick = { vibrator.vibrate() }) {
            Text(text = "Click me to vibrate")
        }
    }
}

总结

通过依赖注入,我们解决了KMP 中的一个痛点,这使我们入门极为顺利!

源码

代码已开源:github.com/MReP1/Goose…