Using room as singleton in kotlin

16.2k views Asked by At

I'm trying to use Room as singleton so I didn't have to invoke Room.databaseBuilder() -which is expensive- more than once.

@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()
    }
}

Note:

  1. Can't use Object because Room requires using abstract class.
  2. singleton must be thread safe because multiple threads might access it at the same time.
  3. must be able to take Context as an argument.

I have looked at all similar StackOverflow questions and none of them satisfy my requirements

Singleton with argument in Kotlin isn't thread-safe

Kotlin - Best way to convert Singleton DatabaseController in Android isn't thread-safe

Kotlin thread save native lazy singleton with parameter uses object

6

There are 6 answers

4
humazed On BEST ANSWER

After some research, I found that I have two options.

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

I considered implementing one of them, but this didn't felt right for Kotlin - too much boilerplate code.


After more research, I stumbled upon this great article which provides an excellent solution, which uses Double-checked locking but in an elegant way.

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

From the article:

A reusable Kotlin implementation:

We can encapsulate the logic to lazily create and initialize a singleton with argument inside a SingletonHolder class. In order to make that logic thread-safe, we need to implement a synchronized algorithm and the most efficient one — which is also the hardest to get right — is the double-checked locking algorithm.

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

Extra: if you want Singleton with two arguments

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
            }
        }
    }
}
1
ephemient On

You could make use of the Kotlin standard library's

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

Personally though, I would normally add ApplicationContext-dependent singletons inside the Application, e.g.

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

You can even define an extension method for easy access as context.database.

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

Used @Volatile for thread safety.

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

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

0
Rahul Kumar On

Here's how i figured out...

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

}
0
max On

singleton in kotlin is real easy just do this

companion object {
    @JvmStatic
    val DATABASE_NAME = "DataBase"

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

}
4
Jan Slominski On

In this particular case I would resort to using Dagger 2, or some other dependency injection library like Koin or Toothpick. All three libraries allow to provide dependancies as singletons.

Here's the code for Dagger 2 module:

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

AppComponent:

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

Application class to provide injection:

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

And then inject your database as singleton to wherever you need it (for example in your app's repository):

@Inject lateinit var appDatabase: AppDatabase

init {
    App.component.inject(this)
}