An Android Studio Room Database Tutorial

This chapter will combine the knowledge gained in Using the Android Room Persistence Library with the initial project created in the previous chapter to provide a detailed tutorial demonstrating how to implement SQLite-based database storage using the Room persistence library. In keeping with the Android architectural guidelines, the project will use a view model and repository. The tutorial will use all the elements covered in Using the Android Room Persistence Library, including entities, a Data Access Object, a Room Database, and asynchronous database queries.

About the RoomDemo Project

The user interface layout created in the previous chapter was the first step in creating a rudimentary inventory app to store product names and quantities. When completed, the app will provide the ability to add, delete and search for database entries while displaying a scrollable list of all products currently stored in the database. This product list will update automatically as database entries are added or deleted.

Modifying the Build Configuration

Launch Android Studio and open the RoomDemo project started in the previous chapter. Before adding any new classes to the project, the first step is to add some additional libraries to the build configuration, including the Room persistence library. Locate and edit the project level build.gradle.kts file (app -> Gradle Scripts -> build.gradle.kts (Project :RoomDemo)) as follows:

plugins {
    id("com.android.application") version "8.1.0-rc01" apply false
    id("org.jetbrains.kotlin.android") version "1.8.0" apply false
    id("com.google.devtools.ksp") version "1.9.10-1.0.13" apply false
}Code language: Gradle (gradle)

Next, make the following changes to the module level build.gradle.kts file (app -> Gradle Scripts -> build.gradle. kts (Module :app)):

plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
    id("com.google.devtools.ksp")
}
.
.
dependencies {
.
.
    implementation ("androidx.room:room-runtime:2.6.0")
    implementation ("androidx.fragment:fragment-ktx:1.6.2")
    ksp("androidx.room:room-compiler:2.6.0")
.
.
}Code language: Gradle (gradle)

Building the Entity

This project will begin by creating the entity defining the database table schema. The entity will consist of an integer for the product id, a string column to hold the product name, and another integer value to store the quantity. The entity will consist of an integer for the product id, a string column to hold the product name, and another integer value to store the quantity. The product id column will serve as the primary key and will be autogenerated. Table 71-1 summarizes the structure of the entity:

 

You are reading a sample chapter from Android Studio Giraffe Essentials – Kotlin Edition.

Buy the full book now in eBook (PDF) or Print format.

The full book contains 94 chapters and over 830 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

Column

Data Type

productid

Integer / Primary Key / Auto Increment

productname

String

productquantity

Integer

Table 71-1

Add a class file for the entity by right-clicking on the app -> kotlin+java -> com.ebookfrenzy.roomdemo entry in the Project tool window and selecting the New -> Kotlin Class/File menu option. In the new class dialog, name the class Product, select the Class entry in the list, and press the keyboard return key to generate the file. When the Product.kt file opens in the editor, modify it so that it reads as follows:

package com.ebookfrenzy.roomdemo
 
class Product {
 
    var id: Int = 0
    var productName: String? = null
    var quantity: Int = 0
 
    constructor() {}
 
    constructor(id: Int, productname: String, quantity: Int) {
        this.productName = productname
        this.quantity = quantity
    }
    constructor(productname: String, quantity: Int) {
        this.productName = productname
        this.quantity = quantity
    }
}Code language: Kotlin (kotlin)

The class now has variables for the database table columns and matching getter and setter methods. Of course, this class does not become an entity until it has been annotated. With the class file still open in the editor, add annotations and corresponding import statements:

package com.ebookfrenzy.roomdemo
 
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
 
@Entity(tableName = "products")
class Product {
 
    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "productId")
    var id: Int = 0
 
    @ColumnInfo(name = "productName")
    var productName: String? = null
    var quantity: Int = 0
 
    constructor() {}
 
    constructor(id: Int, productname: String, quantity: Int) {
        this.id = id
        this.productName = productname
        this.quantity = quantity
    }
    constructor(productname: String, quantity: Int) {
        this.productName = productname
        this.quantity = quantity
    }
}Code language: Kotlin (kotlin)

These annotations declare this as the entity for a table named products and assign column names for the id and name variables. The id column is also configured to be the primary key and auto-generated. Since it will not be necessary to reference the quantity column in SQL queries, a column name has not been assigned to the quantity variable.

Creating the Data Access Object

With the product entity defined, the next step is to create the DAO interface. Referring again to the Project tool window, right-click on the app -> kotlin+java -> com.ebookfrenzy.roomdemo entry and select the New -> Kotlin Class/ File menu option. In the new class dialog, enter ProductDao into the Name field and select Interface from the list as highlighted in Figure 71-1:

Figure 71-1

Press the Return key to generate the new interface and, with the ProductDao.kt file loaded into the code editor, make the following changes:

 

You are reading a sample chapter from Android Studio Giraffe Essentials – Kotlin Edition.

Buy the full book now in eBook (PDF) or Print format.

The full book contains 94 chapters and over 830 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

package com.ebookfrenzy.roomdemo
 
import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
 
@Dao
interface ProductDao {
 
    @Insert
    fun insertProduct(product: Product)
 
    @Query("SELECT * FROM products WHERE productName = :name")
    fun findProduct(name: String): List<Product>
 
    @Query("DELETE FROM products WHERE productName = :name")
    fun deleteProduct(name: String)
 
    @Query("SELECT * FROM products")
    fun getAllProducts(): LiveData<List<Product>>
}Code language: Kotlin (kotlin)

The DAO implements methods to insert, find and delete records from the products database. The insertion method is passed a Product entity object containing the data to be stored, while the methods to find and delete records are passed a string containing the name of the product on which to perform the operation. The getAllProducts() method returns a LiveData object containing all of the records within the database. This method will be used to keep the RecyclerView product list in the user interface layout synchronized with the database.

Adding the Room Database

The last task before adding the repository to the project is implementing the Room Database instance. Add a new class to the project named ProductRoomDatabase, this time with the Class option selected.

Once the file has been generated, modify it as follows using the steps outlined in the Using the Android Room Persistence Library chapter:

package com.ebookfrenzy.roomdemo
 
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
 
@Database(entities = [(Product::class)], version = 1)
abstract class ProductRoomDatabase: RoomDatabase() {
 
    abstract fun productDao(): ProductDao
 
    companion object {
 
        private var INSTANCE: ProductRoomDatabase? = null
 
        internal fun getDatabase(context: Context): ProductRoomDatabase? {
            if (INSTANCE == null) {
                synchronized(ProductRoomDatabase::class.java) {
                    if (INSTANCE == null) {
                        INSTANCE = 
                           Room.databaseBuilder(
                            context.applicationContext,
                                ProductRoomDatabase::class.java, 
                                    "product_database").build()
                    }
                }
            }
            return INSTANCE
        }
    }
}Code language: Kotlin (kotlin)

Adding the Repository

Add a new class named ProductRepository to the project, with the Class option selected.

The repository class will be responsible for interacting with the Room database on behalf of the ViewModel. It must provide methods that use the DAO to insert, delete, and query product records. Except for the getAllProducts() DAO method (which returns a LiveData object), these database operations must be performed on separate threads from the main thread.

 

You are reading a sample chapter from Android Studio Giraffe Essentials – Kotlin Edition.

Buy the full book now in eBook (PDF) or Print format.

The full book contains 94 chapters and over 830 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

Remaining within the ProductRepository.kt file, make the following changes :

package com.ebookfrenzy.roomdemo
 
import android.app.Application
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import kotlinx.coroutines.*
 
class ProductRepository(application: Application) {
 
    val searchResults = MutableLiveData<List<Product>>()
}Code language: Kotlin (kotlin)

The above declares a MutableLiveData variable named searchResults into which the results of a search operation are stored whenever an asynchronous search task completes (later in the tutorial, an observer within the ViewModel will monitor this live data object).

The repository class must now provide some methods the ViewModel can call to initiate these operations. However, the repository needs to obtain the DAO reference via a ProductRoomDatabase instance to do this. Add a constructor method to the ProductRepository class to perform these tasks:

.
.
class ProductRepository(application: Application) {
 
    val searchResults = MutableLiveData<List<Product>>()
    private var productDao: ProductDao?
 
    init {
        val db: ProductRoomDatabase? = 
                   ProductRoomDatabase.getDatabase(application)
        productDao = db?.productDao()
    }
.
.Code language: Kotlin (kotlin)

The repository will use coroutines to avoid performing database operations on the main thread (a topic covered in the chapter entitled A Guide to Kotlin Coroutines). As such, some additional libraries must be added to the project before work on the repository class can continue. Start by editing the Gradle Scripts -> build.gradle. kts (Module :app) file to add the following lines to the dependencies section:

dependencies {
.
.
    implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
    implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4")
.
.Code language: Gradle (gradle)

After making the change, click on the Sync Now link at the top of the editor panel to commit the changes.

 

You are reading a sample chapter from Android Studio Giraffe Essentials – Kotlin Edition.

Buy the full book now in eBook (PDF) or Print format.

The full book contains 94 chapters and over 830 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

With a reference to DAO stored and the appropriate libraries added, the methods are ready to be added to the ProductRepository class file as follows:

.
.
val searchResults = MutableLiveData<List<Product>>()
private var productDao: ProductDao?
private val coroutineScope = CoroutineScope(Dispatchers.Main)
.
.
fun insertProduct(newproduct: Product) {
    coroutineScope.launch(Dispatchers.IO) {
        asyncInsert(newproduct)
    }
}
 
private fun asyncInsert(product: Product) {
    productDao?.insertProduct(product)
}
 
fun deleteProduct(name: String) {
    coroutineScope.launch(Dispatchers.IO) {
        asyncDelete(name)
    }
}
 
private fun asyncDelete(name: String) {
    productDao?.deleteProduct(name)
}
 
fun findProduct(name: String) {
 
    coroutineScope.launch(Dispatchers.Main) {
        searchResults.value = asyncFind(name).await()
    }
}
 
private fun asyncFind(name: String): Deferred<List<Product>?> =
 
    coroutineScope.async(Dispatchers.IO) {
        return@async productDao?.findProduct(name)
    }
.
.Code language: Kotlin (kotlin)

For the add and delete database operations, the above code adds two methods: a standard method and a coroutine suspend method. In each case, the standard method calls the suspend method to execute the coroutine outside of the main thread (using the IO dispatcher) so as not to block the app while the task is being performed. In the case of the find operation, the asyncFind() suspend method uses a deferred value to return the search results to the findProduct() method. Because the findProduct() method needs access to the searchResults variable, the call to the asyncFind() method is dispatched to the main thread, which, in turn, performs the database operation using the IO dispatcher.

One final task remains to complete the repository class. The RecyclerView in the user interface layout must keep up to date with the current list of products stored in the database. The ProductDao class already includes a method named getAllProducts() which uses a SQL query to select all of the database records and return them wrapped in a LiveData object. The repository needs to call this method once on initialization and store the result within a LiveData object that can be observed by the ViewModel and, in turn, by the UI controller. Once this has been set up, the UI controller observer will be notified each time a change occurs to the database table, and the RecyclerView can be updated with the latest product list. Remaining within the ProductRepository.kt file, add a LiveData variable and call to the DAO getAllProducts() method within the constructor:

.
.
class ProductRepository(application: Application) {
.
.
    val allProducts: LiveData<List<Product>>?
    
    init {
        val db: ProductRoomDatabase? = 
                ProductRoomDatabase.getDatabase(application)
        productDao = db?.productDao()
        allProducts = productDao?.getAllProducts()
    }
.
.Code language: Kotlin (kotlin)

Adding the ViewModel

The ViewModel is responsible for creating an instance of the repository and providing methods, and LiveData objects that the UI controller can utilize to keep the user interface synchronized with the underlying database. As implemented in ProductRepository.kt, the repository constructor requires access to the application context to get a Room Database instance. To make the application context accessible within the ViewModel so it can be passed to the repository, the ViewModel needs to subclass AndroidViewModel instead of ViewModel.

Begin by locating the com.ebookfrenzy.viewmodeldemo entry in the Project tool window, right-clicking it, and selecting the New -> Kotlin Class/File menu option. Next, name the new class MainViewModel and press the keyboard Enter key. Finally, edit the new class file to change the class to extend AndroidViewModel and implement the default constructor:

 

You are reading a sample chapter from Android Studio Giraffe Essentials – Kotlin Edition.

Buy the full book now in eBook (PDF) or Print format.

The full book contains 94 chapters and over 830 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

package com.ebookfrenzy.roomdemo
 
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
 
class MainViewModel(application: Application) : AndroidViewModel(application) {
 
    private val repository: ProductRepository = ProductRepository(application)
    private val allProducts: LiveData<List<Product>>?
    private val searchResults: MutableLiveData<List<Product>>
 
    init {
        allProducts = repository.allProducts
        searchResults = repository.searchResults
    }
}Code language: Kotlin (kotlin)

The constructor creates a repository instance and then uses it to get references to the results and live data objects so that the UI controller can observe them. All that now remains within the ViewModel is to implement the methods that will be called from within the UI controller in response to button clicks and when setting up observers on the LiveData objects:

fun insertProduct(product: Product) {
    repository.insertProduct(product)
}
 
fun findProduct(name: String) {
    repository.findProduct(name)
}
 
fun deleteProduct(name: String) {
    repository.deleteProduct(name)
}
 
fun getSearchResults(): MutableLiveData<List<Product>> {
    return searchResults
}
 
fun getAllProducts(): LiveData<List<Product>>? {
    return allProducts
}Code language: Kotlin (kotlin)

Creating the Product Item Layout

The name of each product in the database will appear within the RecyclerView list in the main user interface. This will require a layout resource file containing a TextView for each row in the list. Add this file now by right-clicking on the app -> res -> layout entry in the Project tool window and selecting the New -> Layout Resource File menu option. Name the file product_list_item and change the root element to a vertical LinearLayout before clicking on OK to create the file and load it into the layout editor. With the layout editor in Design mode, drag a TextView object from the palette onto the layout, where it will appear by default at the top of the layout:

Figure 71-2

With the TextView selected in the layout, use the Attributes tool window to set the ID of the view to product_ row and the layout_height to 30dp. Select the LinearLayout entry in the Component Tree window and set the layout_height attribute to wrap_content.

Adding the RecyclerView Adapter

As outlined in detail in the chapter entitled Android RecyclerView and CardView Overview, a RecyclerView instance requires an adapter class to provide the data to be displayed. Add this class by right-clicking on the app -> kotlin+java -> com.ebookfrenzy.roomdemo entry in the Project tool window and selecting the New -> Kotlin Class menu. In the dialog, name the class ProductListAdapter and choose Class from the list before pressing the keyboard Return key. With the resulting ProductListAdapter.kt class loaded into the editor, implement the class as follows:

package com.ebookfrenzy.roomdemo
 
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.ebookfrenzy.roomdemo.Product
import com.ebookfrenzy.roomdemo.R
 
class ProductListAdapter(private val productItemLayout: Int) : 
               RecyclerView.Adapter<ProductListAdapter.ViewHolder>() {
 
    private var productList: List<Product>? = null
 
    override fun onBindViewHolder(holder: ViewHolder, listPosition: Int) {
        val item = holder.item
        productList.let {
            item.text = it!![listPosition].productName
        }
    }
 
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): 
                                                             ViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(
                                   productItemLayout, parent, false)
        return ViewHolder(view)
    }
 
    fun setProductList(products: List<Product>) {
        productList = products
        notifyDataSetChanged()
    }
 
    override fun getItemCount(): Int { 
        return if (productList == null) 0 else productList!!.size
    }
 
    class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        var item: TextView = itemView.findViewById(R.id.product_row)
    }
}Code language: Kotlin (kotlin)

Preparing the Main Activity

The last remaining component to modify is the MainActivity class which needs to configure listeners on the Button views and observers on the live data objects in the ViewModel class. Before adding this code, some preparation work must be performed to add some imports and variables. Edit the MainActivity.kt file and modify it as follows:

 

You are reading a sample chapter from Android Studio Giraffe Essentials – Kotlin Edition.

Buy the full book now in eBook (PDF) or Print format.

The full book contains 94 chapters and over 830 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

package com.ebookfrenzy.roomdemo
.
.
import androidx.activity.viewModels
import androidx.recyclerview.widget.LinearLayoutManager
import com.ebookfrenzy.roomdemo.Product

import java.util.*
.
.
class MainActivity : AppCompatActivity() {
 
   private lateinit var binding: ActivityMainBinding
   private var adapter: ProductListAdapter? = null
   private val viewModel: MainViewModel by viewModels()
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
 
        listenerSetup()
        observerSetup()
        recyclerSetup()
    }
.
.Code language: Kotlin (kotlin)

At various stages in the code, the app will need to clear the product information displayed in the user interface. To avoid code repetition, add the following clearFields() convenience function:

private fun clearFields() {
    binding.productID.text = ""
    binding.productName.setText("")
    binding.productQuantity.setText("")
}Code language: Kotlin (kotlin)

Before the app can be built and tested, the three setup methods called from the onCreate() method above need to be added to the class.

Adding the Button Listeners

The user interface layout for the main fragment contains three buttons, each needing to perform a specific task when clicked by the user. Edit the MainActivity.kt file and add the listenerSetup() method:

private fun listenerSetup() {
 
    binding.addButton.setOnClickListener {
        val name = binding.productName.text.toString()
        val quantity = binding.productQuantity.text.toString()
 
        if (name != "" && quantity != "") {
            val product = Product(name, Integer.parseInt(quantity))
            viewModel.insertProduct(product)
            clearFields()
        } else {
            binding.productID.text = "Incomplete information"
        }
    }
 
    binding.findButton.setOnClickListener { viewModel.findProduct(
                              binding.productName.text.toString()) }
 
    binding.deleteButton.setOnClickListener {
        viewModel.deleteProduct(binding.productName.text.toString())
        clearFields()
    }
}Code language: Kotlin (kotlin)

The addButton listener performs some basic validation to ensure that the user has entered a product name and quantity and uses this data to create a new Product entity object (note that the quantity string is converted to an integer to match the entity data type). The ViewModel insertProduct() method is then called and passed the Product object before the fields are cleared.

The findButton and deleteButton listeners pass the product name to either the ViewModel findProduct() or deleteProduct() method.

 

You are reading a sample chapter from Android Studio Giraffe Essentials – Kotlin Edition.

Buy the full book now in eBook (PDF) or Print format.

The full book contains 94 chapters and over 830 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

Adding LiveData Observers

The user interface now needs to add observers to remain synchronized with the searchResults and allProducts live data objects within the ViewModel. Remaining in the MainActivity.kt file, implement the observer setup method as follows:

private fun observerSetup() {
 
    viewModel.getAllProducts()?.observe(this) { products ->
        products?.let {
            adapter?.setProductList(it)
        }
    }
 
    viewModel.getSearchResults().observe(this) { products ->
 
        products?.let {
            if (it.isNotEmpty()) {
                binding.productID.text = String.format(Locale.US, "%d", it[0].id)
                binding.productName.setText(it[0].productName)
                binding.productQuantity.setText(
                    String.format(
                        Locale.US, "%d",
                        it[0].quantity
                    )
                )
            } else {
                binding.productID.text = "No Match"
            }
        }
    }
}Code language: Kotlin (kotlin)

The “all products” observer passes the current list of products to the setProductList() method of the RecyclerAdapter where the displayed list will be updated.

The “search results” observer checks that at least one matching result has been located in the database, extracts the first matching Product entity object from the list, gets the data from the object, converts it where necessary, and assigns it to the TextView and EditText views in the layout. If the product search fails, the user is notified via a message displayed on the product ID TextView.

Initializing the RecyclerView

Add the final setup method to initialize and configure the RecyclerView and adapter as follows:

private fun recyclerSetup() {
    adapter = ProductListAdapter(R.layout.product_list_item)
    binding.productRecycler.layoutManager = LinearLayoutManager(this)
    binding.productRecycler.adapter = adapter
}Code language: Kotlin (kotlin)

Testing the RoomDemo App

Compile and run the app on a device or emulator, add some products, and ensure they appear automatically in the RecyclerView. Perform a search for an existing product and verify that the product ID and quantity fields update accordingly. Finally, enter the name of an existing product, delete it from the database, and confirm that it is removed from the RecyclerView product list.

 

You are reading a sample chapter from Android Studio Giraffe Essentials – Kotlin Edition.

Buy the full book now in eBook (PDF) or Print format.

The full book contains 94 chapters and over 830 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

When building the project, you may encounter an error that reads in part:

org.gradle.api.GradleException: 'compileDebugJavaWithJavac' task (current target is 1.8) and 'kspDebugKotlin' task (current target is 17) jvm target compatibility should be set to the same Java version.Code language: plaintext (plaintext)

This is caused by a bug in the Android Studio build toolchain and can be resolved by making the following changes to the build.gradle.kts (Module: app) file:

compileOptions {
    sourceCompatibility JavaVersion.VERSION_17
    targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
    jvmTarget = '17'
}Code language: Gradle (gradle)

Using the Database Inspector

As previously outlined in Using the Android Room Persistence Library, the Database Inspector tool may be used to inspect the content of Room databases associated with a running app and to perform minor data changes. After adding some database records using the RoomDemo app, display the Database Inspector tool using the View -> Tool Windows -> App Inspection menu option:

From within the inspector window, select the running app from the menu marked A in Figure 71-3 below:

Figure 71-3

From the Databases panel (B), double-click on the products table to view the table rows currently stored in the database. Enable the Live updates option (C) and then use the running app to add more records to the database. Note that the Database Inspector updates the table data (D) in real-time to reflect the changes.

 

You are reading a sample chapter from Android Studio Giraffe Essentials – Kotlin Edition.

Buy the full book now in eBook (PDF) or Print format.

The full book contains 94 chapters and over 830 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

Turn off Live updates so that the table is no longer read-only, double-click on the quantity cell for a table row, and change the value before pressing the keyboard Enter key. Return to the running app and search for the product to confirm that the change made to the quantity in the inspector was saved to the database table. Finally, click on the table query button (indicated by the arrow in Figure 71-4 below) to display a new query tab (A), make sure that product_database is selected (B), and enter a SQL statement into the query text field (C) and click the Run button(D):

Figure 71-4

The list of rows should update to reflect the SQL query (E) results.

Summary

This chapter has demonstrated the use of the Room persistence library to store data in a SQLite database. The finished project used a repository to separate the ViewModel from all database operations. It demonstrated the creation of entities, a DAO, and a room database instance, including the use of asynchronous tasks when performing some database operations.