76. An Android Room Database and Repository Tutorial

This chapter will combine the knowledge gained in the chapter entitled “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 make use of a view model and repository. The tutorial will make use of all of the elements covered in “The Android Room Persistence Library” including entities, a Data Access Object, a Room Databases and asynchronous database queries.

76.1 About the RoomDemo Project

The user interface layout created in the previous chapter was the first step in creating a rudimentary inventory app designed to store the names and quantities of products. When completed, the app will provide the ability to add, delete and search for database entries while also 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.

76.2 Modifying the Build Configuration

Begin by launching Android Studio and opening 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. This is also an ideal opportunity to add the Kotlin extensions plugin. Locate and edit the module level build.gradle file (app -> Gradle Scripts -> build.gradle (Module: RoomDemo.app)) and modify it as follows:

plugins {

    id 'com.android.application'

    id 'kotlin-android'

    id 'kotlin-android-extensions'

    id 'kotlin-kapt'

}

.

.

dependencies {

.

.

    implementation "androidx.room:room-runtime:2.2.5"

    implementation "androidx.fragment:fragment-ktx:1.2.5"

    kapt "androidx.room:room-compiler:2.2.5"

.

.

}

76.3 Building the Entity

This project will begin by creating the entity which defines the schema for the database table. 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 auto-generated. Table 76-6 summarizes the structure of the entity:

Column

Data Type

productid

Integer / Primary Key / Auto Increment

productname

String

productquantity

Integer

Table 76-6

Add a class file for the entity by right clicking on the app -> java -> com.ebookfrenzy.roomdemo entry in the Project tool window and selecting the New -> Kotlin File/Class 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.id = id

        this.productName = productname

        this.quantity = quantity

    }

    constructor(productname: String, quantity: Int) {

        this.productName = productname

        this.quantity = quantity

    }

}

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.annotation.NonNull

import androidx.room.ColumnInfo

import androidx.room.Entity

import androidx.room.PrimaryKey

 

@Entity(tableName = "products")

class Product {

 

    @PrimaryKey(autoGenerate = true)

    @NonNull

    @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

    }

}

These annotations declare this as the entity for a table named products and assigns column names for both the id and name variables. The id column is also configured to be the primary key and auto-generated. Since a primary key can never be null, the @NonNull annotation is also applied. 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.

76.4 Creating the Data Access Object

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

Figure 76-1

Click on OK to generate the new interface and, with the ProductDao.kt file loaded into the code editor, make the following changes:

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

}

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.

76.5 Adding the Room Database

The last task before adding the repository to the project is to implement 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 “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

import com.ebookfrenzy.roomdemo.Product

import com.ebookfrenzy.roomdemo.ProductDao

 

@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<ProductRoomDatabase>(

                            context.applicationContext,

                                ProductRoomDatabase::class.java,

                                    "product_database").build()

                    }

                }

            }

            return INSTANCE

        }

    }

}

76.6 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 and will need to provide methods that use the DAO to insert, delete and query product records. With the exception of the getAllProducts() DAO method (which returns a LiveData object) these database operations will need to be performed on separate threads from the main thread using the AsyncTask class.

Remaining within the ProductRepository.kt file, add the code for the search AsyncTask. Also add a method named asyncFinished() which will be called by the query AsyncTask to return the search results to the repository thread:

package com.ebookfrenzy.roomdemo

 

import android.app.Application

import android.os.AsyncTask

import androidx.lifecycle.LiveData

import androidx.lifecycle.MutableLiveData

 

class ProductRepository(application: Application) {

 

    val searchResults = MutableLiveData<List<Product>>()

    

     fun asyncFinished(results: List<Product>) {

        searchResults.value = results

    }

 

    private class QueryAsyncTask constructor(val asyncTaskDao: ProductDao?) :

                       AsyncTask<String, Void, List<Product>>() {

        var delegate: ProductRepository? = null

 

        override fun doInBackground(vararg params: String): List<Product>? {

            return asyncTaskDao?.findProduct(params[0])

        }

 

        override fun onPostExecute(result: List<Product>) {

            delegate?.asyncFinished(result)

        }

    }

}

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 AsyncTask class contains a constructor method into which must be passed a reference to the DAO object. The doInBackground() method is passed a String containing the product name for which the search is to be performed, passes it to the findProduct() method of the DAO and returns a list of matching Product entity objects which will, in turn, be passed to the onPostExecute() method. Finally, the onPostExecute() method stores the matching product list in the searchResults MutableLiveData object.

The repository will also need to include the following AsyncTask implementation for inserting products into the database:

private class InsertAsyncTask constructor(private val asyncTaskDao: ProductDao?) : AsyncTask<Product, Void, Void>() {

    override fun doInBackground(vararg params: Product): Void? {

        asyncTaskDao?.insertProduct(params[0])

        return null

    }

}

Once again a constructor method is passed a reference to the DAO object, though this time the doInBackground() method is passed an array of Product entity objects to be inserted into the database. Since the app allows only one new product to be added at a time, the method simply inserts the first Product in the array into the database via a call to the insertProduct() DAO method. In this case, no results need to be returned from the task.

The only remaining AsyncTask will be used when deleting products from the database and should be added beneath the insertAsyncTask declaration as follows:

private class DeleteAsyncTask constructor(private val asyncTaskDao: ProductDao?) : AsyncTask<String, Void, Void>() {

 

    override fun doInBackground(vararg params: String): Void? {

        asyncTaskDao?.deleteProduct(params[0])

        return null

    }

}

With the AsyncTask classes defined, the repository class now needs to provide some methods that can be called by the ViewModel to initiate these operations. These methods will create and call appropriate AsyncTask instances and pass through a reference to the DAO. In order to be able to do this, however, the repository needs to obtain the DAO reference via a ProductRoomDatabase instance. 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()

    }

.

.

With a reference to DAO stored, the methods are ready to be added to the class file:

.

.

fun insertProduct(newproduct: Product) {

    val task = InsertAsyncTask(productDao)

    task.execute(newproduct)

}

 

fun deleteProduct(name: String) {

    val task = DeleteAsyncTask(productDao)

    task.execute(name)

}

 

fun findProduct(name: String) {

    val task = QueryAsyncTask(productDao)

    task.delegate = this

    task.execute(name)

}

.

.

In the cases of the insertion and deletion methods, the appropriate AsyncTask instance is created and passed the necessary arguments. In the case of the findProduct() method, the delegate property of the class is set to the repository instance so that the asyncFinished() method can be called after the search completes.

One final task remains to complete the repository class. The RecyclerView in the user interface layout will need to be able to keep up to date 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, each time a change occurs to the database table the UI controller observer will be notified 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()

    }

.

.

76.7 Modifying the ViewModel

The ViewModel is responsible for creating an instance of the repository and for providing methods and LiveData objects that can be utilized by the UI controller to keep the user interface synchronized with the underlying database. As implemented in ProductRepository.kt, the repository constructor requires access to the application context in order to be able to get a Room Database instance. To make the application context accessible within the ViewModel so that it can be passed to the repository, the ViewModel needs to subclass AndroidViewModel instead of ViewModel. Begin, therefore, by editing the MainViewModel.kt file (located in the Project tool window under app -> java -> com.ebookfrenzy.roomdemo -> ui.main) and changing the class to extend AndroidViewModel and to implement the default constructor:

package com.ebookfrenzy.roomdemo.ui.main

 

import android.app.Application

import androidx.lifecycle.AndroidViewModel

import androidx.lifecycle.LiveData

import androidx.lifecycle.MutableLiveData

import com.ebookfrenzy.roomdemo.Product

import com.ebookfrenzy.roomdemo.ProductRepository

 

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

    }

}

The constructor essentially creates a repository instance and then uses it to get references to the results and live data objects so that they can be observed by the UI controller. 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

}

76.8 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 to be used 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 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 76-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.

76.9 Adding the RecyclerView Adapter

As outlined in detail in the chapter entitled “Working with the RecyclerView and CardView Widgets”, a RecyclerView instance requires an adapter class to provide the data to be displayed. Add this class now by right clicking on the app -> java -> com.ebookfrenzy.roomdemo -> ui.main entry in the Project tool window and selecting the New -> Kotlin File/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.ui.main

 

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)

    }

}

76.10 Preparing the Main Fragment

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

package com.ebookfrenzy.roomdemo.ui.main

.

.

import androidx.lifecycle.Observer

import androidx.recyclerview.widget.LinearLayoutManager

import androidx.recyclerview.widget.RecyclerView

import com.ebookfrenzy.roomdemo.Product

import androidx.fragment.app.viewModels

 

import kotlinx.android.synthetic.main.main_fragment.*

import java.util.*

 

class MainFragment : Fragment() {

 

    private var adapter: ProductListAdapter? = null

.

.

    val viewModel: MainViewModel by viewModels()

 

    override fun onActivityCreated(savedInstanceState: Bundle?) {

        super.onActivityCreated(savedInstanceState)

         

        listenerSetup()

        observerSetup()

        recyclerSetup()

    }

.

.

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() {

    productID.text = ""

    productName.setText("")

    productQuantity.setText("")

}

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

76.11 Adding the Button Listeners

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

private fun listenerSetup() {

 

    addButton.setOnClickListener {

        val name = productName.text.toString()

        val quantity = productQuantity.text.toString()

 

        if (name != "" && quantity != "") {

            val product = Product(name, Integer.parseInt(quantity))

            viewModel.insertProduct(product)

            clearFields()

        } else {

            productID.text = "Incomplete information"

        }

    }

 

    findButton.setOnClickListener { viewModel.findProduct(productName.text.toString()) }

 

    deleteButton.setOnClickListener {

        viewModel.deleteProduct(productName.text.toString())

        clearFields()

    }

}

The addButton listener performs some basic validation to ensure that the user has entered both 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.

76.12 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 Mainfragment.kt file, implement the observer setup method as follows:

private fun observerSetup() {

 

    viewModel.getAllProducts()?.observe(this, Observer { products ->

        products?.let {

            adapter?.setProductList(it)

        }

    })

 

    viewModel.getSearchResults().observe(this, Observer { products ->

 

        products?.let {

            if (it.isNotEmpty()) {

                productID.text = String.format(Locale.US, "%d", it[0].id)

                productName.setText(it[0].productName)

                productQuantity.setText(String.format(Locale.US, "%d",

                    it[0].quantity))

            } else {

                productID.text = "No Match"

            }

        }

    })

}

The “all products” observer simply 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 failed, the user is notified via a message displayed on the product ID TextView.

76.13 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)

    val recyclerView: RecyclerView? = view?.findViewById(R.id.product_recycler)

    recyclerView?.layoutManager = LinearLayoutManager(context)

    recyclerView?.adapter = adapter

}

76.14 Testing the RoomDemo App

Compile and run the app on a device or emulator, add some products and make sure that 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 for an existing product, delete it from the database and confirm that it is removed from the RecyclerView product list.

76.15 Using the Database Inspector

As previously outlined in “The Android Room Persistence Library”, the Database Inspector tool window 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 window using the tab button indicated in Figure 76-3:

Figure 76-3

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

Figure 76-4

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.

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 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 76-5 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 76-5

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

76.16 Summary

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

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.14.84.90