Quantcast
Channel: Kodeco | High quality programming tutorials: iOS, Android, Swift, Kotlin, Unity, and more
Viewing all articles
Browse latest Browse all 4370

Data Persistence With Room

$
0
0

Data Persistence With Room

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:

  • You will be using the Android RecyclerView to display lists. If you’ve never used them or need a refresher, the Android RecyclerView Tutorial with Kotlin is a great place to start.
  • This tutorial utilizes Data Binding and Binding Adapters. Again, if you have never used these or need a refresher, you should take a look at the data binding documentation from the Android project pages.
  • The code snippets in this tutorial do not include the needed 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.

    Glue code

    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.

    Room as Glue

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

    Welcome to Android Studio

    Choose the ListMaster directory of the starter project, and click Open.

    Import project

    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:

    Starter app

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

    Espresso

    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.

    androidTest package

    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:

    1. First, your test calls the getAll() method on your DAO that will get all of the current records in your database.
    2. Second, you create an entity object and insert it into your database.
    3. 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.

    Run test menu

    When it asks you to select a deployment target, select your favorite emulator or device.

    Select device

    Your project will build, install, and the tests will run. The bottom of your Android Studio window should look like this:

    Passing test

    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…

    Where is the data?

    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.

    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.

    Run the app

    Next, select the App option:

    App option

    Then, select a device to run it on:

    Select device

    Your app will run and you’ll be able to add categories to your list.

    Add category

    Go ahead and add a few categories. Then stop the app and build and run again.

    Saved Data

    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!

    Puppy

    The post Data Persistence With Room appeared first on Ray Wenderlich.


    Viewing all articles
    Browse latest Browse all 4370

    Trending Articles



    <script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>