Shopping List with a Room

S

This tutorial teaches you how to save data in a local database using a Room. Room provides an abstraction layer over SQLite to allow fluent database access while harnessing the full power of SQLite. Read carefully Room introduction from the Android developer site: Room overview. Get familiar with Database, Entity and DAO. You can also read other sections behind “Save data in a local database” in the Android Developer site.

Create a new project

Launch Android Studio and create a new project with default settings.

  • Give unique application name Room Shopping List
  • Use your own company domain name in package name (for example example.com)
  • Select Phone and Table target and for example Android API 28 – Android 9 Pie, or a newer one
  • Select Basic Activity and use default naming for the Activity, Layout and Resources (leave checkboxes as selected), template includes: AppBar and FloatingActionButton
  • Click Finish

Add room libraries

You need to add Room libraries to build.gradle (Module: App) file in your project. Remember check latest stable Room version and use that one: Room.

Open build.gradle (Module: App) and following line inside dependencies block.

apply plugin: 'kotlin-kapt' // <- ADD THIS ONE TO USE ROOM

dependencies {
 // ADD THESE TO Use Room Architecture Components
 def room_version = "2.2.4"
 implementation "androidx.room:room-runtime:$room_version"
 kapt  "androidx.room:room-compiler:$room_version"
}

Create a Layout files

You will need layout files for the MainActivity, which will hold a RecyclerView. RecyclerView need a layout file to display each shopping list item. And finally one layout file for the custom dialog to ask a new shopping list item.

activity_main.xml

Create a new Vector Asset and use it in the FloatingActionButton. Select your project app folder with right mouse button and select: new > Vector Asset. Click a "android robot" and search "add" asset. Rename asset and use white color.

Modify activity_main.xml file and use app:srcCompat="@drawable/ic_add_white_24dp" in FloatingActionButton.

content_main.xml

Modify content_main.xml file to contain only RecyclerView inside ConstraintLayout. Remember that you just can drag and drop RecyclerView to your layout, if you are using a design mode.

<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/recyclerView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_marginStart="8dp"
    android:layout_marginTop="8dp"
    android:layout_marginEnd="8dp"
    android:layout_marginBottom="8dp"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

recyclerview_item.xml

Add a new layout file and name it to recyclerview_item.xml. This layout will be used inside a RecyclerView component.

Use horizontal LinearLayout to hold three TextViews. Give the following id's to TextViews: nameTextViewcountTextView and priceTextView.

You can use String resources to display some text in TextViews, because those will be overwritten from the code.

dialog_ask_new_shopping_list_item.xml

Add a new layout file and name it to dialog_ask_new_shopping_list_item.xml. This layout will be used in the custom dialog.

Use multiple LinearLayout's to hold three TextViews and EditTexts. Give the following id's to EditTexts: nameEditTextcountEditText and priceEditText.

You can use hints to display some text in EditTexts.

Room components

ShoppingListItem

Create a new Kotlin file and name it to ShoppingListItem. This file represents a table within the Room database. Table name will be shopping_list_table, id will be autoGenerate id and other variables describes a shopping list data.

import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "shopping_list_table")
data class ShoppingListItem(
  @PrimaryKey(autoGenerate = true)
  var id: Int,
  var name: String?,
  var count: Int?,
  var price: Double?
)

ShoppingListDao

Create a new Kotlin file and name it to ShoppingListDao. This file contains methods used for accessing the database. All shopping list items can be queried with getAll() function. This function will return MutableList with ShoppingListItems. A new item can be added with insert() function. This function will return the auto increment id (Long) to the caller application. One item can be deleted with a delete() function. This function id parameter will be used inside "DELETE FROM" query.

import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query

@Dao
interface ShoppingListDao {

  @Query("SELECT * from shopping_list_table")
  fun getAll(): MutableList

  @Insert
  fun insert(item: ShoppingListItem) : Long

  @Query("DELETE FROM shopping_list_table WHERE id = :itemId")
  fun delete(itemId: Int)

}

ShoppingListRoomDatabase

Create a new Kotlin file and name it to ShoppingListRoomDatabase. This file contains the database holder and serves as the main access point for the underlying connection to your app's persisted, relational data. Above dao funtions can be called with shoppingListDao() function.

import androidx.room.Database
import androidx.room.RoomDatabase

@Database(entities = [ShoppingListItem::class], version = 1)
abstract class ShoppingListRoomDatabase : RoomDatabase() {
  abstract fun shoppingListDao(): ShoppingListDao
}

RecyclerView and it's Adapter

Get familiar with a RecyclerView and Adapter from course material or here Create a List with RecyclerView from Android Developer site.

ShoppingListAdapter

RecyclerView needs an adapter, which holds the recyclerView's data and binds it to the UI. Adapter will get the shopping list data using a parameter. onCreateViewHolder function will be called every single time, when a new shopping list item will be displayed in the recycler view. This will use ViewHolder inner class to get access to recyclerview_item layout views. Finally onBindViewHolder function will be called to bind data to layout and UI. update function will be used later when a new data will be updated to adapter.

Create a new Kotlin file and name it to ShoppingListAdapter.

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

class ShoppingListAdapter (var shoppingList: MutableList<ShoppingListItem>)
    : RecyclerView.Adapter<ShoppingListAdapter.ViewHolder>() {

  // create UI View Holder from XML layout
  override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
    val layoutInflater = LayoutInflater.from(parent.context)
    val view = layoutInflater.inflate(R.layout.recyclerview_item, parent, false)
    return ViewHolder(view)
  }

  // return shopping list size
  override fun getItemCount(): Int = shoppingList.size

  // bind data to UI View Holder
  override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    // item to bind UI
    val item: ShoppingListItem = shoppingList[position]
    // name, count, price
    holder.nameTextView.text = item.name
    holder.countTextView.text = item.count.toString()
    holder.priceTextView.text = item.price.toString()
  }

  // View Holder class to hold UI views
  inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
    val nameTextView: TextView = view.nameTextView
    val countTextView: TextView = view.countTextView
    val priceTextView: TextView = view.priceTextView
  }

  // update data inside adapter
  fun update(newList: MutableList<ShoppingListItem>) {
    shoppingList = newList
    notifyDataSetChanged()
  }
}

Use a RecyclerView to display ShoppingList items

Created ShoppingListAdapter need to be used inside RecyclerView. Modify MainActivity's onCreate() function to include adapter and use it in a recyclerView. A new empty shoppingList instance will be created and passed to the adapter.

class MainActivity : AppCompatActivity() {
  // Shopping List items
  private var shoppingList: MutableList = ArrayList()
  // Shopping List adapter
  private lateinit var adapter: ShoppingListAdapter

  // onCreate
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    setSupportActionBar(toolbar)

    // Use LinearManager as a layout manager for recyclerView
    recyclerView.layoutManager = LinearLayoutManager(this)
    // Create Adapter for recyclerView
    adapter = ShoppingListAdapter(shoppingList)
    recyclerView.adapter = adapter

    // continue here...
  }
}

Initialize database and load shopping list items

Database connection

Define a shopping list Room database object inside MainActivity class.

// Shopping List Room database
private lateinit var db: ShoppingListRoomDatabase

Modify MainActivity's onCreate() function to initialize database object after adapter in above code. Here we acquire an instance of database to our code.

// Create database and get instance
db = Room.databaseBuilder(applicationContext, ShoppingListRoomDatabase::class.java, "hs_db").build()
// load all shopping list items
loadShoppingListItems()

Load all shopping list items from db

Create a loadShoppingListItems() function below onCreate() function. A good question is, how we can load data from the database, because it can't be done in Main UI thread (we can, if allowMainThreadQueries() is used, when a database connection is created - but it should be avoid). Use a AsyncTask, Handler or maybe own Thread? All of those are a good answers, but... We need to do a different actions to the database, how we can handle all of those nicely? In a real world, an Android Architecture Components should be used. This tutorial will kept now as simple as it can be. That's why, own thread will be used with database actions.

A new thread will be created and all the stored shopping list items will be loaded. Message will be send (to UI thread) with a Handler and adapter's data will be updated line 19.

// Load shopping list items from db
private fun loadShoppingListItems() {
  // Question: How to create a database operation, if UI thread can't be used
  // -> Handler
  // -> AsyncTask
  // -> Thread
  // How about insert, query, delete, update, etc...
  // -> So you might need to do a multiple Handlers or AsyncTask to handle all situations...
  // => Not GOOD!
  // Other options: Android Architecture Components, RXJava, RXAndroid, RXKotlin
  // - You will learn these in other exercises
  // OK - Now, we will use own Thread and Handler for a learning purpose

  // Create a Handler Object
  val handler = Handler(Handler.Callback {
    // Toast message
    Toast.makeText(applicationContext,it.data.getString("message"), Toast.LENGTH_SHORT).show()
    // Notify adapter data change
    adapter.update(shoppingList)
    true
  })
  // Create a new Thread to insert data to database
  Thread(Runnable {
    shoppingList = db.shoppingListDao().getAll()
    val message = Message.obtain()
    if (shoppingList.size > 0)
      message.data.putString("message","All shopping list items read from db!")
    else
      message.data.putString("message","Shopping list is empty!")
    handler.sendMessage(message)
  }).start()
}

Add a new ShoppingList item with a custom dialog

AskShoppingListItemDialogFragment

Create a new Kotlin file and name it to AskShoppingListItemDialogFragment. This fragment will be used to create a custom dialog, which will be used to ask a new shopping list item from the user. If you are not a familiar with a dialogs yet, you should read Dialogs material from the Android Developer site.

Modify code to include class and onCreateDialog() function, which will be used to create a custom dialog.

class AskShoppingListItemDialogFragment : DialogFragment() {
  override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
    // continue here...
  }
}

The easiest way to create a dialog is to use AlertDialog.Builder class. This class allows you to include dialog view, title, message and buttons easily. Function need to return an instance of the created dialog.

Modify the content's of the onCreateDialog() as shown in a below. First we load the custom layout for the dialog in line 3. A new dialog will be created inline 6. Dialog's button event handlers are defined in lines 10 and 19. A new shopping list item will be created, when "Ok" button is pressed. Dialog will only closed now. A new shopping list item will not be send/saved to database yet. This will be corrected soon.

 return activity?.let {
  // Custom layout for dialog
  val customView = LayoutInflater.from(context).inflate(R.layout.dialog_ask_new_shopping_list_item, null)

  // Build dialog
  val builder = AlertDialog.Builder(it)
  builder.setView(customView)
  builder.setTitle(R.string.dialog_title)
    .setMessage(R.string.dialog_message)
    .setPositiveButton(R.string.dialog_ok) { dialog, id ->
      // create a ShoppingList item
      val name = customView.nameEditText.text.toString()
      val count = customView.countEditText.text.toString().toInt()
      val price = customView.priceEditText.text.toString().toDouble()
      val item = ShoppingListItem(0, name, count, price)
      // more programming need here later..
      dialog.dismiss()
    }
    .setNegativeButton(R.string.dialog_cancel) { dialog, id ->
      dialog.dismiss()
    }
  builder.create()
} ?: throw IllegalStateException("Activity cannot be null")

Sending a new shopping list item from dialog to MainActivity

Now dialog is working as a fragment inside a MainActivity. You will need to define an interface with a method for each type of click event in dialog (now we are only defining a positivebutton == ok). Then, implement that interface in the host component, which will receive the action events from the dialog.

Create AddDialogListener interface inside AskShoppingListItemDialogFragment and define onDialogPositiveClick function inside it. This function need to be inside a MainActivity if it is using this interface.

// Use this instance of the interface to deliver action events
  private lateinit var mListener: AddDialogListener

  /* The activity that creates an instance of this dialog fragment must
    * implement this interface in order to receive event callbacks.
    * Each method passes the DialogFragment in case the host needs to query it. */
  interface AddDialogListener {
    fun onDialogPositiveClick(item: ShoppingListItem)
  }

  // Override the Fragment.onAttach() method to instantiate the AddDialogListener
  override fun onAttach(context: Context) {
    super.onAttach(context)
    // Verify that the host activity implements the callback interface
    try {
      // Instantiate the AddDialogListener so we can send events to the host
      mListener = context as AddDialogListener
    } catch (e: ClassCastException) {
      // The activity doesn't implement the interface, throw exception
      throw ClassCastException((context.toString() +
              " must implement AddDialogListener"))
    }
  }

Modify dialog class to call above interface method, before dialog closes in setPositiveButton.

.setPositiveButton(R.string.dialog_ok) { dialog, id ->
  // create a ShoppingList item
  val name = customView.nameEditText.text.toString()
  val count = customView.countEditText.text.toString().toInt()
  val price = customView.priceEditText.text.toString().toDouble()
  val item = ShoppingListItem(0, name, count, price)
  mListener.onDialogPositiveClick(item)
  dialog.dismiss()
}

Modify MainActivity to use above created interface

Now above dialog will call host's onDialogPositiveClick function, if host is using that interface. Modify MainActivity to use above interface.

class MainActivity : AppCompatActivity(), AskShoppingListItemDialogFragment.AddDialogListener {
...
}

And create onDialogPositiveClick inside it. Here you will get the new shopping list item from the dialog. A new thread will be created and new item will be inserted to the database. Insert function will be used and it returns autoincrement id used with a new item in database. It will be used inside the item used in the UI. So, the correct item can be later deleted with a swipe.

// Add a new shopping list item to db
override fun onDialogPositiveClick(item: ShoppingListItem) {
  // Create a Handler Object
  val handler = Handler(Handler.Callback {
    // Toast message
    Toast.makeText(applicationContext,it.data.getString("message"), Toast.LENGTH_SHORT).show()
    // Notify adapter data change
    adapter.update(shoppingList)
    true
  })
  // Create a new Thread to insert data to database
  Thread(Runnable {
    // insert and get autoincrement id of the item
    val id = db.shoppingListDao().insert(item)
    // add to view
    item.id = id.toInt()
    shoppingList.add(item)
    val message = Message.obtain()
    message.data.putString("message","Item added to db!")
    handler.sendMessage(message)
  }).start()
}

Add a new item

Use a floating action button to create and open a new custom dialog. Modify FAB code in onCreate() function to open a custom dialog.

// add a new item to shopping list
fab.setOnClickListener { view ->
  // create and show dialog
  val dialog = AskShoppingListItemDialogFragment()
  dialog.show(supportFragmentManager, "AskNewItemDialogFragment")
}

Delete a shopping list item with a swipe

A final step in this tutorial is to delete a row from the recyclerView (also in adapter and db). Add a initSwipe() function call at the last line in the onCreate() function and create a following function inside MainActivity.

Here ItemTouchHelper will be used to determine LEFT swipe in line 4. You need to define onSwiped() and onMoved() functions. Now only onSwipedfunction is used for a deleting. RecyclerViews holder position will be used to remove shopping list item from UI and from the database. Own made thread will be used with item deletion. After deletion is ready, Hander object will be used to show a message to end user and notify data change in the adapter.

// Initialize swipe in recyclerView
private fun initSwipe() {
  // Create Touch Callback
  val simpleItemTouchCallback = object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT) {
    // Swiped
    override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
      // adapter delete item position
      val position = viewHolder.adapterPosition
      // Create a Handler Object
      val handler = Handler(Handler.Callback {
        // Toast message
        Toast.makeText(applicationContext,it.data.getString("message"), Toast.LENGTH_SHORT).show()
        // Notify adapter data change
        adapter.update(shoppingList)
        true
      })
      // Get remove item id
      var id = shoppingList[position].id
      // Remove from UI list
      shoppingList.removeAt(position)
      // Remove from db
      Thread(Runnable {
        db.shoppingListDao().delete(id)
        val message = Message.obtain()
        message.data.putString("message","Item deleted from db!")
        handler.sendMessage(message)
      }).start()
    }

    // Moved
    override fun onMove(p0: RecyclerView, p1: RecyclerView.ViewHolder, p2: RecyclerView.ViewHolder): Boolean {
      return true
    }

  }
  // Attach Item Touch Helper to recyclerView
  val itemTouchHelper = ItemTouchHelper(simpleItemTouchCallback)
  itemTouchHelper.attachToRecyclerView(recyclerView)
}

Test

Run your application. Now it should save shopping list items to local database.

Add comment

Pasi Manninen

Pasi Manninen

Pasi is a mobile and web application developer. Currently working as a senior lecturer in JAMK University of Applied Sciences. Pasi has programming experience over many decades and has taught dozens of courses since the 90's.

Recent Posts

Follow Me