Skip to content

Exercise 11 - ShoppingList with a Room

Project name : E11 Room Shopping List

Introduction

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

Example video:

Create a new Android application project

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

  • Select Empty Activity template
  • Give unique application name E11 Room Shopping List
  • Use your own company domain name in package name (for example example.com)
  • Click Finish

Add Room libraries

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

Basicly you will need to modify your project gradle.build file.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
plugins {
    ...
    // add kotlin kapt
    id 'kotlin-kapt'
}
...
dependencies {
  ...
  // Add Room library
  def room_version = "2.4.1"
  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. These will be covered next.

Layout for the main activity

First delete Template generated "Hello World" TextView from your layout and add Recycler View to your main layout. Remember give a recyclerView id for your RecyclerView.

Then 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. Use white color.

!Image 10

Add Float Action Button to your activity_main.xml file and use created drawable in FloatingActionButton.

!Image 10

RecyclerView's layout item

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: nameTextView, countTextView and priceTextView.

!Image 03

Click image to see it bigger!

Tip

You can add sample text to your strings.xml resources and use those text in your TextView elements to see that your layout works correctly. Remember set your layout height to wrap_content or your one item will take a whole screen space: android:layout_height="wrap_content".

Ask a new shoppling list item

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: nameEditText, countEditText and priceEditText.

!Image 04

Click image to see it bigger!

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query

@Dao
interface ShoppingListDao {

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

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

1
2
3
4
5
6
7
import androidx.room.Database
import androidx.room.RoomDatabase

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

RecyclerView and Adapter

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

ShoppingList Adapter

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.

Create a new Kotlin file and name it to ShoppingListAdapter.

1
2
3
4
5
6
7
8
9
class ShoppingListAdapter (var shoppingList: MutableList<ShoppingListItem>)
    : RecyclerView.Adapter<ShoppingListAdapter.ViewHolder>() {

  // onCreateViewHolder
  // ViewHolder
  // onBindViewHolder
  // getItemCount
  // update
}

onCreateViewHolder

onCreateViewHolder function will be called every single time, when a new shopping list item will be displayed in the recycler view.

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

ViewHolder

This will use ViewHolder inner class to get access to recyclerview_item layout views.

1
2
3
4
5
6
7
// View Holder class to hold UI views
inner class ViewHolder(view: View)
  : RecyclerView.ViewHolder(view) {
    val nameTextView: TextView = view.findViewById(R.id.nameTextView)
    val countTextView: TextView = view.findViewById(R.id.countTextView)
    val priceTextView: TextView = view.findViewById(R.id.priceTextView)
}

onBindViewHolder

onBindViewHolder function will be called to bind data to layout and UI.

1
2
3
4
5
6
7
8
9
// 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()
}

getItemCount

getItemCount will return item's count in your data.

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

update

An own update function will be used later when a new data will be updated to adapter.

1
2
3
4
5
// 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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class MainActivity : AppCompatActivity() {
  // Shopping List items
  private var shoppingList: MutableList<ShoppingListItem> = ArrayList()
  // Shopping List adapter
  private lateinit var adapter: ShoppingListAdapter
  // RecyclerView
  private lateinit var recyclerView: RecyclerView

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

    // Find recyclerView from loaded layout
    recyclerView = findViewById(R.id.recyclerView)
    // Use LinearManager as a layout manager for recyclerView
    recyclerView.layoutManager = LinearLayoutManager(this)
    // Create Adapter for recyclerView
    adapter = ShoppingListAdapter(shoppingList)
    recyclerView.adapter = adapter

    //...
  }
  //...

Initialize database and load shopping list items

Database connection

Define a shopping list Room database object inside MainActivity class.

1
2
// 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.

1
2
3
4
5
6
7
8
9
// Create database and get instance
db = Room.databaseBuilder(
  applicationContext, 
  ShoppingListRoomDatabase::class.java, 
  "shopping_list_table"
).build()

// load all shopping list items
loadShoppingListItems()

Load all shopping list items from database

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 (read, delete, update, ...) 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 be 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 12.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Load shopping list items from db
private fun loadShoppingListItems() {
  // Create a new Handler object to display a message in UI
  val handler = Handler(Looper.getMainLooper(), 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 read data from database
  Thread(Runnable {
    shoppingList = db.shoppingListDao().getAll()
    val message = Message.obtain()
    if (shoppingList.size > 0)
      message.data.putString("message","Data read from db!")
    else
      message.data.putString("message","Shopping list is empty!")
    handler.sendMessage(message)
  }).start()
}

Add a new shopping list item

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.

1
2
3
4
5
6
class AskShoppingListItemDialogFragment 
: DialogFragment() {
  override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
    // continue here, add below code in 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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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.findViewById<TextView>(R.id.nameEditText).text.toString()
      val count = customView.findViewById<TextView>(R.id.countEditText).text.toString().toInt()
      val price = customView.findViewById<TextView>(R.id.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 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. This is how we can pass the data from dialog to main activity.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 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.

1
2
3
4
5
6
7
8
9
.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 interface

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

1
2
3
4
5
6
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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Add a new shopping list item to db
override fun onDialogPositiveClick(item: ShoppingListItem) {
  // Create a Handler Object
  val handler = Handler(Looper.getMainLooper(), 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. Add FAB code in onCreate() function to open a custom dialog.

1
2
3
4
5
6
// add a new item to shopping list
findViewById<FloatingActionButton>(R.id.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 exercise 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 onSwiped function 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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// 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(Looper.getMainLooper(), 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)
}

Run and test your application

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

 

Test and push

Test your application in emulator, take screenshots and commit/push your project and screenshots back to JAMKIT/GitLab. Remember move your exercise/issue ticket from Doing to In Review in Issues Board and write your learning comments to issue comments.