Android Room Data Persistance (SQLite)

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()
                    }
            })
    } }