Skip to content
Nate Ebel edited this page Jul 21, 2022 · 11 revisions

πŸ–₯ Lab 14: Building a CRUD Workflow Using Room

Let's add support for creating, retrieving, updating, and deleting custom notes. To support this, we'll leverage that Android Architecture Component library Room.

  • Room provides an abstraction layer over the underlying SQLite database supported by Android.
  • Room allows us to define database entity classes, and database interaction interfaces and generates the required SQLite database and interaction code.
  • Room supports querying for data using a number of apis including coroutines, Flow, LiveData, and RxJava.
  • We will be making use of a feature of the Navigation Component named Safe Args. The Safe Args plugin generates type-safe navigation action classes for us that help ensure we pass the correct arguments to our navigation destinations. Rather than passing a resource id to a navigate() call, we can pass an instance of a Directions class that enforces the arguments we need.

Β 

πŸ“ Objectives 1 - Preparing the Database

  1. Update Note model to be a Room @Entity

    1. Can remove the @Parcelize setup as we will no longer pass Note directly but instead load it from DB
    2. Add the @Entity annotation to the Note class. Room will now create a database table for this model.
    3. Add a class-body var property named id of type Int with default value 0
    4. Annotate id with @PrimaryKey(autoGenerate = true)
    5. This @PrimaryKey annotation will be used by Room to auto-generate unique ids for each Note added to the database
  2. Create a new package db

  3. Within the db package, create a NoteDao interface with the following methods

    1. Annotate NoteDao with @Dao. This will tell Room to generate the needed code.
    2. @Query("SELECT * FROM note") fun getAll(): Flow<List<Note>>
    3. @Query("SELECT * FROM note where id=:noteId") suspend fun get(noteId: Int): Note
    4. @Insert suspend fun save(note: Note)
    5. @Update(onConflict = OnConflictStrategy.REPLACE) suspend fun update(note: Note)
    6. @Delete suspend fun delete(note: Note)
    7. Room will use these methods and their annotations to generate database access code
  4. Within the db package, create an AppDatabase class to access NoteDao and interact with the database

    1. AppDatabase should extend RoomDatabase
    2. Add a method abstract fun noteDao(): NoteDao to the class. This will result in a generated implementation that provides an instance of NoteDao.
    3. Annotate the AppDatabase class with @Database(entities = [Note::class], version = 1). This informs Room about which database tables/entities to care about, and what our database schema version should be. If we were to change the schema of our database, we would likely want to increase this version number as well and provide a migration (migrations are out of scope of this lab).
  5. Create a custom Application class named AndroidStudyGuideApplication

    1. Name the new class AndroidStudyGuideApplication
    2. It should extend Application
    3. Within AndroidManifest.xml add android:name=".AndroidStudyGuideApplication" to the <application> element
    4. This will result in our custom Application class being instantiated when our app is started
  6. Make AppDatabase globally available by creating a public, lazy property on the AndroidStudyGuideApplication class

  7. Create an extension function on Activity for easy access to AndroidStudyGuideApplication

    1. This will require casting Activity.application to AndroidStudyGuideApplication

Β 

πŸ“ Objectives 2 - Implement save/retrieve Note

Once you have access to the database, it's time to implement our CRUD workflow across our application. We'll update our app to support the following:

  • Displaying saved notes (from MyNotesFragment)
  • Saving notes to the database (from CreateNoteFragment)
  • Viewing saved note details (from NoteDetailsFragment)
  1. Update CreateNoteFragment and CreateNoteViewModel to save a Note to the database

    1. Update CreateNoteViewModel to take an instance of NoteDao as a parameter. You'll need to update CreateNoteViewModelFactory to achieve this.
    2. Add a method to CreateNoteViewModel named fun save(title: String, categoryIndex: Int, content: String) and implement using NoteDao to perform the save operation.
    3. In CreateNoteFragment, call viewModel.save() when the save button is clicked
  2. Update MyNotesFragment and MyNotesViewModel to populate MyNotesListAdapter based on items saved to the database

    1. Update MyNotesViewModel to take an instance of NoteDao as a parameter. You'll need to create a class named MyNotesViewModelFactory to achieve this. Use CreateNotViewModelFactory as an example.
    2. Remove the title property from UiState. We will no longer need it.
    3. Refactor the implementation of the state property to instead use noteDao.getAll().map { UiState(it) }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), UiState()
  3. Update how we handle Note click handling

    1. We need to pass the id of a selected Note when navigating to NoteDetailsFragment
    2. Update the click handler we pass to MyNotesListAdapter
    3. Rather than passing a resource id to our call to findNavController.navigate() we will use a Navigation Safe Args generated class named MyNotesFragmentDirections to create an instance of a navigation acton using MyNotesFragmentDirections.actionMyNotesFragmentToNoteDetailsFragment(note.id)
  4. Update NoteDetailFragment and NoteDetailViewModel to load Note data from the database based on a passed note id

    1. Update NoteDetailViewModel to take an id instead of a Note
    2. Update NoteDetailViewModel to take an instance of NoteDao
    3. In the init{} block of NoteDetailViewModel, launch a new coroutine on a background thread
    4. Within the launched coroutine, call noteDao.get(id)
    5. Update the UiState using the loaded Note

Β 

✨ Challenges

  1. Add the ability to delete a note
  2. Add the ability to edit a note

Β 

πŸ–₯ Lab 14: Building a CRUD Working Using Room

Β 

πŸ’‘ Helpful Resources

Β 

πŸ’‘ How do we setup our Note model to support Room?

If we want our Room model data class to have an auto-incrementing id that is not managed by us, then we can't put the property in the constructor. But if we can't put the data class property in the constructor, where can we put it?

Well, we can still add properties to a data class that are outside the constructor. The only caveat, is that those properties won't be considered as part of the auto-generated equals/hashcode & copy() methods

@Entity
data class Note(
  val title: String,
  val category: String,
  val content: String,
) {
  @PrimaryKey(autoGenerate = true)
  var id: Int = 0
}

Β 

πŸ’‘ How to setup our NoteDao interface?

@Dao
interface NoteDao {

  @Query("SELECT * FROM note")
  fun getAll(): Flow<List<Note>>

  @Query("SELECT * FROM note where id=:noteId")
  suspend fun get(noteId: Int): Note

  @Insert
  suspend fun save(note: Note)

  @Update(onConflict = OnConflictStrategy.REPLACE)
  suspend fun update(note: Note)

  @Delete
  suspend fun delete(note: Note)

}

Β 

πŸ’‘ How do we structure our AppDatabase class?

@Database(entities = [Note::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
  abstract fun noteDao(): NoteDao
}

Β 

πŸ’‘ How do I create a lazy property for accessing AppDatabase?

To make accessing a single common instance of our database easier, we will use a custom Application class to store an instance of the database as a property. That property will be evaluated lazily; only being created the first time it's accessed. To make accessing this custom application class easier, we create an extension method on the Activity type to get the current Application instance and cast it to an instance of AndroidStudyGuideApplication.

fun Activity.studyGuideApplication(): AndroidStudyGuideApplication = application as AndroidStudyGuideApplication

class AndroidStudyGuideApplication : Application() {

  val database: AppDatabase by lazy {
    Room.databaseBuilder(this, AppDatabase::class.java, "app-database").build()
  }
}

Β 

πŸ’‘ Setting our custom Application class in the manifest?

<application
    ...
     android:name=".AndroidStudyGuideApplication"
    >

</application>

Β 

πŸ’‘ How to create MyNotesViewModelFactory?

class MyNotesViewModelFactory(
  private val noteDao: NoteDao
) : ViewModelProvider.Factory {
  override fun <T : ViewModel> create(modelClass: Class<T>): T {
    return MyNotesViewModel(noteDao) as T
  }
}

Β 

πŸ’‘ How to load Notes from the database in MyNotesViewModel

Now that we have database access, we can initialize our UiState Flow based on the result of calling noteDao.getAll(). We can observe those results, and convert each result into a UiState instance that is then observed by MyNotesFragment. The stateIn() call here converts the Flow returned from NoteDao into an instance of a StateFlow.

val state: StateFlow<UiState> = noteDao
    .getAll()
    .map { UiState(it) }
    .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), UiState()
  )

Β 

πŸ’‘ How to pass Note id to NoteDetailsFragment

Within MyNotesFragment we need to update the MyNotesListAdpater click handler. This update makes use of Navigation Safe Args to use generated, type-safe navigation classes that help enforce we pass the correct navigation arguments to our destinations.

private val notesAdapter = MyNotesListAdapter() { note ->
  findNavController().navigate(MyNotesFragmentDirections.actionMyNotesFragmentToNoteDetailsFragment(note.id))
}

Β 

πŸ’‘ If we are using suspend, and Flow as part of our Dao, how do we call those methods from a Fragment?

Because suspend and Flow are part of the Kotlin coroutines package, we must sometimes call those methods from a coroutine.

From within a Fragment, the best place to start is to call lifecycleScope.launch {} and perform any coroutine executions within the launch{} lambda.

Clone this wiki locally