How to Build a CRUD App With Kotlin and Android Studio

Chapter 6 Database class and string.xml

Problems covered in this chapter
  • Pre-filling the database with language-specific values

  • Database converters

  • Multilingualism

In this chapter we deal with the class CrudDiaryDatabase.kt, which creates the database and allows us to access the database in Kotlin, a converter class that ensures that date values are stored as long values in the database, as well as the XML file string.xml, in which we store English (main language) and German strings and which we use when pre-filling the database.

6.1 Converters.kt

Listing 14: Converters.kt
package app.gedama.tutorial.cruddiary.database
import androidx.room.TypeConverter
import java.util.Date
class Converters {
    @TypeConverter
    fun fromTimestamp(value: Long?): Date? {
        return value?.let { Date(it) }
    }
    @TypeConverter
    fun dateToTimestamp(date: Date?): Long? {
        return date?.time
    }
}

6.2 string.xml im values Verzeichnis

Listing 15: string.xml (English)
<resources>
    <string name="app_name">CrudDiary</string>
    <string name="nothing_selected" translatable="false">---</string>
    <string name="male_gender">male</string>
    <string name="female_gender">female</string>
    <string name="diverse_gender">diverse</string>
</resources>

6.3 string.xml im values-de Verzeichnis

Listing 16: string.xml (German)
<resources>
    <string name="app_name">CrudDiary</string>
    <string name="male_gender">m\"annlich</string>
    <string name="female_gender">weiblich</string>
    <string name="diverse_gender">divers</string>
</resources>"

6.4 CrudDiaryDatabase.kt

Listing 17: CrudDiaryDatabase.kt
package app.gedama.tutorial.cruddiary.database
import android.content.Context
import android.util.Log
import androidx.room.*
import androidx.sqlite.db.SupportSQLiteDatabase
import app.gedama.tutorial.cruddiary.GlobalAppStore
import app.gedama.tutorial.cruddiary.R
import app.gedama.tutorial.cruddiary.dao.*
import app.gedama.tutorial.cruddiary.data.*
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
@Database(entities = [Person::class, Gender::class, DiaryEntry::class,
    Tag::class, DiaryEntriesTagsCrossRef::class, Preferences::class],
    version = 1, exportSchema = true)
@TypeConverters(Converters::class)
abstract class CrudDiaryDatabase: RoomDatabase() {
    abstract val personDao: PersonDao
    abstract val genderDao: GenderDao
    abstract val diaryEntryDao: DiaryEntryDao
    abstract val tagDao: TagDao
    abstract val preferencesDao: PreferencesDao
    companion object {
        val TAG = this::class.simpleName
        @Volatile
        var INSTANCE: CrudDiaryDatabase? = null
        fun getInstance(context: Context): CrudDiaryDatabase {
            synchronized(this) {
                var instance = INSTANCE
                if (instance == null) {
                    Log.d(TAG, "Database gets created ...")
                    instance = createInstance(context)
                    INSTANCE = instance
                }
                return instance
            }
        }
        // found at https://anadea.info/blog/how-to-pre-populate-android-room-database-on-first-application-launch
        private fun createInstance(context: Context) =
            Room.databaseBuilder(context.applicationContext, CrudDiaryDatabase::class.java, GlobalAppStore.APP_DB_NAME)
                .addCallback(object : Callback() {
                    override fun onCreate(db: SupportSQLiteDatabase) {
                        super.onCreate(db)
                        Thread({ prepopulateDb(context, getInstance(context)) }).start()
                    }
                })
                .build()
        private fun prepopulateDb(context: Context, db: CrudDiaryDatabase) {
            db.runInTransaction( DatabaseInitialization(context, db))
        }
    }
    // for database export see https://stackoverflow.com/questions/51055184/exporting-room-database-to-csv-file-in-android
}
class DatabaseInitialization(val context: Context, val db: CrudDiaryDatabase): Runnable {
    override fun run() {
        val nothingSelected = context.resources
            .getString(R.string.nothing_selected)
        val maleGender = context.resources.getString(R.string.male_gender)
        val femaleGender = context.resources.getString(R.string.female_gender)
        val diverseGender = context.resources.getString(R.string.diverse_gender)
        GlobalScope.launch {
            val systemUser = Person()
            systemUser.name = nothingSelected
//            systemUser.birthDate = Date()
            db.personDao.insert(systemUser)
            val preferences = Preferences()
            preferences.preferenceId = 1
            preferences.mainUserId = 1L
            preferences.showInactive = false
            db.preferencesDao.insert(preferences)
            val tag = Tag()
            tag.title = nothingSelected
            db.tagDao.insert(tag)
            var genderType = Gender()
            genderType.name = nothingSelected
            db.genderDao.insert(genderType)
            genderType = Gender()
            genderType.name = femaleGender
            db.genderDao.insert(genderType)
            genderType = Gender()
            genderType.name = maleGender
            db.genderDao.insert(genderType)
            genderType = Gender()
            genderType.name = diverseGender
            db.genderDao.insert(genderType)
        }
    }
}

Most of the code in this class is so-called boiler-plate code and looks quite similar whenever you access an SQLite database with Room. Interesting is the method onCreate() specified in addCallback(), in which we do the pre-filling of the database when it is first created. It is important here that the insert statements for the initial values are executed in a single transaction. With a few values this is not so important, but with a large number of insert statements, such as in the GeDaMa app, the start-up time of the app increases dramatically if these statements are executed as separate transactions.

Note: If this start-up time (on older devices) exceeds the 10 second mark, you will receive a corresponding warning in the app store!

Therefore, pack all insert statements as in the code above into the separate runnable class DatabaseInitialization and execute its run method with the method runInTransaction() in a single transaction. Because the database values to be entered are determined beforehand using context.resources.getString(R.string.xxx), you can fill the database with the appropriate, language-specific values depending on the user’s language setting: If the user has set German as the default language, for example, the genders table is filled with the German values männlich, weiblich and divers; if the user has set another language, the table is created with the (default) English values male, female and diverse.