se22as
se22as

Reputation: 2382

Android Kotlin : Room with corountines : how to create db, preseed data and populate maps from db values before updating UI

I am writing an application in Kotlin which needs to use data seeded into the database on app install/first start. I am wanting to use Room for database access and cooroutines for async execution.

I see lots of examples on how to use Room and lots of examples on how to use corountines. However i do not see any examples of using both together.

I need to create a database on application install and seed data and then immediately read from that database to have a cached map of values which i can use in the application. I do not understand how to do the create/seed/read together and not on the main thread.

Later in the application i need to empty the map of values and query the database again to repopulate the maps before updating the UI from the new values in the map.

Background information on my application

My application will show a set of images to the user based on two options the user has selected. These two options are stored in the preference store as a number which can be 1 or 0.

Therefore if the user has selected "option1=0" and "option2=1" and my database contained data such as below

id   option1  option2  letter    image
------------------------------------------------
1        0        0       A      image_a_0_0.png
2        0        0       B      image_b_0_0.png
3        0        0       C      image_c_0_0.png
4        0        0       D      image_d_0_0.png
5        1        0       A      image_a_1_0.png
6        1        0       B      image_b_1_0.png
7        1        0       C      image_c_1_0.png
8        1        0       D      image_d_1_0.png

9        0        1       A      image_a_0_1.png
10       0        1       B      image_b_0_1.png
11       0        1       C      image_c_0_1.png
12       0        1       D      image_d_0_1.png

13       1        1       A      image_a_1_1.png
14       1        1       B      image_b_1_1.png
15       1        1       C      image_c_1_1.png
16       1        1       D      image_d_1_1.png

then the following images will be shown to the user:

image_a_0_1.png
image_b_0_1.png
image_c_0_1.png
image_d_0_1.png

My requirements

In my Room database code I have a singleton for my DB instance, and if the DB does not exist i create it. I use a callback to know when the Database is created. In this callback i use a corountine to call the code to seed data into the database.

My Room's Database code

@Database(....)
abstract class AppDatabase : RoomDatabase() {

    abstract fun myDao(): MyDao

    companion object {

        private const val DATABASE_NAME = "my_databse"

        // Singleton prevents multiple instances of database opening at the same time.
        @Volatile
        private var sINSTANCE: AppDatabase? = null

        fun getInstance(context: Context): AppDatabase {
            val tempInstance = sINSTANCE
            if (tempInstance != null) {
                return tempInstance
            }
            synchronized(this) {
                val instance = Room.databaseBuilder(
                        context.applicationContext,
                        AppDatabase::class.java,
                        DATABASE_NAME
                )
                .addCallback(seedDatabaseCallback(context))
                .build()

                sINSTANCE = instance
                return instance
            }
        }

        /**
         * Callback which is called when the Database is first created.
         */
        private fun seedDatabaseCallback(context: Context): Callback {
            return object : Callback() {
                override fun onCreate(db: SupportSQLiteDatabase) {
                    super.onCreate(db)
                    seedData(context)
                }
            }
        }

        suspend fun seedData(context:Context){
            val db = AppDatabase.getInstance(context)
            var myDao = db.myDao()
            myDao.insert(..........)
            .....
        }
    }

}

I have a "DataProvider.kt" file which is responsible for reading values from the preference store and the database (using the DAO) and updating the maps used in the rest of the application. This could be called any time within my app when the user changes any of the two options so that the map always contains the correct images applicable to the users current settings

suspend fun updateMaps(context: Context) {
    val mSharedPref = context.getSharedPreferences(OPTIONS_PREFERENCES_KEY, Context.MODE_PRIVATE)
    val option1 = mSharedPref.getLong("option1_PREF", 0)
    val option2 = mSharedPref.getLong("option2_PREF", 0)

    updateMaps(context, option1, option2)
}

private suspend fun updateMaps(context: Context, option1 : Int, option2 : Int) {
    withContext(Dispatchers.IO) {
        var db = AppDatabase.getInstance(context)
        var myValues = db.myDao().getValues(option1, option2)
        for (value in myValues){
            myMap[value .letter] = value.image
        }
    }
}

My Issue

In my main activity's onCreate method i am trying to check the database exists (creating and seeding data if need be) before setting up the maps. If the maps have values i set views visible on my main activity.

I do not entirely understand how to put Room and Corountines together. I am finding on app install, in my main activity, it calls updateMaps and the DB is created and data seeded, but it never sets my view to visible as the maps do not have values when it comes to the check in main activity.

I "think" its because the db creation is seeded on one thread, then the db values are being red and maps updated on another thread so they are not dependant on each other.

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    var myView = findViewById<CardView>(R.id.myView)
    ....
    ....

    GlobalScope.launch {
        // Set up the maps, this will create the database and seed the factory
        // data the first time this application is run
        DataProvider.updateMaps(this@DashboardActivity)
        if (DataProvider.myMap.isNotEmpty()) {
            // ISSUE DOES NOT COME IN HERE
            myView.visibility = View.VISIBLE
        }
    }

    ....
}

I have seen comments about async() and await() but I am not entirely clear when you would use those rather than the code i have in this question.

Upvotes: 0

Views: 752

Answers (1)

Stachu
Stachu

Reputation: 1724

there is an example with prepopulating the database using coroutines in the Google's Room with a View course, only it does repopulate on every open

https://codelabs.developers.google.com/codelabs/android-room-with-a-view-kotlin/index.html?index=..%2F..index#11

EDIT - after more detail read, I think it's better if you look into the sample Sunflower app

https://github.com/android/sunflower

it uses seed worker to populate the database based on JSON file

class SeedDatabaseWorker(
    context: Context,
    workerParams: WorkerParameters
) : CoroutineWorker(context, workerParams) {
    override suspend fun doWork(): Result = coroutineScope {
        try {
            applicationContext.assets.open(PLANT_DATA_FILENAME).use { inputStream ->
                JsonReader(inputStream.reader()).use { jsonReader ->
                    val plantType = object : TypeToken<List<Plant>>() {}.type
                    val plantList: List<Plant> = Gson().fromJson(jsonReader, plantType)

                    val database = AppDatabase.getInstance(applicationContext)
                    database.plantDao().insertAll(plantList)

                    Result.success()
                }
            }
        } catch (ex: Exception) {
            Log.e(TAG, "Error seeding database", ex)
            Result.failure()
        }
    }

Upvotes: 1

Related Questions