android_room_with_a_view_-_kotlin

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:

  • UI controller (activity/fragment)
  • ViewModel
    • contains LiveData models.
      • LiveData is observable (can notify observers about changes)
    • survives activity/fragment recreation
  • Repository
    • necessary to manage multiple data sources
  • RoomDatabase
    • usually SQLite
    • DAO
      • mapping of SQL queries to functions
    • Entities
      • ⇒ tables
      • Citation: “Annotated class that describes a database table when working with Room.”

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)
  • Each @Entity class represents a SQLite table. Annotate your class declaration to indicate that it's an entity (default table name = class name?).
  • Autogenerate keys : '' @PrimaryKey(autoGenerate = true)''

6.Create the DAO

  • In the DAO (data access object), you specify SQL queries and associate them with method calls.
  • The DAO must be an interface or abstract class.
  • By default, all queries must be executed on a separate thread.
    • Room has Kotlin coroutines support. This allows your queries to be annotated with the 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()
}

7.Observing database changes

  • To observe data changes you will use Flow from kotlinx-coroutines. Use a return value of type Flow in your method description, and Room generates all necessary code to update the Flow when the database is updated.
   @Query("SELECT * FROM word_table ORDER BY word ASC")
   fun getAlphabetizedWords(): Flow<List<Word>>

8.Add a Room database

  • Room is a database layer on top of an SQLite database.
    • “replacement” for SQLiteOpenHelper
  • By default when Room queries return Flow, the queries are automatically run asynchronously on a background thread.
  • Room provides compile-time checks of SQLite statements.
  • Your Room database class must be abstract and extend 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”.
  • Notes
    • When you modify the database schema, you'll need to update the version number and define a migration strategy.

9.Create the Repository

  • A repository class abstracts access to multiple data sources (e.g. local / network )
// 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)
    }
}
  • The main takeaways:
    • The DAO is passed into the repository constructor as opposed to the whole database. This is because it only needs access to the DAO, since the DAO contains all the read/write methods for the database. There's no need to expose the entire database to the repository.
    • The list of words is a public property. It's initialized by getting the Flow list of words from Room; you can do this because of how you defined the getAlphabetizedWords method to return Flow in the “Observing database changes” step. Room executes all queries on a separate thread.
    • The suspend modifier tells the compiler that this needs to be called from a coroutine or another suspending function.
    • Room executes suspend queries off the main thread.
  • Notes:
    • In this simple example, you only have one data source, so the Repository doesn't do much. See the BasicSample for a more complex implementation.
      • network synchronization ?

10.Create the ViewModel

  • The 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
      • observable
      • NOT lifecycle aware (destroyed every time on configuration change)
    • LiveData
      • observable
      • lifecycle aware
    • ViewModel
      • lifecycle aware
      • transforms Flow to LiveData
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

  • Make your application theme material by setting the AppTheme parent to Theme.MaterialComponents.Light.DarkActionBar. Add a style for list items in values/styles.xml.
  • Add dimension resources in values/dimens.xml
  • Add a layout/recyclerview_item.xml layout
  • In layout/activity_main.xml, replace the TextView with a RecyclerView and add a floating action button (FAB).
  • Replace the FAB icon with a ‘+' symbol

12.Add a RecyclerView

  • You'll need to create:
    • The WordListAdapter class that extends ListAdapter.
    • A nested DiffUtil.ItemCallback class part of the WordListAdapter.
    • The ViewHolder that will display each word in our list.
  • Add the RecyclerView in the onCreate() method of MainActivity

13.Instantiate the repository and the database

  • Create a new class called WordsApplication 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()) }
}
  • Update the 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">
...

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!

…

android_room_with_a_view_-_kotlin.txt · Last modified: by kalaschnikow