75. 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 Database and asynchronous database queries.

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

75.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. Now is also a good time to enable view binding in the build.gradle file. 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-kapt'

}

.

.

android {

 

    buildFeatures {

        viewBinding true

    }

.

.

dependencies {

.

.

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

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

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

.

.

}

75.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 75-1 summarizes the structure of the entity:

Column

Data Type

productid

Integer / Primary Key / Auto Increment

productname

String

productquantity

Integer

Table 75-1

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

75.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 75-1:

Figure 75-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.

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

        }

    }

}

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

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

}

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 now needs to provide some methods that can be called by the ViewModel to initiate these operations. 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()

    }

.

.

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

dependencies {

.

.

    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.1'

    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.1'

.

.

}

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

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 suspend fun asyncInsert(product: Product) {

    productDao?.insertProduct(product)

}

 

fun deleteProduct(name: String) {

    coroutineScope.launch(Dispatchers.IO) {

        asyncDelete(name)

    }

}

 

private suspend fun asyncDelete(name: String) {

    productDao?.deleteProduct(name)

}

 

fun findProduct(name: String) {

    coroutineScope.launch(Dispatchers.Main) {

        searchResults.value = asyncFind(name).await()

    }

}

 

private suspend fun asyncFind(name: String): Deferred<List<Product>?> =

 

    coroutineScope.async(Dispatchers.IO) {

        return@async productDao?.findProduct(name)

    }

.

.

For the add and delete database operations the above code adds two methods, one a standard method and the other a coroutine suspend method. In each case, the standard method makes a call to 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 also makes use of 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 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()

    }

.

.

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

}

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

75.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)

    }

}

75.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. We will also need to modify the fragment to make use of view binding. 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 com.ebookfrenzy.roomdemo.Product

import androidx.fragment.app.viewModels

 

import java.util.*

 

import com.ebookfrenzy.roomdemo.databinding.MainFragmentBinding

 

class MainFragment : Fragment() {

 

    private var adapter: ProductListAdapter? = null

.

.

    val viewModel: MainViewModel by viewModels()

    private var _binding: MainFragmentBinding? = null

    private val binding get() = _binding!!

 

   override fun onCreateView(

        inflater: LayoutInflater, container: ViewGroup?,

        savedInstanceState: Bundle?

    ): View {

        _binding = MainFragmentBinding.inflate(inflater, container, false)

        return binding.root

    }

 

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

    binding.productID.text = ""

    binding.productName.setText("")

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

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

 

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

    }

}

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.

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

                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"

            }

        }

    })

}

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.

75.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)

    binding.productRecycler.layoutManager = LinearLayoutManager(context)

    binding.productRecycler.adapter = adapter

}

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

75.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 75-3:

Figure 75-3

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

Figure 75-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 75-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 75-5

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

75.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
13.58.247.31