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.
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()))
}
}
}
}
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 ORDER BY id DESC")
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)
}
}
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
}
}
<?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>
<?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>
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'
}