====== Android Room with a View - Kotlin ====== My conspectus of [[https://developer.android.com/codelabs/android-room-with-a-view-kotlin|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 ===== * [[https://developer.android.com/topic/libraries/architecture/index.html|Architecture Components]] * Data layer: https://developer.android.com/topic/architecture/data-layer * Prerequisites * RecyclerView and adapters * SQLite database and the SQLite query language * Basic coroutines (If you're not familiar with coroutines, you can start with [[https://developer.android.com/codelabs/kotlin-coroutines?continue=https%3A%2F%2Fdeveloper.android.com%2Fcourses%2Fpathways%2Fandroid-coroutines%23codelab-https%3A%2F%2Fdeveloper.android.com%2Fcodelabs%2Fkotlin-coroutines#0|Using Kotlin Coroutines in your Android App]].) * Gitlab repository: https://github.com/android/codelab-android-room-with-a-view/tree/kotlin ===== 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 [[https://kotlinlang.org/docs/reference/coroutines-overview.html|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 @Insert(onConflict = OnConflictStrategy.IGNORE) suspend fun insert(word: Word) @Query("DELETE FROM word_table") suspend fun deleteAll() } * Room DAO Docs: [[https://developer.android.com/training/data-storage/room/accessing-data.html|Accessing data using Room DAOs]] ===== 7.Observing database changes ===== * To observe data changes you will use [[https://kotlinlang.org/docs/reference/coroutines/flow.html|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> ===== 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> = 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 [[https://github.com/android/architecture-components-samples/tree/master/BasicSample|BasicSample]] for a more complex implementation. * network synchronization ? ===== 10.Create the ViewModel ===== * The ''ViewModel'''s role is to provide data to the UI and survive configuration changes. * The ''ViewModel'' is part of the [[https://developer.android.com/topic/libraries/architecture/lifecycle.html|lifecycle library]] (part of [[Android Jetpack]]). * For an introductory guide to this topic, see [[https://developer.android.com/topic/libraries/architecture/viewmodel.html|ViewModel Overview]] or the [[https://medium.com/androiddevelopers/viewmodels-a-simple-example-ed5ac416317e|ViewModels: A Simple Example]] blog post. * 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> = 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 create(modelClass: Class): 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'' ... ===== 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! ===== ...