Android Kotlin: Room RecyclerView LiveData example

Introduction

In modern Android app development, building efficient and responsive user interfaces that can handle large datasets is a crucial aspect. Combining components like RecyclerView for displaying data and Room for database management is a common practice, especially when you want to ensure smooth data flow and user experience. This example demonstrates how to use Room, RecyclerView, and LiveData in Kotlin to create an application where user inputs can be dynamically stored in a local database and displayed in real-time.

This tutorial will walk through each part of the code in detail, from setting up the data model and database to integrating a view model and RecyclerView for efficient data handling. By the end of the description, you'll have a clear understanding of how these components work together to create a seamless Android application.

MainActivity.kt

In the MainActivity.kt, the app's primary logic is implemented. First, a StudentViewModel is retrieved using ViewModelProviders, which provides access to data (in this case, a list of students) that persists across activity restarts. The activity also initializes the RecyclerView, setting a LinearLayoutManager for vertical scrolling, which is essential for displaying data in a list format.

The key functionality is observing changes in the allStudents LiveData from the StudentViewModel. Whenever the data changes, the RecyclerView is updated by setting a new RecyclerViewAdapter. This ensures that the UI reflects any updates in the data, such as when new students are added. The button click listener inserts a new Student into the database asynchronously using doAsync to keep the main thread free from heavy operations.

RoomSingleton.kt

The RoomSingleton class defines the Room database configuration for the app. It ensures a single instance of the database exists during the application's lifecycle using the singleton pattern. The @Database annotation specifies that the database has one table, defined by the Student entity, and its version is set to 1. The companion object includes the getInstance() method, which creates the database if it doesn't exist and returns the instance, providing centralized access to the Room database.

By using Room.databaseBuilder, the app creates the database named "roomdb". This class is crucial for managing database connections efficiently and providing access to the studentDao() interface.

RoomEntity.kt

The Student data class defines the table schema for the studentTbl table in the Room database. Each instance of Student represents a row in the table. It has two properties: id, which serves as the primary key and is automatically generated, and name, which stores the student's UUID.

Annotations like @PrimaryKey and @ColumnInfo specify how the table is structured in the database. This mapping between Kotlin classes and database tables makes it easy to work with SQL-based databases without manually writing SQL queries.

RoomDao.kt

The StudentDao interface is the Data Access Object (DAO) for interacting with the studentTbl in the Room database. The @Dao annotation marks this interface as a Room DAO. It defines two methods: allStudents(), which retrieves all students from the table in descending order by id, and insert(), which inserts a new student into the table.

The allStudents() method returns a LiveData<List<Student>>, allowing the app to observe changes in the table and update the UI automatically without needing explicit callbacks. The insert() method is annotated with OnConflictStrategy.REPLACE, which ensures that if a student with the same id already exists, it will be replaced with the new one.

StudentViewModel.kt

The StudentViewModel class extends AndroidViewModel, providing a lifecycle-conscious component that holds the app’s data. It retrieves the list of students from the Room database and exposes it as LiveData through the allStudents property. This allows the UI to automatically observe the data and react to changes without additional configuration.

Additionally, the insert() method in the ViewModel calls the DAO's insert() method, providing a simple way for the MainActivity to add new data to the database. By managing the data in a ViewModel, the app separates UI logic from data handling, which promotes a clean architecture.

RecyclerViewAdapter.kt

The RecyclerViewAdapter class manages the display of data in the RecyclerView. It extends RecyclerView.Adapter and binds the student data to each item in the list. When the onBindViewHolder() method is called, the student’s id and name are assigned to the respective TextView elements in the custom layout (custom_view.xml).

The getItemCount() method ensures the RecyclerView knows how many items to display, which is derived from the size of the student list. This adapter allows dynamic content to be displayed in a scrollable list format, making it an essential part of the UI for this app.

XML Layouts

The activity_main.xml defines the layout for the main screen, including a button to insert data and a RecyclerView to display the list of students. The RecyclerView is constrained to take up most of the screen's height, positioned below the button.

The custom_view.xml defines the layout for individual items in the RecyclerView. It uses MaterialCardView to give each item a card-like appearance, and two TextView elements display the student's ID and name. The layout ensures that each student entry appears neatly within the list.

Summary

This example demonstrates how to build an Android app that uses Room, RecyclerView, and LiveData to create a responsive, data-driven interface. The app efficiently stores and retrieves student data using a Room database and updates the UI in real-time using LiveData. By leveraging the ViewModel architecture, the app separates UI logic from data handling, promoting a cleaner and more maintainable codebase.

The combination of Room, RecyclerView, and LiveData is a powerful pattern in Android development that allows for smooth data handling, responsive UIs, and scalable applications. Understanding how these components interact will help developers build modern Android apps that are robust and efficient.


MainActivity.kt

package com.cfsuman.jetpack

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
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)

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

        // Specify layout for recycler view
        val linearLayoutManager = LinearLayoutManager(
            this, RecyclerView.VERTICAL,false)
        recyclerView.layoutManager = linearLayoutManager

        // Observe the model
        model.allStudents.observe(this, Observer{ students->
            // Data bind the recycler view
            recyclerView.adapter = RecyclerViewAdapter(students)
        })

        // 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 ORDER BY id DESC")
    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)
    }
}
RecyclerViewAdapter.kt

package com.cfsuman.jetpack

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.custom_view.view.*

class RecyclerViewAdapter(val students: List<Student>)
    : RecyclerView.Adapter<RecyclerViewAdapter.ViewHolder>(){

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int)
            : RecyclerViewAdapter.ViewHolder {
        val v: View = LayoutInflater.from(parent.context)
            .inflate(R.layout.custom_view,parent,false)
        return ViewHolder(v)
    }

    override fun onBindViewHolder(holder: RecyclerViewAdapter.ViewHolder, position: Int) {
        holder.id.text = students[position].id.toString()
        holder.name.text = students[position].name
    }

    override fun getItemCount(): Int {
        return students.size
    }

    override fun getItemId(position: Int): Long {
        return super.getItemId(position)
    }

    override fun getItemViewType(position: Int): Int {
        return super.getItemViewType(position)
    }

    class ViewHolder(itemView:View): RecyclerView.ViewHolder(itemView){
        val id = itemView.tvId
        val name = itemView.tvName
    }
}
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"/>
    <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recyclerView"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_marginEnd="8dp"
            app:layout_constraintStart_toStartOf="parent"
            android:layout_marginStart="8dp"
            app:layout_constraintHorizontal_bias="0.0"
            android:layout_marginBottom="8dp"
            app:layout_constraintBottom_toBottomOf="parent"
            android:layout_marginTop="8dp"
            app:layout_constraintTop_toBottomOf="@+id/btn"/>
</androidx.constraintlayout.widget.ConstraintLayout>
custom_view.xml

<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:contentPadding="10dp"
    android:layout_margin="3dp"
    app:cardElevation="2dp"
    app:cardMaxElevation="3dp">
    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <TextView
            android:id="@+id/tvId"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            tools:text="100"
            android:layout_margin="10dp"
            app:layout_constraintBottom_toBottomOf="parent"
            android:layout_marginBottom="8dp"
            android:layout_marginTop="8dp"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            android:layout_marginStart="8dp"/>
        <TextView
            android:id="@+id/tvName"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            tools:text="dsfdfdsfdfdf"
            android:layout_marginTop="4dp"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_marginEnd="8dp"
            app:layout_constraintStart_toEndOf="@+id/tvId"
            android:layout_marginStart="8dp"
            app:layout_constraintHorizontal_bias="0.0"
            android:layout_margin="10dp"
        />
    </androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
gradle settings

apply plugin: 'kotlin-kapt'

dependencies {
    // Material components theme
    implementation 'com.google.android.material:material:1.0.0'

    // 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"

    // RecyclerView
    implementation 'androidx.recyclerview:recyclerview:1.0.0'
}