【问题标题】:Using room as singleton in kotlin在 kotlin 中使用房间作为单例
【发布时间】:2018-02-05 08:19:25
【问题描述】:

我正在尝试将 Room 用作单例,因此我不必多次调用 Room.databaseBuilder() - 这很昂贵 - 。

@Database(entities = arrayOf(
        Price::class,
        StationOrder::class,
        TicketPrice::class,
        Train::class,
        TrainCategory::class
), version = 2)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {

    abstract fun dao(): TrainDao

companion object {
        fun createDatabase(context: Context): AppDatabase
                = Room.databaseBuilder(context, AppDatabase::class.java, "trains.db").build()
    }
}

注意:

  1. 无法使用 Object,因为 Room 需要使用 abstract class
  2. 单例必须是线程安全的,因为多个线程可能同时访问它。
  3. 必须能够将Context 作为参数。

我查看了所有类似的 StackOverflow 问题,但没有一个满足我的要求

Singleton with argument in Kotlin 不是线程安全的

Kotlin - Best way to convert Singleton DatabaseController in Android 不是线程安全的

Kotlin thread save native lazy singleton with parameter 使用对象

【问题讨论】:

  • 您能否解释一下为什么您说调用Room.databaseBuilder() 是“昂贵的”?此调用设计为在 UI 线程上进行,因此我的期望是它应该非常轻量级。
  • 注意:如果您的应用程序在单个进程中运行,则在实例化 AppDatabase 对象时应遵循单例设计模式。每个 RoomDatabase 实例都相当昂贵,您很少需要在单个进程中访问多个实例。 |来源:developer.android.com/training/data-storage/room/index.html

标签: android singleton kotlin android-room


【解决方案1】:

经过一番研究,我发现我有两种选择。

  1. Double-checked locking
  2. Initialization-on-demand holder idiom

我考虑过实现其中之一,但这对 Kotlin 来说感觉不合适 - 样板代码太多。


经过更多研究,我偶然发现了this great article,它提供了一个出色的解决方案,它使用双重检查锁定但以一种优雅的方式。

companion object : SingletonHolder<AppDatabase, Context>({
       Room.databaseBuilder(it.applicationContext, AppDatabase::class.java, "train.db").build()
})

来自文章:

一个可重用的 Kotlin 实现:

我们可以将逻辑封装到 懒惰地创建和初始化一个带有参数的单例 SingletonHolder 班级。为了使该逻辑线程安全,我们 需要实现同步算法和最有效的 一个 — 也是最难做对的 — 是双重检查 锁定算法。

open class SingletonHolder<T, A>(creator: (A) -> T) {
    private var creator: ((A) -> T)? = creator
    @Volatile private var instance: T? = null

    fun getInstance(arg: A): T {
        val i = instance
        if (i != null) {
            return i
        }

        return synchronized(this) {
            val i2 = instance
            if (i2 != null) {
                i2
            } else {
                val created = creator!!(arg)
                instance = created
                creator = null
                created
            }
        }
    }
}

额外: 如果你想要带有两个参数的单例

open class SingletonHolder2<out T, in A, in B>(creator: (A, B) -> T) {
    private var creator: ((A, B) -> T)? = creator
    @Volatile private var instance: T? = null

    fun getInstance(arg0: A, arg1: B): T {
        val i = instance
        if (i != null) return i

        return synchronized(this) {
            val i2 = instance
            if (i2 != null) {
                i2
            } else {
                val created = creator!!(arg0, arg1)
                instance = created
                creator = null
                created
            }
        }
    }
}

【讨论】:

  • 小心将应用程序上下文传递给 Room,就像我文章中的示例一样。
  • 我为什么要小心!
  • databaseBuilder 的文档说:“上下文:数据库的上下文。这通常是应用程序上下文。”我宁愿遵循示例并传递 applicationContext 以避免麻烦或内存泄漏。
  • 好吧,我想第一次理解你,我以为你建议在使用 application-context 时要小心,你是对的,使用活动上下文会导致内存泄漏。因为我们总是对活动有一个静态引用。
【解决方案2】:

在这种特殊情况下,我将求助于使用Dagger 2,或其他一些依赖注入库,如KoinToothpick。这三个库都允许以单例的形式提供依赖项。

这是 Dagger 2 模块的代码:

@Module
class AppModule constructor(private val context: Context) {
    @Provides
    @Singleton
    fun providesDatabase(): AppDatabase {
        return Room.databaseBuilder(
                context,
                AppDatabase::class.java,
                "train.db")
                .build()
    }
}

应用组件:

@Singleton
@Component(modules = arrayOf(
        AppModule::class
))
interface AppComponent {
    fun inject(viewModel: YourViewModel)
    fun inject(repository: YourRepository)
}

提供注入的应用类:

class App : Application() {
    companion object {
        private lateinit var appComponent: AppComponent
        val component: AppComponent get() = appComponent
    }

    override fun onCreate() {
        super.onCreate()
        initializeDagger()
    }

    private fun initializeDagger() {
        component = DaggerAppComponent.builder()
                .appModule(AppModule(this))
                .build()
    }
}

然后将您的数据库作为单例注入到您需要的任何地方(例如在您应用的repository 中):

@Inject lateinit var appDatabase: AppDatabase

init {
    App.component.inject(this)
}

【讨论】:

  • 为什么要使用 m 前缀?它已经过时了。
  • @gderaco 因为我喜欢它。它并不过时。这只是一种编码风格约定。
  • 谢谢。希望我们可以一起建立一个社区,让人们使用最新的标准。我还是 Kotlin 的新手,所以如果我犯了任何错误,请再次编辑。
  • mPrefixes 在 Kotlin 中不鼓励 。见stackoverflow.com/a/48056303/2413303
【解决方案3】:

使用 @Volatile 来保证线程安全。

public abstract class AppDatabase : RoomDatabase() {

   abstract fun trainDao(): trainDao

   companion object {
        @Volatile
        private var INSTANCE: AppDatabase? = null

        fun getDatabase(context: Context): Db = INSTANCE ?: synchronized(this){
            val instance = Room.databaseBuilder(
            context.applicationContext,
            AppDatabase ::class.java,
            "train-db"
          ).build()
          INSTANCE = instance
          instance
        }
   }
}

取自:https://developer.android.com/codelabs/android-room-with-a-view-kotlin#7

【讨论】:

    【解决方案4】:

    您可以使用 Kotlin 标准库的

    fun lazy(LazyThreadSafetyMode.SYNCHRONIZED, 初始化器: () -> T): Lazy
    companion object {
        private lateinit var context: Context
        private val database: AppDatabase by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
            Room.databaseBuilder(context, AppDatabase::class.java, "trains.db").build()
        }
        fun getDatabase(context: Context): AppDatabase {
            this.context = context.applicationContext
            return database
        }
    }
    

    不过,我通常会在应用程序中添加依赖于 ApplicationContext 的单例,例如

    <!-- AndroidManifest.xml -->
    <manifest>
      <application android:name="MyApplication">
    ...
    
    class MyApplication : Application() {
        val database: AppDatabase by lazy {
            Room.databaseBuilder(this, AppDatabase::class.java, "train.db").build()
        }
    }
    

    您甚至可以将扩展方法定义为context.database,以便于访问。

    val Context.database
        get() =
            generateSequence(applicationContext) {
           (it as? ContextWrapper)?.baseContext
           }.filterIsInstance<MyApplication>().first().database
    

    【讨论】:

    • 第一种解决方案在伴生对象中保留对 Context 的永久静态引用,这并不理想,因为它可能会造成内存泄漏。上下文只能在初始化期间使用,然后清除。第二种解决方案是危险的:您永远不应该将 getApplicationContext() 强制转换为您的应用程序类,因为在某些情况下它可能不会返回您的应用程序实例。更多信息:stackoverflow.com/questions/5018545/…
    【解决方案5】:

    这就是我的想法......

    @Database(entities = [MyEntity::class], version = dbVersion, exportSchema = true)
    abstract class AppDB : RoomDatabase() {
    
    // First create a companion object with getInstance method
        companion object {
            fun getInstance(context: Context): AppDB = 
        Room.databaseBuilder(context.applicationContext, AppDB::class.java, dbName).build()
        }
    
        abstract fun getMyEntityDao(): MyEntityDao
    }
    
    // This is the Singleton class that holds the AppDB instance 
    // which make the AppDB singleton indirectly
    // Get the AppDB instance via AppDBProvider through out the app
    object AppDBProvider {
    
    private var AppDB: AppDB? = null
    
        fun getInstance(context: Context): AppDB {
            if (appDB == null) {
                appDB = AppDB.getInstance(context)
            }
           return appDB!!
        }
    
    }
    

    【讨论】:

      【解决方案6】:

      kotlin 中的单例真的很简单,只要这样做就可以了

      companion object {
          @JvmStatic
          val DATABASE_NAME = "DataBase"
      
          @JvmField
          val database = Room.databaseBuilder(App.context(), DataBase::class.java, DataBase.DATABASE_NAME).build()
      
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2022-07-14
        • 1970-01-01
        • 2020-01-04
        • 1970-01-01
        • 2020-08-29
        • 1970-01-01
        相关资源
        最近更新 更多