Discover millions of ebooks, audiobooks, and so much more with a free trial

Only $11.99/month after trial. Cancel anytime.

Elements of Android Room
Elements of Android Room
Elements of Android Room
Ebook529 pages2 hours

Elements of Android Room

Rating: 0 out of 5 stars

()

Read preview

About this ebook

Storing data locally is a key part of many Android apps. SQLite is built into Android, but the low-level API that the Android SDK provides is a bit clunky to use.

Google’s solution for that is Room, an object wrapper around that API. This gives us a cleaner, type-safe, reactive API for our database operations. Room is part of the Android Jetpack, and so it is a key element of Google’s recommended “stack” of technologies for building Android apps.

This book explores Room, starting with basic stuff like:

- Adding Room to your app
- Defining your entities and data access operation (DAO) APIs
- Testing your database I/O
- Using Room with Kotlin coroutines, LiveData, and RxJava
- Migrating your database schema as your app evolves

It also gets into more elaborate scenarios, such as using SQLCipher for Android for encrypting your Room database.

LanguageEnglish
PublisherMark Murphy
Release dateJan 29, 2022
ISBN9781005564100
Elements of Android Room
Author

Mark Murphy

Mark Murphy is a FranklinCovey Senior Consultant who has facilitated content successfully to clients worldwide for the last twenty-nine years. During that time, he also spent eleven years as a founding partner of a small boutique firm specializing in project management consulting. Mark grew up in Colorado and lives in Dallas, Texas.

Read more from Mark Murphy

Related to Elements of Android Room

Related ebooks

Internet & Web For You

View More

Related articles

Reviews for Elements of Android Room

Rating: 0 out of 5 stars
0 ratings

0 ratings0 reviews

What did you think?

Tap to rate

Review must be at least 10 words

    Book preview

    Elements of Android Room - Mark Murphy

    Room Basics

    Google describes Room as providing an abstraction layer over SQLite to allow fluent database access while harnessing the full power of SQLite.

    In other words, Room aims to make your use of SQLite easier, through a lightweight annotation-based implementation of an object-relational mapping (ORM) engine.

    Wrenching Relations Into Objects

    If you have ever worked with a relational database — like SQLite — from an object-oriented language — like Java or Kotlin — undoubtedly you have encountered the object-relational impedance mismatch. That is a very fancy way of saying it’s a pain getting stuff into and out of the database.

    In object-oriented programming, we are used to objects holding references to other objects, forming some sort of object graph. However, traditional SQL-style relational databases work off of tables of primitive data, using foreign keys and join tables to express relationships. Figuring out how to get our classes to map to relational tables is aggravating, and it usually results in a lot of boilerplate code.

    Traditional Android development uses SQLiteDatabase for interacting with SQLite. That, in turn, uses Cursor objects to represent the results of queries and ContentValues objects to represent data to be inserted or updated. While Cursor and ContentValues are objects, they are fairly generic, much in the way that a HashMap or ArrayList is generic. In particular, neither Cursor nor ContentValues has any of our business logic. We have to somehow either wrap that around those objects or convert between those objects and some of ours.

    That latter approach is what object-relational mapping engines (ORMs) take. A typical ORM works off of Java/Kotlin code and either generates a suitable database structure or works with you to identify how the classes should map to some existing table structure (e.g., a legacy one that you are stuck with). The ORM usually generates some code for you, and supplies a library, which in combination hide much of the database details from you.

    The quintessential Java ORM is Hibernate. However, Hibernate was developed with server-side Java in mind and is not well-suited for slim platfoms like Android devices. However, a vast roster of Android ORMs and similar libraries have been created over the years to try to fill that gap. Some of the more popular ones have been:

    SQLDelight

    DBFlow

    greenDAO

    OrmLite

    Sugar ORM

    Room also helps with the object-relational impedance mismatch. It is not as deep of an ORM as some of the others, as you will be dealing with SQL a fair bit. However, Room has one huge advantage: it is from Google, and therefore it will be deemed official in the eyes of many developers and managers.

    While this book is focused on Room, you may wish to explore other ORMs if you are interested in using Java/Kotlin objects but saving the data in SQLite. Room is popular, but it is far from the only option. In particular, if you are interested in Kotlin/Multiplatform for cross-platform development, you will want to look at SQLDelight, so your database operations can also be cross-platform.

    Room Requirements

    To use Room, you need two dependencies in your module’s build.gradle file:

    The runtime library

    An annotation processor

    In a Kotlin project, those will be:

    room-ktx, to pull in the core Room runtime libraries plus some Kotlin-specific extensions

    room-compiler, used with kapt

    For example, in the NoteBasics module of the book’s primary sample project, we have a build.gradle file that pulls in those two artifacts:

    apply plugin: 'com.android.library' apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt'

     

     

    android {

     

       

    compileSdkVersion 31

     

     

       

    defaultConfig {

     

           

    minSdkVersion 21

     

           

    targetSdkVersion 30

     

         

    testInstrumentationRunner androidx.test.runner.AndroidJUnitRunner

     

       

    }

     

     

       

    buildTypes {

     

           

    release {

     

               

    minifyEnabled false

     

               

    proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'

     

           

    }

     

       

    } }

     

     

    dependencies {

     

       

    implementation org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version

     

       

    implementation androidx.appcompat:appcompat:1.3.1

     

       

    implementation androidx.core:core-ktx:1.6.0

     

       

    implementation androidx.constraintlayout:constraintlayout:2.1.1

     

       

    implementation androidx.room:room-ktx:$room_version

     

       

    kapt androidx.room:room-compiler:$room_version

     

     

       

    androidTestImplementation androidx.test.ext:junit:1.1.3

     

       

    androidTestImplementation 'androidx.test:runner:1.4.0'

     

       

    androidTestImplementation androidx.arch.core:core-testing:2.1.0

     

       

    androidTestImplementation com.natpryce:hamkrest:1.7.0.0 }

    (from NoteBasics/build.gradle)

    Note that Room has a minSdkVersion requirement of API Level 15 or higher. If you attempt to build with a lower minSdkVersion, you will get a build error. If you try to override Room’s minSdkVersion using manifest merger elements, while the project will build, expect Room to crash horribly.

    Room Furnishings

    Roughly speaking, your use of Room is dominated by three sets of classes:

    Entities, which are simple classes that model the data you are transferring into and out of the database

    The data access object (DAO), that provides the description of the API that you want for working with certain entities

    The database, which ties together all of the entities and DAOs for a single SQLite database

    If you have used Square’s Retrofit, some of this will seem familiar:

    The DAO is roughly analogous to your Retrofit interface on which you declare your Web service API

    Your entities are the POJOs that you are expecting Gson/Moshi/whatever to create based on the Web service response

    The NoteBasics module mentioned above has a few classes related to a note-taking application, exercised via instrumented tests.

    Entities

    In many ORM systems, the entity (or that system’s equivalent) is a simple class that you happen to want to store in the database. It usually represents some part of your overall domain model, so a payroll system might have entities representing departments, employees, and paychecks.

    With Room, a better description of entities is that they are classes representing:

    the data that you want to store into a table, and

    a typical unit of a result set that you are trying to retrieve from the database

    That difference may sound academic. It starts to come into play a bit more when we start thinking about relations.

    However, it also more closely matches the way Retrofit maps to Web services. With Retrofit, we are not describing the contents of the Web service’s database. Rather, we are describing how we want to work with defined Web service endpoints. Those endpoints have a particular set of content that we can work with, courtesy of whoever developed the Web service. We are simply mapping those to methods and classes, both for input and output. Room is somewhere in between a Retrofit-style we just take what the Web service gives us approach and a full ORM-style we control everything about the database approach.

    From a coding standpoint, an entity is a Java/Kotlin class marked with the @Entity annotation. For example, here is a NoteEntity class that serves as a Room entity:

    package com.commonsware.room.notes

     

     

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

     

     

    @Entity(tableName = notes) data class NoteEntity(

     

     

    @PrimaryKey val id: String,

     

     

    val title: String,

     

     

    val text: String,

     

     

    val version: Int )

    (from NoteBasics/src/main/java/com/commonsware/room/notes/NoteEntity.kt)

    There is no particular superclass required for entities, and the expectation is that often they will be simple data classes, as we see here.

    The @Entity annotation can have properties customizing the behavior of your entity and how Room works with it. In this case, we have a tableName property. The default name of the SQLite table is the same as the entity class name, but tableName allows you to override that and supply your own table name. Here, we override the table name to be notes.

    Sometimes, your properties will be marked with annotations describing their roles. In this example, the id field has the @PrimaryKey annotation, telling Room that this is the unique identifier for this entity. Room will use that to know how to update and delete Note objects by their primary key values. In Java, Room also requires that any @PrimaryKey field of an object type — like String — be annotated with @NonNull, as primary keys in SQLite cannot be null. In Kotlin, you can just use a non-nullable type, such as String.

    We will explore entities in greater detail in an upcoming chapter.

    DAO

    Data access object (DAO) is a fancy way of saying the API into the data. The idea is that you have a DAO that provides methods for the database operations that you need: queries, inserts, updates, deletes, and so on.

    In Room, the DAO is identified by the @Dao annotation, applied to either an abstract class or an interface. The actual concrete implementation will be code-generated for you by the Room annotation processor.

    The primary role of the @Dao-annotated abstract class or interface is to have one or more methods, with their own Room annotations, identifying what you want to do with the database and your entities. This serves the same role as the functions annotated @GET or @POST in a Retrofit interface.

    The sample app has a NoteStore that is our DAO:

    package com.commonsware.room.notes

     

     

    import androidx.room.*

     

     

    @Dao interface NoteStore {

     

     

    @Query(SELECT * FROM notes)

     

     

    fun loadAll(): List<NoteEntity>

     

     

     

    @Insert

     

     

    fun insert(note: NoteEntity)

     

     

     

    @Update

     

     

    fun update(note: NoteEntity)

     

     

     

    @Delete

     

     

    fun delete(vararg notes: NoteEntity) }

    (from NoteBasics/src/main/java/com/commonsware/room/notes/NoteStore.kt)

    Besides the @Dao annotation on the NoteStore interface, we have four functions, each with their own annotations: @Query, @Insert, @Update, and @Delete, each which map to the corresponding database operations.

    The loadAll() function has the @Query annotation. Principally, @Query will be used for SQL SELECT statements, where you put the actual SQL in the annotation itself. Here, we are retrieving everything from the notes table.

    The remaining three functions use the @Insert, @Update, and @Delete annotations, mapped to functions of the same name. The actual function names do not matter — they could be larry(), curly(), and moe() and work just as well. As you might expect, @Insert inserts an entity into our table, @Update updates an existing table row to reflect the supplied entity’s properties, and @Delete deletes table rows corresponding with the supplied entities’ primary keys. In this sample, insert() and update() each take a single NoteEntity, while delete() takes a vararg of NoteEntity. Room supports either pattern, as well as others, such as a List of NoteEntity — choose what fits your needs.

    We will explore the DAO in greater detail in an upcoming chapter.

    Database

    In addition to entities and DAOs, you will have at least one @Database-annotated abstract class, extending a RoomDatabase base class. This class knits together the database file, the entities, and the DAOs.

    In the sample project, we have a NoteDatabase serving this role:

    package com.commonsware.room.notes

     

     

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

     

     

    @Database(entities = [NoteEntity::class], version = 1) abstract class NoteDatabase : RoomDatabase() {

     

     

    abstract fun notes(): NoteStore }

    (from NoteBasics/src/main/java/com/commonsware/room/notes/NoteDatabase.kt)

    The @Database annotation configures the code generation process, including:

    Identifying all of the entity classes that you care about in the entities collection

    Identifying the schema version of the database (as you see with SQLiteOpenHelper in conventional Android SQLite development)

    Here, we are saying that we have just one entity class (NoteEntity), and that this is schema version 1.

    You also need abstract functions for each DAO class that return an instance of that class. Here, we have a notes() function that returns NoteStore.

    Get a Room

    Our NoteDatabase is an abstract class. Somewhere, though, we need to get an instance of it, so we can call notes() and be able to start manipulating the database.

    To create a NoteDatabase, you need a RoomDatabase.Builder. There are two functions on the Room class for getting one:

    databaseBuilder(), and

    inMemoryDatabaseBuilder()

    databaseBuilder() will help you create a database backed by a traditional SQLite database file. inMemoryDatabaseBuilder() creates a SQLite database whose contents are only stored in memory — as soon as the database is closed, the memory holding the database contents gets freed.

    Both functions take a Context and the Java Class object of your RoomDatabase subclass as parameters. databaseBuilder() also takes the name of the database file to use.

    So, we could create a regular, file-backed NoteDatabase via:

    private val db =

     

     

    Room.databaseBuilder(context, NoteDatabase::class.java, notes.db).build()

    (where context is a suitable Context, such as the Application singleton)

    While there are some configuration methods that can be called on the RoomDatabase.Builder, we skip those here, simply calling build() to build the NoteDatabase, assigning it to the db property.

    From there, we can:

    Call notes() on the NoteDatabase to retrieve the NoteStore DAO

    Call methods on the NoteStore to query, insert, update, or delete NoteEntity objects

    Testing Room

    Once you have a RoomDatabase and its associated DAO(s) and entities set up, you should start testing it.

    The good news is that testing Room is not dramatically different than is testing anything else in Android. Room has a few characteristics that make it a bit easier than some things to test, as it turns out.

    You can learn more about testing in the Testing Your Changes chapter of Elements of Android Jetpack!

    Writing Instrumented Tests

    On the whole, writing instrumented tests for Room — where the tests run on an Android device or emulator — is unremarkable. You get an instance of your RoomDatabase subclass and exercise it from there.

    So, for example, here is an instrumented test case class to exercise the NoteDatabase:

    package com.commonsware.room.notes

     

     

    import androidx.room.Room import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import com.natpryce.hamkrest.hasSize import com.natpryce.hamkrest.isEmpty import org.junit.Test import org.junit.runner.RunWith import java.util.*

     

     

    @RunWith(AndroidJUnit4::class) class NoteStoreTest {

     

     

    private val db = Room.inMemoryDatabaseBuilder(

     

       

    InstrumentationRegistry.getInstrumentation().targetContext,

     

       

    NoteDatabase::class.java

     

     

    )

     

       

    .build()

     

     

    private val underTest = db.notes()

     

     

     

    @Test

     

     

    fun insertAndDelete() {

     

       

    assertThat(underTest.loadAll(), isEmpty)

     

     

       

    val entity = NoteEntity(

     

         

    id = UUID.randomUUID().toString(),

     

         

    title = This is a title,

     

         

    text = This is some text,

     

         

    version = 1

     

       

    )

     

     

       

    underTest.insert(entity)

     

     

       

    underTest.loadAll().let {

     

         

    assertThat(it, hasSize(equalTo(1)))

     

         

    assertThat(it[0], equalTo(entity))

     

       

    }

     

     

       

    underTest.delete(entity)

     

     

       

    assertThat(underTest.loadAll(), isEmpty)

     

     

    }

     

     

     

    @Test

     

     

    fun update() {

     

       

    val entity = NoteEntity(

     

         

    id = UUID.randomUUID().toString(),

     

         

    title = This is a title,

     

         

    text = This is some text,

     

         

    version = 1

     

       

    )

     

     

       

    underTest.insert(entity)

     

     

       

    val updated = entity.copy(title = This is new, text = So is this)

     

     

       

    underTest.update(updated)

     

     

       

    underTest.loadAll().let {

     

         

    assertThat(it, hasSize(equalTo(1)))

     

         

    assertThat(it[0], equalTo(updated))

     

       

    }

     

     

    } }

    (from NoteBasics/src/androidTest/java/com/commonsware/room/notes/NoteStoreTest.kt)

    Using In-Memory Databases

    When testing a database, though, one of the challenges is in making those tests hermetic, or self-contained. One test method should not depend upon another test method, and one test method should not affect the results of another test method accidentally. This means that we want to start with a known starting point before each test, and we have to consider how to do that.

    One approach — the one taken in the above NoteStoreTest class — is to use an in-memory database. The db property is initialized using Room.inMemoryDatabaseBuilder(), so we get our fast, disposable in-memory database. For a context, we use InstrumentationRegistry.getInstrumentation().targetContext, a Context for the code being tested. We then set up underTest to be the object that we are testing: the NoteStore and its functions.

    There are two key advantages for using an in-memory database for instrumented testing:

    It is intrinsically self-contained. Once the NoteDatabase is closed (or garbage-collected), its memory is released, and if separate tests use separate NoteDatabase instances, one will not affect the other.

    Reading and writing to and from memory is much faster than is reading and writing to and from disk, so the tests run much faster.

    On the other hand, this means that the instrumented tests are useless for performance testing, as (presumably) your production app will actually store its database on disk. You could use Gradle command-line switches, custom build types and buildConfigField, or other means to decide when tests are run whether they should use memory or disk.

    The Test Functions

    Our test functions do things like:

    Creating NoteEntity instances, using a UUID for the id

    Calling insert(), update(), and delete() to manipulate the table

    Enjoying the preview?
    Page 1 of 1