Kotlin MPP expect / actual 具有不同的签名

Kotlin MPP expect / actual with different signatures

我有一个管理器 class,它有一个 Android 和 iOS 实现(来自第 3 方库)。 “例如 MyManagerImpl()”。要构造第 3 方管理器,iOS 不需要上下文,但 Android 需要。我创建了一个公共 class“MyManager”,它提取了需要在 commonMain 中调用的所有公共方法。

//commonMain
expect class MyManager {
  fun method1()
  companion object Factory {
    makeManager(): MyManager
  }
}

val manager = MyManager.Factory.makeManager() // ex intended usage

//androidMain
MyManagerImpl(context: Context) {
  fun method1()
}

actual class MyManager private constructor(manager: MyManagerImpl) {
  ..
  actual companion object Factory {
    override fun makeManager(): MyManager {
      return MyManager(MyManagerImpl(?how to get context?))
    }
  }
}

//iosMain
MyManagerImpl() {
  fun method1()
}

actual class MyManager private constructor(manager: MyManagerImpl) {
  ..
  actual companion object Factory {
    override fun makeManager(): MyManager {
      return MyManager(MyManagerImpl())
    }
  }
}

合并这两个实现的最简洁的方法是什么?即使它们具有不同的构造函数依赖性,是否也可以这样做?我们希望能够在 commonMain 中懒惰地构建 classes。这可能吗?

一般来说,没有一种超级干净的方法可以做到这一点。无法从 Android 全局获取 Context。虽然不漂亮,但我会做这样的事情:

//androidMain
class MyManagerImpl(context: Context) {
    fun method1(){}
}

actual class MyManager private constructor(manager: MyManagerImpl) {

    actual companion object Factory {
        lateinit var factoryContxet :Context
        override fun makeManager(): MyManager {
            return MyManager(MyManagerImpl(factoryContxet))
        }
    }
}

class SampleApplication : Application{
    override fun onCreate() {
        super.onCreate()
        MyManager.Factory.factoryContxet = this
    }
}

如果您希望能够从任何代码调用它,请在应用程序启动时初始化 Context。将其保存在静态参考中不会出现在每个人的最佳实践列表中,但这并不是一个技术问题。或者,您可以使用 Activity 做类似的事情,但它有自己的一系列问题。

我们这样做(bundleId 的示例 - 也许它会对您有所帮助):

expect fun bundleId(context: Any?): String?

androidMain:

actual fun bundleId(context: Any?): String? {
    (context as? Context)?.let {
        return AndroidIdentifier(it).getBundleId()
    }
    throw Exception("")
}

iosMain:

actual fun bundleId(context: Any?): String? =
        NSBundle.mainBundle.bundleIdentifier

上下文的依赖注入

我遇到了与 SQLDelight SqlDriver 相同的问题,它需要 Android 上的上下文,但不需要 iOS 上的上下文。使用 Kodein-DI 或 Koin,这可以在没有任何混乱的静态变量的情况下通过对上下文使用注入来完成。

基本概念是expect/actual用于创建platform-specific工厂class(ManagerFactory)。

在Android上,ManagerFactoryactual实现将上下文作为参数,可以从DI上下文中获取(for Kodein-DI on Android,见 androidXModule code and docs).

一旦在 android 和 iOS DI 模块中定义了工厂 class,它就可以在公共模块中 injected/retrieved,并检索MyManager 绑定到 DI 的实例,并在需要的地方使用。

使用 Kodein-DI 看起来像这样:

commonMain

//commonMain
expect class ManagerFactory {
    fun createManager(): MyManager
}

val sharedModule = DI.Module("common") {
    bind<MyManager>() with singleton { instance<ManagerFactory>().createManager() }

    // now you can inject MyManager wherever...
}

android主要

//androidMain
actual class ManagerFactory(private val context: Context) {
    actual fun createManager(): MyManager = MyAndroidManagerImpl(context)
}

val androidModule = DI.Module("android") {
    importAll(sharedModule)

    // instance<Context> via androidXModule, see `MainApplication` below
    bind<ManagerFactory>() with singleton { ManagerFactory(instance<Context>()) }
}

class MainApplication : Application(), DIAware {
    override val di by DI.lazy {
        // androidXModule (part of Kodein's android support library) gives us access to the context, as well as a lot of other Android services
        // see https://github.com/Kodein-Framework/Kodein-DI/blob/7.1/framework/android/kodein-di-framework-android-core/src/main/java/org/kodein/di/android/module.kt
        import(androidXModule(this@MainApplication))
        importAll(androidModule)
    }
}

iosMain

//iosMain
actual class ManagerFactory {
    actual fun createManager(): MyManager = MyNativeManagerImpl()
}

val iosModule = DI.Module("ios") {
    importAll(sharedModule)
    bind<ManagerFactory>() with singleton { ManagerFactory() }
}

我想到了另一个不需要静态引用的“hacky”解决方案:

data class PlatformDependencies(
  val androidContext: Any?,
  val someIosOnlyDependency: Any?
)
// in commonMain
expect class ManagerFactory(platformDeps: PlatformDependencies) {
  fun create(): Manager
}

// in androidMain
actual class ManagerFactory actual constructor(
  private val platformDeps: PlatformDependencies
) {
  override fun create(): Manager {
    val context = platformDeps.androidContext as? Context 
      ?: error("missing context in platform deps")     
    return AndroidManager(context)
  }
}

// in iosMain: similar to androidMain, but using "someIosOnlyDependency"

然后您必须在平台根 gradle 项目中将 PlatformDependencies 的实例与特定值绑定。

拥有一个仅包含特定于平台的 dep 的对象,您可以使用它来构建依赖于 DI 模块层次结构的任何平台相关 class。

它离优雅的解决方案还有很远的距离,它依赖于纪律,并且在某种程度上将平台的细节泄露到通用代码中,但它确实有效。

这是我们为数据库工厂解决这个问题所做的(没有依赖注入,因为我们还没有设置它):

我们在底层(没有其他依赖项)模块中创建了一个名为 AppContext 的 class。在 commonMain 我们有 expected class:

expect class AppContext private constructor()

然后在 androidMain 我们有这个:

actual class AppContext private actual constructor() {
    lateinit var context: Context
        private set

    constructor(context: Context) : this() {
        this.context = context
    }
}

iosMain 实现与 commonMain 相同,但具有 actual(如果需要,您可以执行类似于实际 Android 实现的操作访问一些其他属性):

actual class AppContext private actual constructor()

这里的关键是它具有相同的 expectedactual 构造函数,因此编译器认为它具有相同的签名并且是相同的 class,但是我们AndroidiOS.

实际上有不同的实现

我们在应用程序启动时创建此 AppContext 并将其传递给引导程序,然后我们将其传递给 DbFactory 以便我们可以创建相应的数据库。

class DbFactory(appContext: AppContext, userId: String) : DataBaseFactory {
    private val sqlDriver =
        appContext.createSqlDriver(dbFileName = "$userId$DB_FILE_NAME_SUFFIX")
}

以及 createSqlDriverexpectedactual 实现:

commonMain:

internal expect fun AppContext.createSqlDriver(dbFileName: String): SqlDriver

androidMain:

internal actual fun AppContext.createSqlDriver(dbFileName: String): SqlDriver =
    AndroidSqliteDriver(
        schema = KmmDb.Schema,
        context = context,
        name = dbFileName
    )

iosMain:

internal actual fun AppContext.createSqlDriver(dbFileName: String): SqlDriver =
    NativeSqliteDriver(
        schema = KmmDb.Schema,
        name = dbFileName
    )