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.
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()))
}
}
}
}
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
}
}
}
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
)
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)
}
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)
}
}
<?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>
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"
}