KotlinMPP预期值/实际值,签名不同

ijnw1ujt  于 2023-01-26  发布在  Kotlin
关注(0)|答案(6)|浏览(139)

我有一个管理器类,它有一个Android和iOS impl(来自第三方库)。“ex. MyManagerImpl()"。要构造第三方管理器,iOS不需要上下文,但Android需要。我创建了一个公共类“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中懒惰地构造类。这可能吗?

dohp0rv5

dohp0rv51#

上下文的依赖注入

SQLDelight SqlDriver也有同样的问题,它需要Android上的上下文,但在iOS上不需要。使用Kodein-DI或Koin,通过使用上下文注入,无需任何混乱的静态变量就可以做到这一点。
基本概念是expect/actual用于创建特定于平台的工厂类(ManagerFactory)。
在Android上,ManagerFactoryactual实现将上下文作为参数,可以从DI上下文中获取(对于Android上的Kodein-DI,请参见androidXModulecodedocs)。
一旦在android和iOS DI模块中定义了工厂类,就可以在公共模块中注入/检索它,并将检索到的MyManager示例绑定到DI中,然后在需要时使用它。
使用Kodein-DI时,它看起来如下所示:

公用主电源

//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...
}

机器人主页

//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() }
}
2mbi3lxu

2mbi3lxu2#

一般来说,没有一种非常干净的方法可以做到这一点。没有办法从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
    }
}

如果你想从任何代码调用它,在app start上初始化Context。在静态引用中保存它不会出现在每个人的最佳实践列表中,但这不是一个技术问题。或者,你可以用Activity做类似的事情,但这有它自己的一系列问题。

hfyxw5xn

hfyxw5xn3#

下面是我们为数据库工厂解决这个问题所做的工作(没有依赖注入,因为我们还没有设置它):
我们在底层(没有其他依赖项)模块中创建了一个名为AppContext的类,在commonMain中我们有expected类:

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构造函数,因此编译器认为它具有相同的签名并且是相同的类,但实际上,我们为AndroidiOS公开了不同的实现。
我们在应用程序启动时创建这个AppContext,并将其传递给 Bootstrap ,然后再传递给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
    )
yr9zkbsy

yr9zkbsy4#

我们是这样做的(例如bundleId -也许它会帮助你):

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

安卓主界面:

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

IOS主界面:

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

izj3ouym5#

我想出了另一个"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"

然后,您必须将PlatformDependencies的示例与平台根gradle项目中的特定值绑定。
拥有一个只包含特定于平台的deps的对象允许您使用它来构造DI模块层次结构中的任何依赖于平台的类。
它还远远谈不上优雅的解决方案,依赖于规则,并在一定程度上将平台的细节泄漏到一个公共代码中,但它是有效的。

chy5wohz

chy5wohz6#

您可以在Android上使用App Startup Library获取Android特定代码中的上下文,而无需公开与iOS不同的API。
基本上,您可以将App Startup添加到Android模块依赖项中,在AndroidManifest中添加一个提供者条目,并实现Initializer接口:Android将在应用启动时使用有效上下文自动调用create方法。

lateinit var applicationContext: Context
    private set

object MyModule

class MyInitializer: Initializer<MyModule> {
    override fun create(context: Context): MyModule {
        applicationContext = context.applicationContext
        return MyModule
    }

    override fun dependencies(): List<Class<out Initializer<*>>> {
        return listOf()
    }
}

然后,您可以在需要时在任何地方使用applicationContext

相关问题