Many apps need to deal with persisting data. Perhaps you have an app that stores your favorite pet photos, a social networking app for cat lovers, or an app to maintain lists of things that you might need for your next vacation.
Android provides a number of options, including:
- Shared Preferences: For storing primitive data in key-value pairs.
- Internal Storage: For storing private data on device storage.
- External Storage: For storing public data on shared external storage.
- SQLite Databases: For storing structured data in a private database.
When your data is structured, and you need to be able to do things such as searching for records in that data, a SQLite database is often the best choice. This is where Room comes in. Room is a SQLite wrapper library from Google that removes much of the boilerplate code you need to interact with SQLite, and also adds compile-time checking of your SQL queries.
In this tutorial, you will build an application that creates a generic list that could be used as a shopping, to-do or packing list. In this tutorial you will learn:
- The basics of setting up a Room database.
- How to use a DAO to Create and Read data.
- The basics of unit testing your persistence layer.
- How to hook up your database to an Android UI.
Note: This tutorial assumes that you have some experience developing Android applications. A few points to keep in mind:
import
statements. Use the key combination Option-Return on Mac Alt-Enter on PC to resolve any missing dependencies as you work through your project.Introduction to Android Data Persistence
Classes, Tables, Rows and Instances
To understand Room, it is helpful to understand the sum of its parts, so let’s start with a simple example of storing the names, addresses and phone numbers of a few people.
When you are developing applications using an object-oriented programming language like Kotlin, you use classes to represent the data that you are storing. In our example you could create a class called Person, with the following attributes:
- name
- address
- phoneNumber
For each person you’d then create an instance of a Person, with unique data for that individual.
With a SQL relational database, you would model the Person class as a table. Each instance of that person would be a row in that table. In order to store and retrieve this data, SQL commands need to be be issued to the database, telling it to retrieve and store the data.
For example, to store a record in a table you might use a command like:
INSERT INTO Persons (Name, Address, TelephoneNumber)
VALUES ('Grumpy Cat', '1 Tuna Way, Los Angeles CA', '310-867-5309');
In the early days of Android, if you had a Person object that you wanted to store in the SQLite database, you would have had to create glue code that would turn objects into to SQL and SQL into objects.
ORMs and Android
Long before the days of Android, developers in other object-oriented languages started using a class of tool called an ORM to solve this problem. ORM stands for Object Relational Mapper. The best way to think of it is as a tool designed to automatically generate glue code to map between your object instances and rows in your database.
When Android came on the scene, no ORM existed for the Android environment. Over the years, open-source ORM frameworks emerged, including DBFlow, GreenDAO, OrmLite, SugarORM and Active Android. While these solutions have helped to solve the basic problem of reducing glue code, the developer community has never really gravitated towards one (or two) common solutions. This has led to significant fragmentation and limitations in many of these frameworks, especially with more complex application lifecycles.
Google’s Android Architecture Components and Room
Beyond data persistence, a number of patterns have emerged within the Android development community to deal with things such as maintaining state during application lifecycle changes, callbacks, separating application concerns, view models for MVVM applications, etc. Luckily, in 2017, Google took some of the best practices from the community and created a framework called the Android Architecture Components. Included in this framework was a new ORM called Room. With Room you have an ORM to generate your glue code with the backing of the creators of Android.
Getting Started With Room
To kick things off, start by downloading the materials for this tutorial (you can find the link at the top or bottom of this tutorial), unzip it, and start Android Studio 3.1.1 or later.
In the Welcome to Android Studio dialog, select Import project (Eclipse ADT, Gradle, etc.).
Choose the ListMaster directory of the starter project, and click Open.
If you see a message to update the project’s Gradle plugin since you’re using a later version of Android Studio, go ahead and choose “Update”.
Check out the project for the List Master app and you will see two packages for list categories and list items. You’ll be working only in the first package in this tutorial.
Build and run the application and your app will look like this:
Under the Gradle Scripts part of your project you’ll see a build.gradle file with a (Module:app) notation. Double-click to open it and add the following dependencies that add Room to your project.
implementation "android.arch.persistence.room:runtime:1.0.0"
kapt "android.arch.persistence.room:compiler:1.0.0"
annotationProcessor "android.arch.persistence.room:compiler:1.0.0"
androidTestImplementation "android.arch.persistence.room:testing:1.0.0"
androidTestImplementation "android.arch.core:core-testing:1.1.0"
Go ahead and sync Gradle files once you’ve made the change.
You now have your Room dependencies. Next, you will need to add the following things to use Room in your app:
- Entity: An entity represents the data model that you are mapping to a table in your database.
- DAO: short for Data Access Object, an object with methods used to access the database.
- Database: A database holder that serves as the main access point for the connection to your database.
Entities
An entity is an object that holds data for you to use in your application with some extra information to tell Room about its structure in the database. To start, you are going to create an Entity to store a category_name
and id
in a table called list_categories
.
Under the com.raywenderlich.listmaster.listcategory
package in your app you will see a Kotlin class named ListCategory
. Open it by double-clicking and replace the data class with the following code:
@Entity(tableName = "list_categories")
data class ListCategory(@ColumnInfo(name="category_name") var categoryName: String,
@ColumnInfo(name="id") @PrimaryKey(autoGenerate = true) var id: Long = 0)
The @Entity(tableName = "list_categories")
annotation is telling Room that this is an Entity object that is mapped to a table called list_categories
. The @ColumnInfo
annotation is telling Room about the columns in your table. For example, the name
argument in the @ColumnName(name="category_name")
annotation tells Room that the data class property categoryName
directly after it has a column name of category_name
in the table.
DAOs
Now that you have your Entity which contains your data, you are going to need a way to interact it. This is done with a data access object, also referred to as a DAO. To start out, you are going to create a DAO to insert and retrieve records from the table that you’ve created with your Entity.
To do that, right-click the listcategory package in com.reywenderlich.listmaster in your project, select New > Kotlin File/Class, give it a name of ListCategoryDao
and press OK. When the editor opens up your new file, paste the following code snippet:
@Dao
interface ListCategoryDao {
@Query("SELECT * FROM list_categories")
fun getAll(): List<ListCategory>
@Insert
fun insertAll(vararg listCategories: ListCategory)
}
The first thing you will notice is that for a DAO you’re creating an interface instead of a class. That is because Room is creating the implementation for you. The @Dao
annotation tells Room that this is a Dao interface.
The @Query
annotation on getAll()
tells room that this function definition represents a query and takes a parameter in the form of a SQL statement. In this case you have a statement that retrieves all of the records in the list_categories
table.
Your getAll()
method has a return type of List
. Under the hood, Room looks at the results of the query and maps any fields that it can to the return type you have specified. In your case, you’re querying for multiple ListCategory
entities and have your return value specified as a List
of ListCategory
items.
If you have a query that returns data that Room is not able to map, Room will print a CURSOR_MISMATCH warning and will continue to set any fields that it’s able to.
Your insertAll
method is annotated with @Insert
. This tells Room that you are inserting data into the database. Because Room knows about the table and its columns, it takes care of generating the SQL for you.
Database
Now that you have an Entity and a DAO, you’re going to need an object to tie things together. This is what the Database object does. Under the com.reywenderlich.listmaster
package in your app, create a new Kotlin File/Class called AppDatabase
and paste in the following code:
@Database(entities = [(ListCategory::class)], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun listCategoryDao(): ListCategoryDao
}
You tell Room that the class is a Database object using the @Database
annotation. The entities
parameter tells your database which entities are associated with that database. There is also a version
that is set to 1. The database version will need to be changed when performing a database migration, which will be covered in a later tutorial.
Your database class can be named whatever you want it to be, but it needs to be abstract, and it needs to extend RoomDatabase
. All of your DAOs need to have abstract methods that return the corresponding DAO. This is how Room associates a DAO with a database.
Database Instances
In order to use your database, you’re going to need to create an instance of it in your application. Although you only needed to create a few classes, behind the scenes, Room is doing a lot of work for you to manage, map and generate SQL. Because of this, an instance of Room is resource-intensive, which is why its creators recommend that you use a singleton pattern when when using it in your application.
An easy way to achieve that is by creating an instance of the database when the application is loaded, and then referencing it at the Application level. Go to the com.reywenderlich.listmaster package, open ListMasterApplication by double clicking, and replace the class with the following code:
class ListMasterApplication: Application() {
companion object {
var database: AppDatabase? = null
}
override fun onCreate() {
super.onCreate()
ListMasterApplication.database = Room.databaseBuilder(this, AppDatabase::class.java,
"list-master-db").build()
}
}
When looking at the call to create your database instance you’ll notice that three parameters are passed in. A reference to the app context, a reference to your database class, and a name. Behind the scenes, this name corresponds to a filename that is used to store your database in your apps’ internal storage.
Testing the Essence of your Database with Espresso
When you think about espresso, your first thought may be about about a machine creating a delicious brown nectar that gives you energy during the day. :]
While the happiness and energy from a good cup of espresso are always helpful when you’re developing Android applications, in this case, we are talking about an Android testing framework called Espresso.
Now, to test your database, you could write some code in your activity that inserts data and then prints out the results. You could also begin to wire it to a UI. But, this can be treated as a separate testable piece of the application to make sure your database is set up correctly.
If you haven’t worked with Espresso or unit testing before, don’t worry, we’ll guide you through what you need test your database with it. So grab your cup of espresso and let’s get testing!
In your project, there’s a com.raywenderlich.listmaster
package with (androidTest) next to it.
Under that package, there’s a Kotlin file called ListCategoryDaoTest
. Open it by double-clicking and replace the contents with the following code:
@RunWith(AndroidJUnit4::class)
class ListCategoryDaoTest {
@Rule
@JvmField
val rule: TestRule = InstantTaskExecutorRule()
private lateinit var database: AppDatabase
private lateinit var listCategoryDao: ListCategoryDao
@Before
fun setup() {
val context: Context = InstrumentationRegistry.getTargetContext()
try {
database = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java)
.allowMainThreadQueries().build()
} catch (e: Exception) {
Log.i("test", e.message)
}
listCategoryDao = database.listCategoryDao()
}
@After
fun tearDown() {
database.close()
}
}
So, what is going on here? You created database and DAO properties for the test class. In your setup()
method, you are creating an in-memory version of our database using Room.inMemoryDatabaseBuilder
and getting a reference to your listCategoryDao
. You close the database in tearDown()
.
Now, using the magic of testing you’re going to create some queries and use asserts to see what is going on. Add the following test into the test class:
@Test
fun testAddingAndRetrievingData() {
// 1
val preInsertRetrievedCategories = listCategoryDao.getAll()
// 2
val listCategory = ListCategory("Cats", 1)
listCategoryDao.insertAll(listCategory)
//3
val postInsertRetrievedCategories = listCategoryDao.getAll()
val sizeDifference = postInsertRetrievedCategories.size - preInsertRetrievedCategories.size
Assert.assertEquals(1, sizeDifference)
val retrievedCategory = postInsertRetrievedCategories.last()
Assert.assertEquals("Cats", retrievedCategory.categoryName)
}
Your test is doing the following:
- First, your test calls the
getAll()
method on your DAO that will get all of the current records in your database. - Second, you create an entity object and insert it into your database.
- Finally, you perform some
Assert.assertEquals
calls on the result set after the record is added to ensure that a record was added. To do that, you are comparing the size before and after adding a record to make sure that the difference is 1, and then you look at the last record to ensure that its elements match the record you added.
When you run an Espresso test, it installs your application on a device or emulator and then runs all of the code in your tests. Under the com.raywenderlich.listmaster
package with the (androidTest) notation, right-click your ListCategoryDaoTest file and select Run ‘ListCategoryDaoTest’. You can also click the green arrow next to the test class name, or click the arrow next to an individual test.
When it asks you to select a deployment target, select your favorite emulator or device.
Your project will build, install, and the tests will run. The bottom of your Android Studio window should look like this:
Wiring Up Your Interface
While a unit test against your database is a good start, when you try to run the application, you may feel like this…
To get beyond that, you’d need to have an interface to interact with your database. Your sample project already has the skeleton elements for your applications layout, colors etc. You’re going to hook up some queries to read from and write to your database.
To make that process smoother, you’ll use data binding to feed that data into and out of the interface. Your list will also be using a Recycler View.
When you’re finished, you’ll have an app that shows a list of categories. Each row will have a circle that has the first letter of the category on the left, with one of six colors based on a hash code of the letter.
To begin, go to the listcategory
package under com.raywenderlich.listmaster
, and double-click the file called ListCategoryViewModel
. You will see the following code:
data class ListCategoryViewModel(val listCategory: ListCategory = ListCategory("")) {
private val highlightColors = arrayOf(R.color.colorPrimary, R.color.colorPrimaryDark, R.color.colorAccent,
R.color.primaryLightColor, R.color.secondaryLightColor, R.color.secondaryDarkColor)
fun getHighlightLetter(): String {
return listCategory.categoryName.first().toString()
}
fun getHighlightLetterColor(): Int {
val uniqueIdMultiplier = getHighlightLetter().hashCode().div(6)
val colorArrayIndex = getHighlightLetter().hashCode() - (uniqueIdMultiplier * 6)
Log.i("color", colorArrayIndex.toString())
return (highlightColors[colorArrayIndex])
}
}
In this code, you’re using a standard data class and adding some extra logic for setting up the circle and highlight letter. The highlight tint is hooked in using the adapter in your DataBindingAdapters.kt
file. You’re going to display your list in a RecyclerView, which consists of a ViewHolder and Adapter that you will initialize in your Activity.
To use the adapter, you’ll need to a layout for each item. Your starter project already has one. To take a look at it, open the file under the res/layout part of your project called holder_list_category_item.xml, a snippet of which is shown here:
<data>
<variable
name="listCategoryItem"
type="com.raywenderlich.listmaster.listcategory.ListCategoryViewModel" />
</data>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:layout_editor_absoluteX="8dp">
<TextView
android:id="@+id/category_header"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:background="@drawable/circle"
android:gravity="center"
android:highlight_tint="@{listCategoryItem.highlightLetterColor}"
android:text="@{listCategoryItem.highlightLetter}"
android:textAlignment="center"
android:textColor="@android:color/white"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/category_name"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintHorizontal_weight=".15"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
The listCategoryItem
in the data
tag is your bound View Model. The various @{...}
items in the text view attribute values make use of the data in the View Model. You will also notice a text view attribute called highlight_tint
. This is a custom field that was added via a BindingAdapter in the file named DataBindingAdapters.kt
under com.raywenderlich.listmaster
in your starter package.
Your starter project already has a ListCategoryAdapter
and ListCategoryViewHolder
under the listcategory
package.
In order to display your list, you’re going to need a RecyclerView for the ListCategoriesActivity
. Luckily your starter project has that in the content_list_categories.xml
layout, which is included under the res/layout
section of your project.
<?xml version="1.0" encoding="utf-8"?>
<layout>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:context="com.raywenderlich.listmaster.ListCategoriesActivity"
tools:showIn="@layout/activity_list_categories">
<android.support.v7.widget.RecyclerView
android:id="@+id/list_category_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</android.support.constraint.ConstraintLayout>
</layout>
Now it’s time to wire up your activity. To do that, you will need to add a few things to ListCategoriesActivity
. Go to the listcategory
package under com.raywenderlich.listmaster
and open ListCategoriesActivity
by double-clicking.
To start, paste in the following properties near the top of the class:
private lateinit var contentListsBinding: ContentListCategoriesBinding
private lateinit var appDatabase: AppDatabase
private lateinit var listCategoryDao: ListCategoryDao
The first property will be generated by the data binding API when the project is built. The other properties are for references to the database and the list category DAO. If necessary, add the following import to your import statments:
import com.raywenderlich.listmaster.databinding.ContentListCategoriesBinding
Next, add the following function to the bottom of the activity:
private fun setupRecyclerAdapter() {
val recyclerViewLinearLayoutManager = LinearLayoutManager(this)
contentListsBinding = activityListsBinding.contentLists!!
contentListsBinding.listCategoryRecyclerView.layoutManager = recyclerViewLinearLayoutManager
listCategoryAdapter = ListCategoryAdapter(listOf(), this)
contentListsBinding.listCategoryRecyclerView.adapter = listCategoryAdapter
}
As its name suggests, this function sets up the recycler view. In order to initialize this before you have a list of items from the database, an empty list is passed to its adapter.
Now that you have these building blocks, paste the snippet below right after the setupAddButton()
call in your onCreate()
method:
// Set up our database
appDatabase = ListMasterApplication.database!!
listCategoryDao = appDatabase.listCategoryDao()
// Set up our recycler adapter
setupRecyclerAdapter()
This is getting references to your database
and listCategoryDao
for use in your activity, along with calling your setupRecyclerAdapter()
method.
Next, add code to query Room for the list of categories and add them to the adapter by pasting the onResume
method into your ListCategoriesActivity
class:
override fun onResume() {
super.onResume()
AsyncTask.execute({
listCategoryAdapter.categoryList = listCategoryDao.getAll()
runOnUiThread { listCategoryAdapter.notifyDataSetChanged() }
})
}
You may have noticed that this call the the DAO is run in a AsyncTask. This is because these database calls are reaching out to your file system and could take a while to complete, especially if you have a complex query. A long query can cause the UI to freeze or even produce a dreaded Application Not Responding error. Since both of these things can lead to unhappy users, it’s best to always put these calls in a background thread.
Now that you have your list query wired in, you’re going to need to have a way to add in category items. To do that, your starter project has a dialog layout in a file called dialog_add_category.xml
that you are going to hook up to the Add button. Find the function called setupAddButton
in your ListCategoriesActivity
and update the call to setPositiveButton
on alertDialogBuilder
to look as follows:
alertDialogBuilder.setPositiveButton(android.R.string.ok, { dialog: DialogInterface,
which: Int ->
AsyncTask.execute({
listCategoryDao.insertAll(listCategoryViewModel.listCategory)
listCategoryAdapter.categoryList = listCategoryDao.getAll()
runOnUiThread { listCategoryAdapter.notifyDataSetChanged() }
})
})
You insert the new category from the dialog into the database, and then update your list of categories on the adapter and refresh the adapter on the UI thread.
Now it’s time to run your application! Go to the Run menu and select the Run option. Note: If you select ListCategoryDaoTest instead, it will run your tests instead of your app.
Next, select the App option:
Then, select a device to run it on:
Your app will run and you’ll be able to add categories to your list.
Go ahead and add a few categories. Then stop the app and build and run again.
You see that your categories have been persisted in SQLite thanks to Room! :]
Where To Go From Here?
You can download the final project using the link at the top or bottom of this tutorial.
There’s a lot more to learn and do with Room!
- Migrations for making changes to an existing data store.
- Indexes to make your queries faster.
- Type Converters to make it easier to map database types to your own custom types.
Stay tuned for future Room tutorials where these will be covered. :]
If you’re just getting started with data persistence on Android and want some more background, you can check out our Saving Data on Android video course, which covers SharedPreferences, saving to files, SQLite and migrations, and also persisting using Room along with other Android Architecture Components such as ViewModel and LiveData.
As a challenge, you can also try to:
- Add the ability to delete a list category.
- Create a
onClick
event in each category that allows you to edit the category and save it to your database.
Feel free to share your feedback, findings or ask any questions in the comments below or in the forums. I hope you enjoyed this tutorial on data persistence with Room!
The post Data Persistence With Room appeared first on Ray Wenderlich.