1

I have an app where I am prepopulating my Room Databse with a json file. Here is my database setup for that:

@Database(version = 2, entities = [(ClimbingRoute::class)], exportSchema = false)
abstract class ClimbingRoutesDatabase : RoomDatabase() {

    abstract fun climbingRouteDao(): ClimbingRouteDao

    private class ClimbingRouteDatabaseCallback(
        private val scope: CoroutineScope,
        private val resources: Resources
    ) : RoomDatabase.Callback() {

        override fun onCreate(db: SupportSQLiteDatabase) {
            super.onCreate(db)
            INSTANCE?.let { database ->
                //1
                scope.launch{
                    val climbingRouteDao = database.climbingRouteDao() // 2
                    prePopulateDatabase(climbingRouteDao) // 3
                }            }
        }
        private suspend fun prePopulateDatabase(climbingRouteDao: ClimbingRouteDao){
            // 1
            val jsonString = resources.openRawResource(R.raw.climbing_routes).bufferedReader().use {
                it.readText()
            }
            // 2
            val typeToken = object : TypeToken<List<ClimbingRoute>>() {}.type
            val boulderClimbingRoutes = Gson().fromJson<List<ClimbingRoute>>(jsonString, typeToken)
            // 3
            climbingRouteDao.insertAllClimbingRoutes(boulderClimbingRoutes)
        }
    }


    companion object {

        @Volatile
        private var INSTANCE: ClimbingRoutesDatabase? = null

        fun getDatabase(
            context: Context,
            coroutineScope: CoroutineScope, // 1
            resources: Resources // 2
        ): ClimbingRoutesDatabase {
            val tempInstance = INSTANCE
            if (tempInstance != null) {
                return tempInstance
            }

            synchronized(this) {
                val instance = Room.databaseBuilder(context.applicationContext,
                    ClimbingRoutesDatabase::class.java,
                    "climbing_routes_database")
                    .addCallback(ClimbingRouteDatabaseCallback(coroutineScope, resources))
                    .build()
                INSTANCE = instance
                return instance
            }
        }
    }
}

Now I am following a tutorial from codingwithflow which uses Hilt. I want use his pattern to add dependency injection to my app. Here is codingwithflow's TaskDatabase and AppModule from his tutorial.

His TaskDatabase

@Database(entities = [Task::class], version = 1)
abstract class TaskDatabase : RoomDatabase() {

    abstract fun taskDao(): TaskDao

    class Callback @Inject constructor(
        private val database: Provider<TaskDatabase>,
        @ApplicationScope private val applicationScope: CoroutineScope,
    ) : RoomDatabase.Callback() {

        override fun onCreate(db: SupportSQLiteDatabase) {
            super.onCreate(db)

            val dao = database.get().taskDao()

            applicationScope.launch {
                dao.insert(Task("Wash the dishes"))
                dao.insert(Task("Do the laundry"))
                dao.insert(Task("Buy groceries", important = true))
                dao.insert(Task("Prepare food", completed = true))
                dao.insert(Task("Call mom"))
                dao.insert(Task("Visit grandma", completed = true))
                dao.insert(Task("Repair my bike"))
                dao.insert(Task("Call Elon Musk"))
            }
        }
    }
}

His AppModule:

@Module
@InstallIn(ApplicationComponent::class)
object AppModule {

    @Provides
    @Singleton
    fun provideDatabase(
        app: Application,
        callback: TaskDatabase.Callback
    ) = Room.databaseBuilder(app, TaskDatabase::class.java, "task_database")
        .fallbackToDestructiveMigration()
        .addCallback(callback)
        .build()

    @Provides
    fun provideTaskDao(db: TaskDatabase) = db.taskDao()

    @ApplicationScope
    @Provides
    @Singleton
    fun provideApplicationScope() = CoroutineScope(SupervisorJob())
}

@Retention(AnnotationRetention.RUNTIME)
@Qualifier
annotation class ApplicationScope

I have attempted to utilize this approach for my app but cannot successfully pre-populate the database using my json with dependency injection. The app is loading up with no crashes, but I keep getting an empty database.

I do not yet understand di enough to modify appmodule to make this work. Here is my database setup, appmodule and dao as it stands right now. (not working):

My ClimbingRouteDatabase:

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

    abstract fun climbingRouteDao(): ClimbingRouteDao

    class Callback @Inject constructor(
        private val database: Provider<ClimbingRouteDatabase>,
        @ApplicationScope private val applicationScope: CoroutineScope,
        private val resources: Resources
    ) : RoomDatabase.Callback() {

        override fun onCreate(db: SupportSQLiteDatabase) {
            super.onCreate(db)

            val climbingRouteDao = database.get().climbingRouteDao()

                applicationScope.launch{
                    prePopulateDatabase(climbingRouteDao)
                }
        }

        private suspend fun prePopulateDatabase(climbingRouteDao: ClimbingRouteDao){
            
            val jsonString = resources.openRawResource(R.raw.climbing_routes).bufferedReader().use {
                it.readText()
            }
            
            val typeToken = object : TypeToken<List<ClimbingRoute>>() {}.type
            val boulderClimbingRoutes = Gson().fromJson<List<ClimbingRoute>>(jsonString, typeToken)
            
            climbingRouteDao.insert(boulderClimbingRoutes)
        }
    }
}

My AppModule:

@Module
@InstallIn(ApplicationComponent::class)
object AppModule {

    @Provides
    @Singleton
    fun provideDatabase(
        app: Application,
        callback: ClimbingRouteDatabase.Callback
    ) = Room.databaseBuilder(
        app,
        ClimbingRouteDatabase::class.java,
        "climbing_route_database")
        .fallbackToDestructiveMigration()
        .addCallback(callback)
        .build()

    @Provides
    fun provideClimbingRouteDao(db: ClimbingRouteDatabase) = db.climbingRouteDao()

    @Provides
    @Singleton
    fun resourcesProvider(@ApplicationContext context: Context): Resources = context.resources

    @ApplicationScope
    @Provides
    @Singleton
    fun provideApplicationScope() = CoroutineScope(SupervisorJob())
}

@Retention(AnnotationRetention.RUNTIME)
@Qualifier
annotation class ApplicationScope

ClimbingRouteDao

@Dao
interface ClimbingRouteDao {

    @Insert(onConflict = OnConflictStrategy.IGNORE)
    suspend fun insert(climbingRoutes: List<ClimbingRoute>)

    @Query("SELECT * FROM climbing_route_table")
    fun getClimbingRoutes(): Flow<List<ClimbingRoute>>

    @Update
    suspend fun update(climbingRoutes: ClimbingRoute)

    @Delete
    suspend fun delete(climbingRoutes: ClimbingRoute)
}

Any help would be greatly appreciated. Thank you.

tommybomb
  • 53
  • 9
  • Hi! I'm following this and still having the issue, even after removing app and installing it again. I have a pretty similar code structure...any suggestion? I guess I am missing something, somewhere... – riccardogabellone Jul 15 '22 at 19:05
  • 1
    @riccardogabellone check full implementation of populating `room` from `json` file: https://github.com/yasserakbbach/Guidomia – Yasser AKBBACH Sep 10 '22 at 18:56

2 Answers2

0

OK so I resolved the issue by....

UNINSTALLING THE APP FROM MY DEVICE BEFORE RUNNING IT AGAIN FROM ANDROID STUDIO!

Gosh, live and learn I guess. This code above seems to be working fine so far.

tommybomb
  • 53
  • 9
0

In case you have an issue with loading from room on the first app launch, use channelFlow instead of flow For more details check this example on my repo here

Yasser AKBBACH
  • 539
  • 5
  • 7