My question is that if I'm using my database in different threads , should I still keep single instance of database or do something else?
I studied that it's better to keep single instance of room database within synchronized block to avoid some race conditions which might create more instances.
fun getDatabase(context: Context): VideosDatabase {
synchronized(VideosDatabase::class.java){
if (!::INSTANCE.isInitialized) {
INSTANCE = Room.databaseBuilder(
context.applicationContext,
VideosDatabase::class.java, "videos"
).build()
}
}
return INSTANCE
}
But wouldn't this cause performance issues as some threads will be blocked? And since we cant keep on creating unlimited instances of database so what can be solution in that case or am I missing something from start?
The short answer is that the snychronization is needed but it won't have any impact on the performance.
Long answer:
It's important to only have a single instance of the database object in your code to prevent multiple instances concurrently accessing the same database file which would incur an additional synchronizing effort on every database access (also see the last section at the bottom). Using a synchronized
block is one way to assure there only ever is one instance.
Although it comes with a small performance hit, that will be negligible because this function will not be called often. It is only executed once for each repository where you need to access the database, something like this:
class VideosRepository(
context: Context,
) {
private val dao = VideosDatabase.getDatabase(context).dao()
// access the database using the dao instance
}
This will only call getDatabase
once on object creation, which should only happen once for your app until it is closed. synchronized
may still block here should, at the same time, another thread want to create an instance of another repository. Unlikely, but not impossible in general. But you you don't have thousands of repositories that are created all the time, you'll only have a handful, and they'll only be created once, and they won't even all need the database. In conclusion, synchronized
won't have a measurable performance impact.
Having said that, a more convenient way to retrieve a database instance would be to use a dependency injection framework like Hilt. When properly set up you can tell the framework to handle the database as a singleton, like this:
@Module
@InstallIn(SingletonComponent::class)
object VideosDatabaseModule {
@Provides
@Singleton
fun providesVideosDatabase(@ApplicationContext context: Context): VideosDatabase =
Room.databaseBuilder(
context = context.applicationContext,
klass = VideosDatabase::class.java,
name = "videos",
).build()
@Provides
fun providesVideosDao(db: VideosDatabase): VideosDao =
db.dao()
}
The @Singleton
annotation tells Hilt to only execute providesVideosDatabase
once when a VideosDatabase
object is needed, all successive requests must be served the same instance that was retrieved the first time.
Your repository would then look like this:
@Singleton
class VideosRepository @Inject constructor(
private val dao: VideosDao,
) {
// access the database using the dao instance
}
The @Singleton
here just instructs Hilt to also handle the repository itself as a singleton, that has no influence on the database instance.
Using dependency injection like this doesn't get rid of the synchronisation, but you do not need to bother yourself to get it right, Hilt handles that under the hood. It's also easier to use because you do not need to pass instances around (especially obtaining the Context
can be a pain) and your database class looks cleaner:
@Database(...)
abstract class VideosDatabase : RoomDatabase() {
abstract fun dao(): VideosDao
}
Both of the above approaches work fine in a multi-threaded environment. The only thing to look out for is when you actually have multiple processes accessing the same database. That's not a common use case in an Android app and must be explicitly configured.
But if you do, then the different processes will each have their own database instance. They cannot share the same database instance because processes do not share any memory. But you still want the changes one process makes to the database seen by all other processes. For that to work you must use enableMultiInstanceInvalidation()
in the database builder. This will impact the performance, so only activate it when really needed.