19

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

humazed
  • 74,687
  • 32
  • 99
  • 138
  • Could you please clarify why you state that invoking `Room.databaseBuilder()` is “expensive?” This call is designed to take place on the UI thread, so my expectation is that it should be very lightweight. – Alex Peters Nov 16 '17 at 00:02
  • Note: If your app runs in a single process, you should follow the singleton design pattern when instantiating an AppDatabase object. Each RoomDatabase instance is fairly expensive, and you rarely need access to multiple instances within a single process. | Source: https://developer.android.com/training/data-storage/room/index.html – Martin Melka Jun 14 '19 at 15:16

6 Answers6

19

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
            }
        }
    }
}
Błażej Michalik
  • 4,474
  • 40
  • 55
humazed
  • 74,687
  • 32
  • 99
  • 138
  • 2
    Be careful to pass the application Context to Room like in the example in my article. – BladeCoder Aug 28 '17 at 18:25
  • why I should be careful! – humazed Aug 30 '17 at 08:36
  • 1
    The documentation of databaseBuilder says: "Context: The context for the database. This is usually the Application context." I would rather follow the samples and pass the applicationContext to avoid trouble or memory leaks. – BladeCoder Aug 30 '17 at 11:00
  • Okay, I miss understand you at the first time, I thought you are recommending to be careful when using application-context, and you are right using activity context with leads to memory leaks. as we always would have a static reference to the activity. – humazed Aug 30 '17 at 13:02
4

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)
}
Zun
  • 1,553
  • 3
  • 15
  • 26
Jan Slominski
  • 2,968
  • 4
  • 35
  • 61
4

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

  • 1
    I wonder why there's no double check for INSTANCE inside synchronized block? like in Java example https://developer.android.com/codelabs/android-room-with-a-view#7 – Artem_Iens Aug 14 '22 at 16:01
1

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
humazed
  • 74,687
  • 32
  • 99
  • 138
ephemient
  • 198,619
  • 38
  • 280
  • 391
  • 2
    The first solution keeps a permanent static reference to the Context in the companion object, which is not ideal because it may create memory leaks. Context should only be used during initialization, then cleared. The second solution is dangerous: you should never cast getApplicationContext() to your application class, because in some cases it may not return your application instance. More info: https://stackoverflow.com/questions/5018545/getapplication-vs-getapplicationcontext – BladeCoder Aug 28 '17 at 18:01
0

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

}
-1

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

}
max
  • 5,963
  • 12
  • 49
  • 80