Search code examples
androidandroid-room

(Android) When is the initial data stored in the Room database?


I'm currently trying to set up some initial data in the Room database.

As a result, the initial data setup was successful, but App Inspection confirmed that the data is saved only when getWorkoutList().

To explain in more detail, the initial data is not saved with the insert function alone, and the initial data is saved in the DB only when a function that calls the DB data called getWorkoutList() is executed from the ViewModel.

When the database is created in the view model, I expected the initial data to be saved only with the insert function. But it wasn't.

Why is the initial data not saved with only the insert function?


The following is the DB status in App inspection according to the function call.

1. When only insertWorkoutList(data) is executed

  • There is no DB and table creation, and no initial data is saved.

enter image description here

2. When only getWokroutList() is executed.

  • DB and table are created, but there is no data.

enter image description here

enter image description here

3. When both are executed.

  • Initial data is normally saved.

enter image description here


Code

Dao

@Dao
interface WorkoutListDao {
    @Query("SELECT * FROM WorkoutList")
    suspend fun getWorkoutList() : WorkoutList

    @Insert
    suspend fun insertWorkoutList(workoutList: WorkoutList)
}

WorkoutListDatabase

@Database(
    entities = [WorkoutList::class],
    version = 1
)
@TypeConverters(WorkoutListTypeConverter::class)
abstract class WorkoutListDatabase : RoomDatabase() {
    abstract fun workoutListDao() : WorkoutListDao

    companion object {
        private var INSTANCE : WorkoutListDatabase? = null

        @Synchronized
        fun getDatabase(context: Context) : WorkoutListDatabase {
            return INSTANCE ?: synchronized(this) {
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    WorkoutListDatabase::class.java,
                    "workoutlist_db"
                )
                    .addCallback(WorkoutListCallback(context))
                    .build()
                INSTANCE = instance
                instance
            }
        }
    }
}

WorkoutListCallback

class WorkoutListCallback(private val context: Context) : RoomDatabase.Callback() {
    override fun onCreate(db: SupportSQLiteDatabase) {
        super.onCreate(db)
        CoroutineScope(Dispatchers.IO).launch {
            fillWithStartingWorkoutList(context)
        }
    }

    private fun fillWithStartingWorkoutList(context: Context) {
        val dao = WorkoutListDatabase.getDatabase(context).workoutListDao()

        try {
            val data = loadJsonData(context)
//            dao.insertWorkoutList(data)

        } catch (e: JSONException) {
            e.printStackTrace()
        }
    }

    private fun loadJsonData(context: Context) : WorkoutList {
        val assetManager = context.assets
        val inputStream = assetManager.open(WORKOUTLIST_JSON_FILE)
        
        BufferedReader(inputStream.reader()).use { reader ->
            val gson = Gson()
            return gson.fromJson(reader, WorkoutList::class.java)
        }
    }
}

ViewModel

class WorkoutListViewModel(application: Application) : AndroidViewModel(application) {
    private val workoutDao = WorkoutListDatabase.getDatabase(application).workoutListDao()
    private val workoutListRepo = WorkoutListRepository(workoutDao)

    
    fun setList(part : BodyPart) {
        viewModelScope.launch(Dispatchers.IO) {
            workoutListRepo.getWorkoutList()
        }
    }
}

Solution

  • The callback will only be called when the database is actually accessed NOT when an instance of the @Database annotated class is obtained.

    As such the database is not created by:-

    val dao = WorkoutListDatabase.getDatabase(context).workoutListDao()
    

    but is created by

    workoutListRepo.getWorkoutList()
    

    That is the actual relatively resource hungry action of opening the database is left until it is definitely needed.

    A get around could be to use :-

        fun getDatabase(context: Context) : WorkoutListDatabase {
            return INSTANCE ?: synchronized(this) {
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    WorkoutListDatabase::class.java,
                    "workoutlist_db"
                )
                    .addCallback(WorkoutListCallback(context))
                    .build()
                INSTANCE = instance
                instance.getOpenHelper().getWritableDatabase() //<<<<< FORCE OPEN
                instance
            }
        }
    

    This would then force an open of the database and if it does not actually exist then the overidden onCreate method will be invoked. However,this (I think) would be done on the main thread, which you do not want.

    I would strongly suggest NOT using functions from the @Dao annotated class(es) especially if they have suspend as you then may have no control over when the threads will run.

    Instead you should use the SupportSQLiteDatabase passed to the function, it has many methods e.g. you would likely use the insert method.

    see ROOM database: insert static data before other CRUD operations for an example where the order was an issue. The example includes the conversion of the actions to utilise the intended SupportSQLiteDatabase methods.

    You may notice that the example includes placing all the database changes into a single transaction, which is more efficient as instead of each individual action (insert/delete in the example) writing to disk, the entire transaction (all actions) are written to disk once.