Использование комнаты как синглтона в Котлине

Я пытаюсь использовать 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. singleton должен быть потокобезопасным, поскольку несколько потоков могут обращаться к нему одновременно.
  3. должен иметь возможность принимать Context в качестве аргумента.

Я просмотрел все похожие вопросы StackOverflow, и ни один из них не удовлетворяет моим требованиям.

Singleton с аргументом в Kotlin не является потокобезопасным

Kotlin - лучший способ конвертировать Singleton DatabaseController в Android не является потокобезопасным

Поток Kotlin сохраняет собственный ленивый синглтон с параметром использует объект


person humazed    schedule 28.08.2017    source источник
comment
Не могли бы вы уточнить, почему вы заявляете, что вызов Room.databaseBuilder() является «дорогим»? Этот вызов предназначен для выполнения в потоке пользовательского интерфейса, поэтому я ожидаю, что он должен быть очень легким.   -  person Alex Peters    schedule 16.11.2017
comment
Примечание. Если ваше приложение выполняется в одном процессе, при создании экземпляра объекта AppDatabase следует следовать шаблону одноэлементного проектирования. Каждый экземпляр RoomDatabase довольно дорог, и вам редко требуется доступ к нескольким экземплярам в рамках одного процесса. | Источник: developer.android.com/training/data-storage/room/ index.html   -  person Martin Melka    schedule 14.06.2019


Ответы (6)


После некоторых исследований я обнаружил, что у меня есть два варианта.

  1. Блокировка с двойной проверкой
  2. Идиома держателя инициализации по запросу

Я думал о реализации одного из них, но это не подходило для Kotlin — слишком много шаблонного кода.


После дополнительных исследований я наткнулся на эту замечательную статью, в которой отличное решение, использующее блокировку с двойной проверкой, но элегантно.

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

Дополнительно: если вам нужен Singleton с двумя аргументами

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
            }
        }
    }
}
person humazed    schedule 28.08.2017
comment
Будьте осторожны, чтобы передать контекст приложения в комнату, как в примере в моей статье. - person BladeCoder; 28.08.2017
comment
почему я должен быть осторожным! - person humazed; 30.08.2017
comment
В документации databaseBuilder сказано: Контекст: Контекст для базы данных. Обычно это контекст приложения. Я бы предпочел следовать образцам и передать applicationContext, чтобы избежать проблем или утечек памяти. - person BladeCoder; 30.08.2017
comment
Хорошо, я не понимаю вас в первый раз, я думал, что вы рекомендуете быть осторожным при использовании контекста приложения, и вы правы, используя контекст активности с утечками памяти. поскольку у нас всегда была бы статическая ссылка на активность. - person humazed; 30.08.2017

В этом конкретном случае я бы прибегнул к использованию Dagger 2 или какой-либо другой библиотеке внедрения зависимостей, такой как Коины или Зубочистка. Все три библиотеки позволяют предоставлять зависимости как синглтоны.

Вот код модуля 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()
    }
}

А затем вставьте свою базу данных как singleton туда, где она вам нужна (например, в вашем приложении репозиторий):

@Inject lateinit var appDatabase: AppDatabase

init {
    App.component.inject(this)
}
person Jan Slominski    schedule 13.11.2017
comment
почему вы используете префикс m? это устарело. - person gderaco; 03.05.2018
comment
@gderaco, потому что мне это нравится. Это не устарело. Это просто соглашение о стиле кодирования. - person Jan Slominski; 03.05.2018
comment
Спасибо. Надеюсь, мы сможем вместе создать сообщество, в котором люди будут использовать современные стандарты. Я все еще новичок в Kotlin, поэтому, если я допустил какие-либо ошибки, отредактируйте снова. - person Zun; 24.04.2019
comment
mPrefixes не рекомендуется в Kotlin. См. stackoverflow.com/a/48056303/2413303. - person EpicPandaForce; 25.04.2019

Вы можете использовать стандартную библиотеку Kotlin

fun <T> lazy(LazyThreadSafetyMode.SYNCHRONIZED, initializer: () -> T): Lazy<T>
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
person ephemient    schedule 28.08.2017
comment
Первое решение сохраняет постоянную статическую ссылку на Context в сопутствующем объекте, что не идеально, поскольку может привести к утечке памяти. Контекст должен использоваться только во время инициализации, а затем очищаться. Второе решение опасно: вы никогда не должны приводить getApplicationContext() к вашему классу приложения, потому что в некоторых случаях он может не вернуть экземпляр вашего приложения. Дополнительная информация: stackoverflow.com/questions/5018545/ - person BladeCoder; 28.08.2017

Используется @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

person Chinthaka Fernando    schedule 20.04.2021

Вот как я понял...

@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!!
    }

}
person Rahul Kumar    schedule 24.05.2018

синглтон в котлине очень прост, просто сделайте это

companion object {
    @JvmStatic
    val DATABASE_NAME = "DataBase"

    @JvmField
    val database = Room.databaseBuilder(App.context(), DataBase::class.java, DataBase.DATABASE_NAME).build()

}
person max    schedule 24.06.2018