Kotlin Singleton Patterns¶
Published:
My brother was looking for an Android app to perform a single task: A big button on top of other apps to store the current location when pressed.
We could not find one which fits that bill and I set out to develop one myself. As many projects do, the app turned from a “let’s do it this weekend” into a bigger project, mostly because I got to learn Kotlin and the Android ecosystem.
What does an Android app have to do with singletons? Databases. There are many different ways to persist data in Android applications. The current (2025) recommended way to store structured data is to use Room, a database library backed by SQLite. This is the perfect fit to store our location data.
I mostly followed the Room tutorial to set everything up:
Add some dependencies (let’s skip this)
Write an entity-backed DAO
Write a database class to access the DAO
Instantiate the database
The second and third steps can be boiled down to this example, in which we store “Spots”:
@Entity
data class Spot(
@PrimaryKey val uid: Int,
val example: String,
)
@Dao
interface SpotDao {
@Query("SELECT * FROM spot")
fun getAll(): List<Spot>
@Insert
fun insertAll(vararg spots: Spot)
}
@Database(entities = [Spot::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun spotDao(): SpotDao
}
This is of course tuned down: We can only insert and retrieve records; to delete the records, we need to delete the app storage. But these steps don’t show any singletons yet… The final step, instantiating the AppDatabase, is where it becomes interesting.
The tutorial uses this pattern:
val db = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java, "database-name"
).build()
So they use a builder, pass some arguments to it, and build the instance. Curiously, the tutorial also points out the following:
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[.]
There is more information about how to deal with multiple processes, which should not be relevant for our app.
I understand it that we have a single process with multiple threads:
At least the main thread for the GUI and an IO thread to which we can dispatch IO
coroutines.
So we should implement the singleton pattern!
I know roughly how to do singletons in various programming languages, but each language has its own conventions and tools for it. However, it mostly boils down to this pseudo code:
class SomeSingletonClass:
private static instance = null
public static function getInstance()
if instance == null
instance = SomeSingletonClass()
return instance
This means that we usually have a class which holds an instance of itself. In many languages, this is denoted as static or a class member to keep it globally available since classes are usually global. If possible, the instance is also marked private so that it cannot be changed accidentally. Additionally, there is an accessor which instantiates said instance when required and returns it. Many implementations aim to be thread-safe by using some sort of locking, too, which is something we also require as our app uses multiple threads.
So I looked up how to do it and in Kotlin there are at least two very neat ways to create singletons. The first way is to declare an object, which is by definition only available once, similar to a class declaration:
object Singleton {
fun doSomething() {
}
}
The second is to use lazy
, a built-in method which allows lazy initialization on the fly:
val db: AppDatabase by lazy {
Room.databaseBuilder(
applicationContext,
AppDatabase::class.java,
"app_database"
).build()
}
Both instantiation variants are thread-safe (for lazy
see also Lazy Initialization in Kotlin on Baeldung).
For many applications, using the object
or lazy
is the way to go.
There is a problem for the Room database though, you may have spotted it already: It requires an argument, specifically, the application context.
This application context somehow needs to be passed to the initialization function.
The object
method will not work in this case, as the object is initialized earlier than we have a chance to grab the application context and pass it.
For lazy
, we might have chance:
@Database(entities = [Spot::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun spotDao(): SpotDao
val db: AppDatabase by lazy {
// Room build()
}
}
No, this is too early, and we cannot pass a parameter here, either.
Additionally, the db
object is not static, that means each instance of AppDatabase would hold its own lazy-initialized copy.
As it turns out, there is no static in Kotlin; instead, we must use a companion object
, which creates class-level functions and properties [1].
@Database(entities = [Spot::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun spotDao(): SpotDao
companion object Companion {
val instance: AppDatabase by lazy {
// Room build()
}
}
}
Again, we cannot pass arguments.
Let’s create the classic getInstance
method and pass the argument.
@Database(entities = [Spot::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun spotDao(): SpotDao
companion object Companion {
// We hold the private instance here and initialize it to null
private var instance: AppDatabase? = null
fun getDatabase(applicationContext: Context): AppDatabase {
if (instance == null) {
instance = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java,
"app_database"
).build()
}
return instance
}
}
}
This looks promising! It is not thread-safe yet, but we are able to pass an application context to the instantiation. I also checked the example repository for the inventory app, which contains very similar code in InventoryDatabase.kt#L32-50:
companion object Companion { // I added the Companion name here for Pygments
@Volatile
private var Instance: InventoryDatabase? = null
fun getDatabase(context: Context): InventoryDatabase {
// if the Instance is not null, return it, otherwise create a new database instance.
return Instance ?: synchronized(this) {
Room.databaseBuilder(context, InventoryDatabase::class.java, "item_database")
/**
* Setting this option in your app's database builder means that Room
* permanently deletes all data from the tables in your database when it
* attempts to perform a migration with no defined migration path.
*/
.fallbackToDestructiveMigration()
.build()
.also { Instance = it }
}
}
}
It uses @Volatile
to make the instance field atomic and synchronized
as a locking mechanism.
I particularly like the .also { Instance = it }
bit (also
), it is a very nice pattern to return the instance and assigning it to the stored instance.
The only thing I want to change is to use full Double-checked locking.
As it is written above, it can happen that two threads determine the instance is null
and attempt to acquire the lock.
Once a thread acquires the lock, it will create an instance and assign it, effectively allowing the second thread to overwrite the first.
To prevent this, a second null
check can be done inside the synchronized block, leading to the following final result:
companion object Companion {
@Volatile
private var instance: AppDatabase? = null
fun getDatabase(applicationContext: Context): AppDatabase {
return instance ?: synchronized(this) {
return instance ?: Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"app_database"
).build().also { instance = it }
}
}
}
Footnotes
The name of the companion object is optional and defaults to Companion, but the Pygments lexer cannot parse it. See pygments/pygments#2525.