20

I'm using Dagger 2 to create and share my RoomDatabase where necessary in my app.

I'm trying to implement addCallback() so I can override the database's onCreate() function and use it to insert my initial database values. This is where I'm running into issues.

I feel like I have to be overlooking something obvious, but I can't figure out a way to do this gracefully.

RoomDatabase class:

@Database(
        entities = [Station::class],
        version = 1,
        exportSchema = false
)
abstract class TrainDB : RoomDatabase() {

    abstract fun stationDao() : StationDao

} 

DAO:

@Dao
abstract class StationDao {

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    abstract fun insert(stations: Station)

    @Query("SELECT * FROM station_table")
    abstract fun getAll() : LiveData<List<Station>>

}

Dagger Module:

@Module
class DataModule {

    @Singleton
    @Provides
    fun provideDb(app: Application): TrainDB {
        var trainDB: TrainDB? = null
        trainDB = Room
                .databaseBuilder(app, TrainDB::class.java, "train.db")
                .allowMainThreadQueries()
                .fallbackToDestructiveMigration()
                .addCallback(object : RoomDatabase.Callback() {
                    override fun onCreate(db: SupportSQLiteDatabase) {
                        super.onCreate(db)

                        /*
                        WHAT GOES HERE?
                        */

                    }
                })
                .build()
        return trainDB
    }

    @Singleton
    @Provides
    fun providesStationDao(db: TrainDB) : StationDao = db.stationDao()

}

I'd like to be able to access my DAO in the onCreate() callback. It seems obvious that this should be possible because Google is pushing Room and Dagger together and this is likely a pretty common use case.

I've tried providing the DAO as a constructor argument for provideDB(), but that creates a circular dependency

I've tried initializing my RoomDatabase as a companion object. Then instead of using the Room.builder format in my provideDB() method, I can call a getInstance() method that has access to the DAO. But this way I'm met with an error for a recursive call to getWriteableDatabase().

I understand that I can use something like db.execSQL(), but it seems like such a shame to do that when I'm using Room.

Is there a better way that I'm missing? I'm using Kotlin, but Java examples are welcome. :)

skomisa
  • 16,436
  • 7
  • 61
  • 102
Chris Feldman
  • 201
  • 2
  • 4
  • This answer looks pretty well: [Prepopulate Room Database with Dagger 2 in Kotlin](https://stackoverflow.com/questions/57968242/dagger-2-get-own-room-instance/60938147#60938147) – Md. Yamin Mollah Mar 30 '20 at 19:37
  • Using coroutines is a better solution in my opinion. A good example here https://www.raywenderlich.com/7414647-coroutines-with-room-persistence-library – Jeffrey May 26 '21 at 05:35

4 Answers4

6

I have managed it like this:

@Module
class DataModule {

lateinit var trainDB: TrainDB

@Singleton
@Provides
fun provideDb(app: Application): TrainDB {
    trainDB = Room
            .databaseBuilder(app, TrainDB::class.java, "train.db")
            .allowMainThreadQueries()
            .fallbackToDestructiveMigration()
            .addCallback(object : RoomDatabase.Callback() {
                override fun onCreate(db: SupportSQLiteDatabase) {
                    super.onCreate(db)

                    /*

                    trainDB.stationDao().insert(...)


                    */

                }
            })
            .build()
    return trainDB
}

@Singleton
@Provides
fun providesStationDao(db: TrainDB) : StationDao = db.stationDao()

}

But remember You need to to do a fake read from the database in order to initiate the db and invoke onCreate(). don't write into db as your first interact when db hasn't been created because it will create a race condition and your on create writing won't take effect.

Amir
  • 1,290
  • 1
  • 13
  • 20
  • 1
    +1 for "You need to to do a fake read from the database in order to initiate the db and invoke onCreate(). don't write into db as your first interact when db hasn't been created because it will create a race condition and your on create writing won't take effect." – Rachita Nanda Jul 05 '19 at 18:25
  • onCreate() would be called only once on first installation of the app. If you want it to be called again you must uninstall the app from the emulator or phone that you are using and install it again. No need for fake reads. – Jeffrey May 26 '21 at 05:28
  • @Jeffrey, true, When the app is installed it won't reach this line unless you ask the DI that you need this dependency (let's say a fake read) then DI reached out to this line and tries to create an instance of the DB as there is no other instance, it goes through on create once, and after that, you have access to Dao to do the read and write – Amir May 27 '21 at 06:18
  • In on create documents say that will call after creation of all tables, but in my case it calls when theres is no table also version of database is 0?! – Mahdi Mar 08 '22 at 07:37
3

You can create final one-element array.

@AppScope
@Provides
public AppDatabase appDatabase(@ApplicationContext Context appContext, AppExecutors executors) {

    final AppDatabase[] databases = new AppDatabase[1];

    databases[0] = Room.databaseBuilder(appContext, AppDatabase.class, AppDatabase.DATABASE_NAME)
            .fallbackToDestructiveMigration()
            .addCallback(new RoomDatabase.Callback() {
                @Override
                public void onCreate(@NonNull SupportSQLiteDatabase db) {
                    super.onCreate(db);
                    executors.diskIO().execute(() -> {
                        databases[0].populateDatabaseOnCreate();
                    });
                }
            }).build();
    return databases[0];
}
michal3377
  • 1,394
  • 12
  • 19
1

We can inject lazily to prevent circular dependency.

For example

    @Singleton
    @Provides
    fun provideDb(
        app: Application,
        trainDBLazy: Lazy<TrainDB> // import dagger.Lazy
    ): TrainDB {
        return Room
                .databaseBuilder(app, TrainDB::class.java, "train.db")
                .allowMainThreadQueries()
                .fallbackToDestructiveMigration()
                .addCallback(object : RoomDatabase.Callback() {
                    override fun onCreate(db: SupportSQLiteDatabase) {
                        super.onCreate(db)
                        var trainDB = trainDBLazy.get()
                    }
                })
                .build()
    }
Kiwi Lin
  • 771
  • 8
  • 15
0

You can do this in java

    AppDatabase appDatabase = null;

    AppDatabase finalAppDatabase = appDatabase;
    appDatabase = Room.databaseBuilder(MyApplication.getApplication(),
            AppDatabase.class, Constants.DATABASE_NAME).
            addCallback(new RoomDatabase.Callback() {
                @Override
                public void onCreate(@NonNull SupportSQLiteDatabase db) {
                    super.onCreate(db);
                    //check for null
                    finalAppDatabase.yourDao();
                }
            }).
            build();
     return appDatabase;
Dishonered
  • 8,449
  • 9
  • 37
  • 50
  • 1
    I tried the Kotlin equivalent of this code. I also converted my file to Java so I could use this directly. In both cases I get a null reference on finalAppDatabase. – Chris Feldman May 25 '18 at 18:16