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 ?)
See this url for diagrams: https://developer.android.com/codelabs/android-room-with-a-view-kotlin#1
Layers:
…
…
@Entity(tableName = "word_table") class Word(@PrimaryKey @ColumnInfo(name = "word") val word: String)
@Entity class represents a SQLite table. Annotate your class declaration to indicate that it's an entity (default table name = class name?).suspend modifier and then called from a coroutine or from another suspension function.@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() }
@Query("SELECT * FROM word_table ORDER BY word ASC") fun getAlphabetizedWords(): Flow<List<Word>>
SQLiteOpenHelperFlow, the queries are automatically run asynchronously on a background thread.RoomDatabase. Usually, you only need one instance of a Room database for the whole app.getDatabase returns the singleton. It'll create the database the first time it's accessed, using Room's database builder to create a RoomDatabase object in the application context from the WordRoomDatabase class and names it “word_database”.// 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) } }
getAlphabetizedWords method to return Flow in the “Observing database changes” step. Room executes all queries on a separate thread.suspend modifier tells the compiler that this needs to be called from a coroutine or another suspending function.ViewModel's role is to provide data to the UI and survive configuration changes.ViewModel will transform the data from the Repository, from Flow to LiveData and expose the list of words as LiveData to the UI. This ensures that every time the data changes in the database, your UI is automatically updated.Flow to LiveDataclass 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") } }
AppTheme parent to Theme.MaterialComponents.Light.DarkActionBar. Add a style for list items in values/styles.xml.values/dimens.xmllayout/recyclerview_item.xml layoutlayout/activity_main.xml, replace the TextView with a RecyclerView and add a floating action button (FAB).WordListAdapter class that extends ListAdapter.DiffUtil.ItemCallback class part of the WordListAdapter.ViewHolder that will display each word in our list.RecyclerView in the onCreate() method of MainActivityWordsApplication that extends Application. 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()) } }
AndroidManifest file and set WordsApplication as application android:name<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"> ...
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 } } } }
…
…
…
…