Room is a persistence library that provides an abstraction layer over SQLite in Android applications. It makes it easier to work with databases, and it supports coroutines in Kotlin for better handling of asynchronous tasks.
Here's a step-by-step guide to using Room with Kotlin coroutines:
Installation
plugins {
.....
id 'kotlin-kapt'
// To use Kotlin annotation processing tool (kapt)
}
dependencies {
def room_version = "2.5.1"
implementation "androidx.room:room-ktx:$room_version"
// To use Kotlin annotation processing tool (kapt)
kapt "androidx.room:room-compiler:$room_version"
}
While working with Database, specially with Room Database we need 3 thing :
1. Entity in a Database:
Tables (Database table)
Attributes (name, address, phone number, etc... [ Data inside one Row ])
Fields (Data inside each column.)
Example: -
name: "Rhytham Negi",
phoneNo : 1234567890,
address: "Amrika Colony"
2. Database:
Where all the tables or Entity of Room Database are stored.
3. Especially for Room DAO:
We need DAO (Data Access Object) interface to describe how we interact with Database.
Let's Start
Step 1:
Create a new data Class that defines our table in the database. Example Code:
// Use @Entity annotation in Data class to create table in Room Database Table
@Entity
data class Contact(
val firstName: String,
val lastName: String,
val phoneNumber: String,
// Use @PrimaryKey annotation to make "id" as Primary Key for Table
@PrimaryKey(autoGenerate = true)
val id: Int = 0
)
Step 2:
In the next step, create an interface DAO for our Entity table. That stores all the operations or functions we want to perform. Example Code:
// Assign this interface as DAO for our Room Database.
@Dao
interface ContactDao{
// Due to the kotlin annotation, For some operations not to write Query for that.
@Upsert
suspend fun upsertContact(contact: Contact)
@Delete
suspend fun deleteContact(contact: Contact)
@Query("SELECT * FROM contact ORDER BY firstName ASC")
fun getContactsOrderedByFirstName(): Flow<List<Contact>>
@Query("SELECT * FROM contact ORDER BY lastName ASC")
fun getContactsOrderedByLastName(): Flow<List<Contact>>
@Query("SELECT * FROM contact ORDER BY phoneNumber ASC")
fun getContactsOrderedByPhoneNumber(): Flow<List<Contact>>
}
Flow<>
Flow<> is used to notify any changes in the database
suspend
suspend function runs in co-routine and blocks until the database operation is closed.
@Upsert and @Delete
We use DAO annotations where SQL query is not required.
@Query
Use to write SQL queries.
Step 3:
Now, the Final step is to tell the Room Database about DAOs and Entities.
Create an abstract class that inherits the Room Database.
Code Example:
// In @Database define all the entities
@Database(
entities=[Contact::class],
version=1
)
abstract class ContactDatabase: RoomDatabase(){
//then, we need to define abstract val for our DAOs.
abstract val dao: ContactDao
}
Step 4
Now, create a sealed interface that defines all the Events in our Compose UI. Like "Click on the floating button", "delete button click", ... etc.
Code Example:
sealed interface ContactEvent{
object SaveContact: ContactEvent,
data class SetFirstName(val firstName:String):ContactEvent,
data class SetLastName(val lastName:String):ContactEvent,
data class SetPhoneNumber(val phoneNumber:String):ContactEvent,
object ShowDialog:ContactEvent,
object HideDialog:ContactEvent,
data class DeleteContact(contact:Contact):ContactEvent,
// For Sorting Events, We need to create Enum that contains all the objects or Events for SortType the data.
data SortContacts(sortType:SortType):ContactEvent
}
Enum
For Contact Sort Type (Example: sortByName, sortByPhoneNumber, etc...) Create an Enum SortType
Code Example:
enum class SortType{
FIRST_NAME,
SECOND_NAME,
PHONE_NUMBER
}
From the UI perspective, we need to create a data class that stores all the states of our UI. Example: isContactAdded, [Default value for] firstName, etc...
Code Example:
data class ContactState{
val contacts:List<String> = emptyList(),
val firstName:String = "",
val lastName:String = "",
val phoneNumber:String = "",
val isAddingContact:Boolean = false,
val sortType:SortType = SortType.FIRST_NAME
}
Step 5
Now, we need to create a View Model class. The ViewModel is responsible for providing data to the UI and surviving configuration changes such as screen rotations.
Create a Kotlin class ContactViewModel
and extend it with ViewModel()
.
class ContactViewModel(
private val dao:ContactDao
) : ViewModel(){
private _sortType = MutableStateFlow(SortType.FIRST_NAME)
private val _contacts = _sortType
.flatMapLatest { sortType ->
when(sortType) {
SortType.FIRST_NAME -> dao.getContactsOrderedByFirstName()
SortType.LAST_NAME -> dao.getContactsOrderedByLastName()
SortType.PHONE_NUMBER -> dao.getContactsOrderedByPhoneNumber()
}
}
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
private val _state = MutableStateFlow(ContactState())
val state = combine(_state, _sortType, _contacts) { state, sortType, contacts ->
state.copy(
contacts = contacts,
sortType = sortType
)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), ContactState())
private _state = MutableStateFlow(ContactState())
/*
MutableStateFlow to represent the state of a view and update it based on user input or other events
*/
fun onEvent(event:ContactEvent){
when(event) {
is ContactEvent.DeleteContact -> {
viewModelScope.launch {
dao.deleteContact(event.contact)
}
}
ContactEvent.HideDialog -> {
_state.update { it.copy(
isAddingContact = false
) }
}
ContactEvent.SaveContact -> {
val firstName = state.value.firstName
val lastName = state.value.lastName
val phoneNumber = state.value.phoneNumber
if(firstName.isBlank() || lastName.isBlank() || phoneNumber.isBlank()) {
return
}
val contact = Contact(
firstName = firstName,
lastName = lastName,
phoneNumber = phoneNumber
)
viewModelScope.launch {
dao.upsertContact(contact)
}
_state.update { it.copy(
isAddingContact = false,
firstName = "",
lastName = "",
phoneNumber = ""
) }
}
is ContactEvent.SetFirstName -> {
_state.update { it.copy(
firstName = event.firstName
) }
}
is ContactEvent.SetLastName -> {
_state.update { it.copy(
lastName = event.lastName
) }
}
is ContactEvent.SetPhoneNumber -> {
_state.update { it.copy(
phoneNumber = event.phoneNumber
) }
}
ContactEvent.ShowDialog -> {
_state.update { it.copy(
isAddingContact = true
) }
}
is ContactEvent.SortContacts -> {
_sortType.value = event.sortType
}
}
}
}
ContactViewModel
which is used to manage the UI state and data associated with displaying and manipulating a list of contacts.
_sortType
: This is a private property that is an instance ofMutableStateFlow
class, which is part of the Kotlin Coroutines library. It represents the current sort order for the list of contacts (by first name, last name, or phone number). It is initialized with a default value of SortType.FIRST_NAME._contacts
: This is a private property that is a flow of the list of contacts. It is generated from the_sortType
property using theflatMapLatest
operator, which allows us to switch between different flow sources depending on the_sortType
value. Whenever_sortType
changes, this flow will emit a new list of contacts sorted according to the updated _sortType
. In other words,_contacts
is aFlow<List<Contact>>
that emits a list of contacts based on the current sort type selected by the user. The sorting is determined by theSortType
enum
, which can be sorted byfirstName
,lastName
orphoneNumber
. TheflatMapLatest
operator is used to switch between different Flows based on the latest value emitted by_sortType
. Depending on the sort type, it will return a different Flow of contacts from the database through the Data Access Object (DAO) associated with thisViewModel
. The resulting Flow of contacts is then transformed into aList<Contact>
using thestateIn
operator. This means that the List will only be recomputed when there are new subscribers, and it will start emitting immediately upon subscription. The initial value is an empty list._state
: This is a private property that holds the current state of the UI-related data: whether we are adding/editing/deleting a contact, the values of the text fields etc. It is an instance ofMutableStateFlow<ContactState>
.state
: This is a public property that is derived from the_state
,_sortType
, and_contacts
properties using the combine operator. It emits a single object of typeContactState
which contains all the state information required to render the UI. It is an instance ofStateFlow<ContactState>
.
The onEvent
method, which handles incoming events triggered by user interactions:
We use a
when
expression to handle different types of events as sealed classes (a type-safe way of defining restricted hierarchies of classes).If the event is to delete a contact, then a coroutine is launched to delete the contact from the database via the
dao
object.If the event is to hide a dialog (e.g., when the user clicks cancel), then we update the
_state
property to indicate that we are no longer adding a contact.If the
event
is to save a new contact, we extract thefirstName
,lastName
, andphoneNumber
from the current state object. We then check if any of these fields are empty, and if so, we return without saving the contact. Otherwise, we create a new contact object and launch a coroutine to insert it into the database via the DAO object. Finally, we update the_state
object to indicate that we are no longer adding a contact and clear the text fields.If the
event
is to set the value of a text field (e.g., when the user types in their name or phone number), then we update the corresponding value in the_state
property.If the
event
is to show the add contact dialog, then we update the_state
property to indicate that we are now adding a contact.If the
event
is to sort the contacts, then we update the_sortType
property to change the order in which the contacts are sorted.
Compose App UI
Now, we create our UI Files Based on Jetpack Compose with implementations:
Contact screen
Add Contact dialogue.
Create ContactScreen
File.
Code Example:
@Composable
fun ContactScreen(
state: ContactState,
onEvent: (ContactEvent) -> Unit
) {
Scaffold(
floatingActionButton = {
FloatingActionButton(onClick = {
onEvent(ContactEvent.ShowDialog)
}) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = "Add contact"
)
}
},
) { _ ->
if(state.isAddingContact) {
AddContactDialog(state = state, onEvent = onEvent)
}
LazyColumn(
contentPadding = PaddingValues(16.dp),
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
item {
Row(
modifier = Modifier
.fillMaxWidth()
.horizontalScroll(rememberScrollState()),
verticalAlignment = Alignment.CenterVertically
) {
SortType.values().forEach { sortType ->
Row(
modifier = Modifier
.clickable {
onEvent(ContactEvent.SortContacts(sortType))
},
verticalAlignment = CenterVertically
) {
RadioButton(
selected = state.sortType == sortType,
onClick = {
onEvent(ContactEvent.SortContacts(sortType))
}
)
Text(text = sortType.name)
}
}
}
}
items(state.contacts) { contact ->
Row(
modifier = Modifier.fillMaxWidth()
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "${contact.firstName} ${contact.lastName}",
fontSize = 20.sp
)
Text(text = contact.phoneNumber, fontSize = 12.sp)
}
IconButton(onClick = {
onEvent(ContactEvent.DeleteContact(contact))
}) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = "Delete contact"
)
}
}
}
}
}
}
Create AddontactDialog
File.
Code Example:
@Composable
fun AddContactDialog(
state: ContactState,
onEvent: (ContactEvent) -> Unit,
modifier: Modifier = Modifier
) {
AlertDialog(
modifier = modifier,
onDismissRequest = {
onEvent(ContactEvent.HideDialog)
},
title = { Text(text = "Add contact") },
text = {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
TextField(
value = state.firstName,
onValueChange = {
onEvent(ContactEvent.SetFirstName(it))
},
placeholder = {
Text(text = "First name")
}
)
TextField(
value = state.lastName,
onValueChange = {
onEvent(ContactEvent.SetLastName(it))
},
placeholder = {
Text(text = "Last name")
}
)
TextField(
value = state.phoneNumber,
onValueChange = {
onEvent(ContactEvent.SetPhoneNumber(it))
},
placeholder = {
Text(text = "Phone number")
}
)
}
},
buttons = {
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.CenterEnd
) {
Button(onClick = {
onEvent(ContactEvent.SaveContact)
}) {
Text(text = "Save")
}
}
}
)
}
MainActivity (To Wrap up the Code).
Code Example:
class MainActivity : ComponentActivity() {
private val db by lazy {
Room.databaseBuilder(
applicationContext,
ContactDatabase::class.java,
"contacts.db"
).build()
}
private val viewModel by viewModels<ContactViewModel>(
factoryProducer = {
object : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return ContactViewModel(db.dao) as T
}
}
}
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
RoomGuideAndroidTheme {
val state by viewModel.state.collectAsState()
ContactScreen(state = state, onEvent = viewModel::onEvent)
}
}
}
}
End
In this guide, we have learned how to use Room Database with Coroutine in Kotlin to create a simple app that stores and displays user data. We have seen how to create entities, DAOs, and databases using Room annotations and extensions. We have also learned how to perform CRUD operations using suspend functions and Flows. Finally, we have explored how to use viewModelScope to run database operations on the background thread and update the UI on the main thread. Room Database with Coroutine in Kotlin is a powerful and convenient way to work with local data in Android apps.