SQLite is the most used database for mobile Apps. Room Data Persistance is a library introduced in 2017 to help with this task. It uses annotations to generate boilerplate code.
When we use Room in a project, we need to create 3 types of Room specific classes.
- A room
@Database
class, which represents the actual sqlite database. - An
@Entity
class, which represents the tables. DAO
interfaces for data access methods.
Implementation
Let’s modify our build.gradle
and add kapt
for annotations processor. We also add the dependencies for room, databinding, viewmodel, livedata and coroutines.
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android {
buildFeatures {
dataBinding = true
}
}
dependencies {
def lifecycle_version = "2.2.0"
def room_version = "2.2.3"
// annotation processor
kapt "androidx.lifecycle:lifecycle-compiler:$lifecycle_version"
// viewmodel
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
// livedata
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
// sqlite room dependency
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
// coroutines support for room
implementation "androidx.room:room-ktx:$room_version"
// coroutines support in Android
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3"
}
Entity Classes
We’re going to create the following table subscriber_data_table
. It contains three columns.
subscriber_id
(primary key)subscriber_name
subscriber_email
This is its class representation.
@Entity(tableName = "subscriber_data_table")
data class Subscriber(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "subscriber_id")
val id: Int,
@ColumnInfo(name = "subscriber_name")
val name: String,
@ColumnInfo(name = "subscriber_email")
val email: String
)
Data Access Object (DAO) Interface
We have the following DAO class. The function’s name is not important. We only need the @Insert
annotation. The method has suspend
modifier because room doesn’t support database access on the main thread as it might lock the UI for a long time. The easiest way to solve this are kotlin coroutines.
For verification we might need to get a return value. We can get the new rowIds
as the return value.
As parameters, OnConflictStrategy
is an important one. REPLACE
will try to insert the value, and replace it if an existing matchine one if found. On the other hand IGNORE
will just ignore the new one.
With @Query
annotation, room allows us to include a SQL query that would run when the function is called. This query will be verified at compile time.
Room facilitates us to get data from a database table as a LiveData of list of entities. These queries are called Async queries because for them, which have a LiveData as a return value, room always runs them on a background thread by itself, so we don’t have to write special code to use coroutines.
@Dao
interface SubscriberDAO {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(subscriber: Subscriber): Long
@Insert
suspend fun insert(subscribers: List<Subscriber>): List<Long>
@Update
suspend fun update(subscriber: Subscriber)
@Update
suspend fun update(subscribers: List<Subscriber>)
@Delete
suspend fun delete(subscriber: Subscriber)
@Delete
suspend fun delete(subscribers: List<Subscriber>)
@Query("DELETE FROM subscriber_data_table")
suspend fun deleteAll()
// this isn't a suspend function!
@Query("SELECT * FROM subscriber_data_table")
fun getAllSubscribers(): LiveData<List<Subscriber>>
}
Database class
At @Database
we need to provide the list of entity classes, then the database version number. This is important when we are migrating the database from one version to another. Then we need to declare an abstract reference for the DAO interface.
Usually we should only use one instance of a room database for the entire App. In Kotlin we create singletons as companion objects.
@Database(entities = [Subscriber::class], version = 1)
abstract class SubscriberDatabase: RoomDatabase() {
abstract val subscriberDAO: SubscriberDAO
companion object {
@Volatile
private var INSTANCE: SubscriberDatabase? = null
fun getInstance(context: Context): SubscriberDatabase {
synchronized(this) {
var instance = INSTANCE
if(instance==null) {
instance = Room.databaseBuilder(
context.applicationContext,
SubscriberDatabase::class.java,
"subscriber_data_database"
).build()
}
return instance
}
}
}
}
Repository in MVVM Architecture
Model in MVVM means all data management related components. It has local database related components, remote data sources related components and a repository.
The purpose of a repository class it to provide a clean API for ViewModels to easily get and send data. You can think of repositories as mediators between different data sources, such as local databases, web services and caches.
class SubscriberRepository(private val dao: SubscriberDao) {
// this already is LiveData
val subscribers = dao.getAllSubscribers()
suspend fun insert(subscriber: Subscriber) {
dao.insert(subscriber)
}
suspend fun update(subscriber: Subscriber) {
dao.update(subscriber)
}
suspend fun delete(subscriber: Subscriber) {
dao.delete(subscriber)
}
suspend fun deleteAll(subscriber: Subscriber) {
dao.deleteAll(subscriber)
}
}
ViewModel and DataBinding
We’re going to use DataBinding in a ViewModel with its related View. The view will contain two fields that will be updated to show the entity values and the view will contain EditText
for those two values, and two buttons that will change dynamically.
class SubscriberViewModel(private val repository: SubscriberRepository): ViewModel(), Observable {
val subscribers = repository.subscribers
@Bindable
val inputName = MutableLiveData<String>()
@Bindable
val inputEmail = MutableLiveData<String>()
@Bindable
val saveOrUpdateButtonText = MutableLiveData<String>()
@Bindable
val clearAllOrDeleteButtonText = MutableLiveData<String>()
init {
saveOrUpdateButtonText.value = "Save"
clearAllOrDeleteButtonText.value = "Clear All"
}
fun saveOrUpdate() {
val name = inputName.value!!
val email = inputEmail.value!!
// since the ID is AutoGenerated we can just set 0 and it will be ignored
insert(Subscriber(0, name, email))
inputName.value = null
inputEmail.value = null
}
fun clearAllOrDelete() {
clearAll()
}
// this will be executed in a background thread
fun insert(subscriber: Subscriber) = viewModelScope.launch {
repository.insert(subscriber)
}
fun update(subscriber: Subscriber) = viewModelScope.launch {
repository.update(subscriber)
}
fun delete(subscriber: Subscriber) = viewModelScope.launch {
repository.delete(subscriber)
}
fun clearAll() = viewModelScope.launch {
repository.deleteAll()
}
override fun removeOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback?) {
}
override fun addOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback?) {
}
}
This is the related view.
<layout>
<data>
<variable
name="myViewModel"
type="es.codes.mario.templates.SubscriberViewModel"/>
</data>
<LinearLayout>
<EditText
...
android:text="@={myViewModel.inputName}"/>
<EditText
...
android:text="@={myViewModel.inputEmail}"/>
<Button
...
android:text="@={myViewModel.saveOrUpdateButtonText}"
android:onClick="@{()->myViewModel.saveOrUpdate()}"/>
<Button
...
android:text="@={myViewModel.clearAllOrDeleteButtonText}"
android:onClick="@{()->myViewModel.clearAllOrDelete()}"/>
...
</LinearLayout>
</layout>
Then we will create a Factory for our ViewModel.
class SubscriberViewModelFactory(private val repository: SubscriberRepository): ViewModelProvider.Factory {
// this is boilerplate code for all Factories
override fun <T: ViewModel?> create(modelClass: Class<T>): T {
if(modelClass.isAssignableFrom(SubscriberViewModel::class.java)) {
return SubscriberViewModel(repository) as T
}
throw IllegalArgumentException("Unknown View Model class")
}
}
This is the code at Main
that binds everything.
class MainActivity: AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var subscriberViewModel: SubscriberViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
val dao = SubscriberDatabase.getInstance(application).subscriberDAO
val repository = SubscriberRepository(dao)
val factory = SubscriberViewModelFactory(repository)
subscriberViewModel = ViewModelProvider(this, factory).get(SubscriberViewModel::class.java)
binding.myViewModel = subscriberViewModel
// this is for livedata usage with data binding
binding.lifecycleOwner = this
displaySubscribersList()
}
private fun displaySubscribersList() {
subscriberViewModel.subscribers.observe(this, Observer {
Log.i("MyTAG", it.toString())
})
}
}
Displaying Toast with ViewModel and LiveData
This way we display messages through Toast
when methods inside the ViewModel are executed. The ViewModel shouldn’t know any detail about the View.
Copy the following Event
class as is.
open class Event<out T>(private val content: T) {
var hasBeenHandled = false
private set // allows external read but not write
/**
* Returns the content and prevents its use again
*/
fun getContentIfNotHandled(): T? {
return if(hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
/**
* Returns the content, even if it's already been handled
*/
fun peekContent(): T = content
}
Now do this inside ViewModel
class ViewModel {
// this is MutableLiveData so we can edit its value, but it's private so we only can access it from this class.
private val statusMessage = MutableLiveData<Event<String>>()
val message: LiveData<Event<String>>
get() = statusMessage
fun insert() {
...
statusMessage.value = Event("subscriber has been inserted")
}
fun delete() {
...
statusMessage.value = Event("subscriber has been deleted")
}
}
You use it like this, at the class you’re going to use your ViewModel
at.
~~~ kotlin class MainActivity: AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { val viewModel = …
...
viewModel.message.observe(this, Observer {
it.getContentIfNotHandled()?.let {
Toast.makeText(this, it, Toast.LENGTH_LONG).show()
}
})
} }