Android Kotlin: Room LiveData ViewModel example

Introduction

In Android development, integrating database operations with the user interface can be challenging, especially when aiming for clean and efficient code. Kotlin's support for Android Jetpack components, such as Room, ViewModel, and LiveData, provides an excellent solution for handling database-related tasks in a lifecycle-aware manner. This article demonstrates a practical example of how to use these components together in an Android application. Specifically, it shows how to implement a simple app where users can insert data into a Room database, and observe changes using LiveData, all managed by a ViewModel.

The example covers the essential aspects of using Room for local storage, LiveData for observing data changes, and ViewModel for preserving UI-related data during configuration changes. The code includes database creation, a DAO interface for queries, and a ViewModel to manage UI data.

MainActivity.kt Overview

The MainActivity serves as the entry point of the app. It is responsible for setting up the UI and interacting with the StudentViewModel. Upon launching, the activity uses ViewModelProviders to retrieve an instance of the StudentViewModel. By doing this, the app ensures that the ViewModel survives configuration changes like screen rotations, allowing the UI to remain responsive and efficient.

Inside the onCreate method, the TextView is made scrollable using the ScrollingMovementMethod, and the app observes changes to the list of students stored in the Room database. Whenever the data in the database changes, the LiveData mechanism triggers an update to the TextView, displaying the latest list of students. The Button in the layout allows users to insert new students into the database, where each student is assigned a unique UUID.

RoomSingleton.kt and Room Database

The RoomSingleton class sets up the Room database, ensuring that there is only a single instance of the database throughout the app's lifecycle. This singleton pattern is implemented to prevent multiple instances of the database being created, which could lead to issues like data inconsistency or unnecessary memory usage.

The Room.databaseBuilder method is used to build the Room database, with StudentDao providing the interface for interacting with the studentTbl table. By abstracting database operations through a DAO, the app can perform tasks like data insertion and retrieval without tightly coupling the UI components with the database logic, thus adhering to best practices in software design.

RoomEntity.kt and RoomDao.kt

In the RoomEntity.kt file, a data class named Student is defined. This class represents the structure of the table in the Room database. Each Student object contains an id (a primary key) and a name, which in this example stores a UUID string. The @Entity annotation is used to mark the class as a table definition.

The RoomDao.kt file contains the StudentDao interface, which defines the database operations related to the studentTbl. The allStudents() method is a query that returns a LiveData object containing a list of students. This makes it possible for the UI to observe changes in the database in real-time. The insert() method inserts new student records, and its OnConflictStrategy.REPLACE ensures that in case of conflicts (like inserting a student with the same ID), the new entry will replace the old one.

StudentViewModel.kt

The StudentViewModel.kt file encapsulates the database interaction logic, providing a clean API for the MainActivity to interact with the Room database. The ViewModel holds a reference to the Room database and exposes a LiveData<List<Student>> object that can be observed by the UI.

The ViewModel class fetches the data from the database and provides methods to insert new students. By keeping this logic in the ViewModel, the app maintains a clear separation of concerns, as the UI (activity) doesn't directly interact with the database. This also ensures that the data survives configuration changes, such as screen rotations, which would otherwise destroy the activity and recreate it with a new instance.

Layout and Gradle Dependencies

The layout file activity_main.xml defines a simple UI with a Button and a TextView. The button allows the user to insert new data into the database, while the text view displays the list of students retrieved from the database. The layout ensures that the user can scroll through the list of students, even as more data is added.

In the build.gradle file, essential dependencies for ViewModel, LiveData, and Room are included. Additionally, the project uses the Anko Commons library to simplify background threading operations, such as inserting data into the database in a non-blocking manner.

Summary

This example demonstrates how to implement a simple Android app using Room, LiveData, and ViewModel to manage and display data from a local database. By separating concerns and leveraging Jetpack components, the app becomes more maintainable, responsive, and resilient to configuration changes. The use of LiveData allows the UI to automatically update when the database changes, while the ViewModel ensures that the data is retained even if the activity is destroyed and recreated.

The combination of these components results in a clean and efficient architecture, suitable for building robust Android applications that require local storage and real-time data updates.


MainActivity.kt

package com.cfsuman.jetpack

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.text.method.ScrollingMovementMethod
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import kotlinx.android.synthetic.main.activity_main.*
import org.jetbrains.anko.doAsync
import java.util.*


class MainActivity : AppCompatActivity() {

    private lateinit var model: StudentViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // Make text view content scrollable
        textView.movementMethod = ScrollingMovementMethod()

        // Get the view model
        model = ViewModelProviders.of(this).get(StudentViewModel::class.java)

        // Observe the model
        model.allStudents.observe(this, Observer{ students->
            textView.text = ""
            for (student in students){
                textView.append("${student.id} | ${student.name}\n")
            }
        })


        // Insert data into table
        btn.setOnClickListener {
            doAsync {
                model.insert(Student(null, UUID.randomUUID().toString()))
               }
        }
    }
}
RoomSingleton.kt

package com.cfsuman.jetpack

import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import android.content.Context


@Database(entities = arrayOf(Student::class), version = 1, exportSchema = false)
abstract class RoomSingleton : RoomDatabase(){
    abstract fun studentDao():StudentDao

    companion object{
        private var INSTANCE: RoomSingleton? = null
        fun getInstance(context:Context): RoomSingleton{
            if (INSTANCE == null){
                INSTANCE = Room.databaseBuilder(
                    context,
                    RoomSingleton::class.java,
                    "roomdb")
                    .build()
            }

            return INSTANCE as RoomSingleton
        }
    }
}
RoomEntity.kt

package com.cfsuman.jetpack

import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "studentTbl")
data class Student(
    @PrimaryKey
    var id:Long?,

    @ColumnInfo(name = "uuid")
    var name: String
)
RoomDao.kt

package com.cfsuman.jetpack

import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query

@Dao
interface StudentDao{
    @Query("SELECT * FROM studentTbl")
    fun allStudents(): LiveData<List<Student>>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insert(student: Student)
}
StudentViewModel.kt

package com.cfsuman.jetpack

import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import android.app.Application


class StudentViewModel(application:Application): AndroidViewModel(application){
    private val db:RoomSingleton = RoomSingleton.getInstance(application)
    internal val allStudents : LiveData<List<Student>> = db.studentDao().allStudents()

    fun insert(student:Student){
        db.studentDao().insert(student)
    }
}
activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/rootLayout"
        tools:context=".MainActivity"
        android:background="#fdfdfc">
    <Button
            android:text="Insert Data"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/btn"
            android:layout_marginTop="8dp"
            app:layout_constraintTop_toTopOf="parent"
            android:layout_marginStart="8dp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_marginEnd="8dp"/>
    <TextView
            android:text=""
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/textView"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_marginEnd="16dp"
            app:layout_constraintStart_toStartOf="parent"
            android:layout_marginStart="16dp"
            android:layout_marginTop="16dp"
            app:layout_constraintTop_toBottomOf="@+id/btn"
            android:textAppearance="@style/Base.TextAppearance.AppCompat.Medium"
            android:textColor="#3F51B5"
            tools:text="TextView"
            android:padding="16dp"
    />
</androidx.constraintlayout.widget.ConstraintLayout>
gradle settings

apply plugin: 'kotlin-kapt'

dependencies {
    // ViewModel and LiveData
    def lifecycle_version = "2.0.0"
    implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"

    // Room support
    def room_version = "2.1.0-alpha04"
    implementation "androidx.room:room-runtime:$room_version"
    kapt 'androidx.room:room-compiler:2.1.0-alpha04'

    // Anko Commons
    implementation "org.jetbrains.anko:anko-commons:0.10.5"
}