Table of Contents

Android Room with a View - Kotlin

My conspectus of Android Room with a View - Kotlin - official code lab from Google (by Florina Muntenescu)

ATTENTION: this codelab is now deprecated (because of Room 3.x ?)

1.Before you begin

2.Using the Architecture Components

See this url for diagrams: https://developer.android.com/codelabs/android-room-with-a-view-kotlin#1

Layers:

3.Create your app

…

4.Update Gradle files

…

5.Create an Entity

@Entity(tableName = "word_table")
class Word(@PrimaryKey @ColumnInfo(name = "word") val word: String)

6.Create the DAO

@Dao
interface WordDao {
 
    @Query("SELECT * FROM word_table ORDER BY word ASC")
    fun getAlphabetizedWords(): List<Word>
 
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    suspend fun insert(word: Word)
 
    @Query("DELETE FROM word_table")
    suspend fun deleteAll()
}

7.Observing database changes

   @Query("SELECT * FROM word_table ORDER BY word ASC")
   fun getAlphabetizedWords(): Flow<List<Word>>

8.Add a Room database

9.Create the Repository

// Declares the DAO as a private property in the constructor. Pass in the DAO
// instead of the whole database, because you only need access to the DAO
class WordRepository(private val wordDao: WordDao) {
 
    // Room executes all queries on a separate thread.
    // Observed Flow will notify the observer when the data has changed.
    val allWords: Flow<List<Word>> = wordDao.getAlphabetizedWords()
 
    // By default Room runs suspend queries off the main thread, therefore, we don't need to
    // implement anything else to ensure we're not doing long running database work
    // off the main thread.
    @Suppress("RedundantSuspendModifier")
    @WorkerThread
    suspend fun insert(word: Word) {
        wordDao.insert(word)
    }
}

10.Create the ViewModel

class WordViewModel(private val repository: WordRepository) : ViewModel() {
 
    // Using LiveData and caching what allWords returns has several benefits:
    // - We can put an observer on the data (instead of polling for changes) and only update the
    //   the UI when the data actually changes.
    // - Repository is completely separated from the UI through the ViewModel.
    val allWords: LiveData<List<Word>> = repository.allWords.asLiveData()
 
    /**
     * Launching a new coroutine to insert the data in a non-blocking way
     */
    fun insert(word: Word) = viewModelScope.launch {
        repository.insert(word)
    }
}
 
class WordViewModelFactory(private val repository: WordRepository) : ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(WordViewModel::class.java)) {
            @Suppress("UNCHECKED_CAST")
            return WordViewModel(repository) as T
        }
        throw IllegalArgumentException("Unknown ViewModel class")
    }
}

11.Add XML layout

12.Add a RecyclerView

13.Instantiate the repository and the database

class WordsApplication : Application() {
    // Using by lazy so the database and the repository are only created when they're needed
    // rather than when the application starts
    val database by lazy { WordRoomDatabase.getDatabase(this) }
    val repository by lazy { WordRepository(database.wordDao()) }
}
<application
        android:name=".WordsApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
...

14.Populate the database

You will add data in two ways: Add some data when the database is created, and add an Activity for adding words.

To delete all content and repopulate the database whenever the app is created, you'll create a RoomDatabase.Callback and override onCreate(). Because you cannot do Room database operations on the UI thread, onCreate() launches a coroutine on the IO Dispatcher.

To launch a coroutine you need a CoroutineScope. Update the getDatabase method of the WordRoomDatabase class, to also get a coroutine scope as parameter:

fun getDatabase(
       context: Context,
       scope: CoroutineScope
  ): WordRoomDatabase {
...
}

Populating the database isn't related to a UI lifecycle, therefore you shouldn't use a CoroutineScope like viewModelScope. It's related to the app's lifecycle. You'll update the WordsApplication to contain an applicationScope, then pass that to the WordRoomDatabase.getDatabase.

class WordsApplication : Application() {
    // No need to cancel this scope as it'll be torn down with the process
    val applicationScope = CoroutineScope(SupervisorJob())
 
    // Using by lazy so the database and the repository are only created when they're needed
    // rather than when the application starts
    val database by lazy { WordRoomDatabase.getDatabase(this, applicationScope) }
    val repository by lazy { WordRepository(database.wordDao()) }
}

In the WordRoomDatabase, you'll create a custom implementation of the RoomDatabase.Callback(), that also gets a CoroutineScope as constructor parameter. Then, you override the onCreate method to populate the database.

private class WordDatabaseCallback(
    private val scope: CoroutineScope
) : RoomDatabase.Callback() {
 
    override fun onCreate(db: SupportSQLiteDatabase) {
        super.onCreate(db)
        INSTANCE?.let { database ->
            scope.launch {
                populateDatabase(database.wordDao())
            }
        }
    }
 
    suspend fun populateDatabase(wordDao: WordDao) {
        // Delete all content here.
        wordDao.deleteAll()
 
        // Add sample words.
        var word = Word("Hello")
        wordDao.insert(word)
        word = Word("World!")
        wordDao.insert(word)
 
        // TODO: Add your own words!
    }
}

Finally, add the callback to the database build sequence right before calling .build() on the Room.databaseBuilder().

The full code of WordRoomDatabase should look like:

@Database(entities = arrayOf(Word::class), version = 1, exportSchema = false)
abstract class WordRoomDatabase : RoomDatabase() {
 
   abstract fun wordDao(): WordDao
 
   private class WordDatabaseCallback(
       private val scope: CoroutineScope
   ) : RoomDatabase.Callback() {
 
       override fun onCreate(db: SupportSQLiteDatabase) {
           super.onCreate(db)
           INSTANCE?.let { database ->
               scope.launch {
                   var wordDao = database.wordDao()
 
                   // Delete all content here.
                   wordDao.deleteAll()
 
                   // Add sample words.
                   var word = Word("Hello")
                   wordDao.insert(word)
                   word = Word("World!")
                   wordDao.insert(word)
 
                   // TODO: Add your own words!
                   word = Word("TODO!")
                   wordDao.insert(word)
               }
           }
       }
   }
 
   companion object {
       @Volatile
       private var INSTANCE: WordRoomDatabase? = null
 
       fun getDatabase(
           context: Context,
           scope: CoroutineScope
       ): WordRoomDatabase {
            // if the INSTANCE is not null, then return it,
            // if it is, then create the database
            return INSTANCE ?: synchronized(this) {
                val instance = Room.databaseBuilder(
                        context.applicationContext,
                        WordRoomDatabase::class.java,
                        "word_database"
                )
                 .addCallback(WordDatabaseCallback(scope))
                 .build()
                INSTANCE = instance
                // return instance
                instance
        }
     }
   }
}

15.Add NewWordActivity

…

16.Connect with the data

…

17.Summary

…

18.Congratulations!

…