Your web browser is out of date. Update your browser for more security, speed and the best experience on this site.

Update your browser
CapTech Home Page

Blog October 8, 2018

Hands-On with Android JetPack's Room

At Google IO 2017, Room was introduced as a native abstraction layer over SQLite. The library helps create a cache of structured data for apps that can benefit from information stored locally. That way, the user can still browse through the app regardless of connection to the network. Room has matured since then with new features and is now part of the newly introduced Android Jetpack.

What are my choices as an Android developer when it comes to setting up persistence?

  • Third party ORMs and Databases:
    • Advantage
      • Ease of use
      • Minimal setup required
      • Little to no SQL required
    • Disadvantage
      • Support and continual development of third-party frameworks is unreliable
      • Compatibility problems across Android versions
      • Limited in capability
      • Difficult to test
      • Performance may be sluggish
      • Third-party databases like Realm provide powerful features, but rely on a proprietary database and locks the developer into their platform and custom models. There is a high learning curve to utilize the more advanced features of the non-traditional database
    • Examples
  • Using Android SQLite API
    • Advantage
      • No dependencies aside from Android's SQLite package
      • Full control of the database
      • Debuggable by pulling the database file into a SQLite browsers
      • ContentProvider to share data between applications
    • Disadvantage
      • No compile-time checks for SQL queries
      • Manual schema migrations and updates to the data graph are time-consuming and error-prone
      • SQL is another language
      • A lot of boilerplate code to convert between SQL queries and data objects


Why does Room matter?

Room reduces the amount of boilerplate code and simplifies complex tasks so developers can focus on the core pieces of their code.

Key Features

  • Less boilerplate code
  • Compile-time checked queries
  • Easier database migrations with automatic validation
  • Support for:
    • RxJava
    • Java Optionals
    • Google Guava
  • Keeps database operations in background threads
  • Easier test-ability using Android J Unit and Espresso
  • Better compatibility and reliability as it is not a third-party framework
  • Integration with JetPack

In this hands-on example, we will build an inventory of vehicles for sale. The TransportDatabase app is in Kotlin using the MVVM software architectural pattern consisting of two views shown below - a list of vehicles for sale and a detail view.

Implementing the Persistence Layer with Room

There are three major parts that make up Room

  • Entity
  • Data Access Object (DAO)
  • Database

Entity

An entity is equivalent to a table within the SQLite database. For each entity, Room creates a table and each property defined in the entity is a column.

To create an Entity the following requirements must be satisfied

  • Annotated with @Entity
  • At least one property assigned as the primary key using @<span class="RadEWrongWord" id="RadESpellError_30">PrimaryKey</span> annotation

<span>@Entity</span><span>(</span><span><span class="RadEWrongWord" id="RadESpellError_31">tableName</span> = </span><strong><span>"cars"</span></strong><span>)
</span><strong><span>data class </span></strong><span>Car(
</span><span>@<span class="RadEWrongWord" id="RadESpellError_32">PrimaryKey</span> @<span class="RadEWrongWord" id="RadESpellError_33">ColumnInfo</span></span><span>(</span><span>name = </span><strong><span>"<span class="RadEWrongWord" id="RadESpellError_34">vin</span>"</span></strong><span>) </span><strong><span>val </span></strong><strong><span class="RadEWrongWord" id="RadESpellError_35">vin</span></strong><span>: String,
</span><strong><span>override val </span></strong><strong><span>manufacturer</span></strong><span>: String,
</span><strong><span>override val </span></strong><strong><span>model</span></strong><span>: String,
</span><strong><span>override val </span></strong><strong><span class="RadEWrongWord" id="RadESpellError_36">modelYear</span></strong><span>: Int,
</span><strong><span>val </span></strong><strong><span>color</span></strong><span>: String,
</span><strong><span>val </span></strong><strong><span>image</span></strong><span>: String,
</span><strong><span>val </span></strong><strong><span>price</span></strong><span>: String,
</span><strong><span>val </span></strong><strong><span>trim</span></strong><span>: String,
</span><strong><span>val </span></strong><strong><span>description</span></strong><span>: String,
</span><strong><span class="RadEWrongWord" id="RadESpellError_37">var </span></strong><strong><span>favorite</span></strong><span>: Boolean = </span><strong><span>false
</span></strong><span>) : Vehicle() {

</span><strong><span>override fun </span></strong><span><span class="RadEWrongWord" id="RadESpellError_38">toString</span>() = String.<em>format</em>(</span><strong><span>"%s %s %s %s"</span></strong><span>, </span><strong><span class="RadEWrongWord" id="RadESpellError_39">modelYear</span></strong><span>, </span><strong><span>manufacturer</span></strong><span>, </span><strong><span>model</span></strong><span>, </span><strong><span>trim</span></strong><span>)
 }</span>

When classes are created for the main purpose of holding data, it is good practice to mark them as a data class. By marking the Car entity as a data class, the compiler will derive functions such as equals(), copy(), and <span class="RadEWrongWord" id="RadESpellError_40">toString</span>() from the properties declared in the constructor.

Note: In the example above, the Car entity implements the Vehicle abstract class. While this step is not required, it will help with readability by grouping common properties if other vehicles such as planes or boats are listed in the future.

Data Access Object (DAO)

A DAO interface contains methods that offer abstract access to the application database. Using a DAO to access the database instead of queries offers better separation of duties. Room generates the implementation of the DAO at compile time.

To create a DAO the following steps are necessary.

  1. An interface or an abstract class
  2. Annotate the class with @<span class="RadEWrongWord" id="RadESpellError_46">Dao</span>
  3. Define methods for convenience using annotations
    1. @Insert - Inserts parameters in a single transaction
    2. @Update - Modifies entities based on a primary key
    3. @Delete - Deletes entities based on a primary key
    4. @Query - The primary annotation that allows read/write operations on a database

<span>/**
</span><span> * The Data Access Object for the Car class.
</span><span> */
</span><span>@<span class="RadEWrongWord" id="RadESpellError_48">Dao</span>
</span><span>interface </span><span class="RadEWrongWord" id="RadESpellError_49">CarDao</span> {

<span>@Query</span>(<span>"SELECT * FROM cars ORDER BY manufacturer, model"</span>)
<span>fun </span><span class="RadEWrongWord" id="RadESpellError_50">getCars</span>(): <span class="RadEWrongWord" id="RadESpellError_51">LiveData</span>>

<span>@Query</span>(<span>"SELECT * FROM cars WHERE <span class="RadEWrongWord" id="RadESpellError_52">vin</span> = :<span class="RadEWrongWord" id="RadESpellError_53">vin</span>"</span>)
<span>fun </span><span class="RadEWrongWord" id="RadESpellError_54">getCarByVin</span>(<span class="RadEWrongWord" id="RadESpellError_55">vin</span>: String): <span class="RadEWrongWord" id="RadESpellError_56">LiveData</span>

<span>@Query</span>(<span>"SELECT * FROM cars WHERE favorite = 1 ORDER BY manufacturer, model"</span>)
<span>fun </span><span class="RadEWrongWord" id="RadESpellError_57">getCarsByFavorite</span>(): <span class="RadEWrongWord" id="RadESpellError_58">LiveData</span>>

<span>@Query</span>(<span>"UPDATE cars SET favorite = :<span class="RadEWrongWord" id="RadESpellError_59">boolInt</span> WHERE <span class="RadEWrongWord" id="RadESpellError_60">vin</span> = :<span class="RadEWrongWord" id="RadESpellError_61">vin</span>"</span>)
<span>fun </span><span class="RadEWrongWord" id="RadESpellError_62">updateCarFavoriteFlag</span>(<span class="RadEWrongWord" id="RadESpellError_63">vin</span>: String, <span class="RadEWrongWord" id="RadESpellError_64">boolInt</span> : Int)

<span>@Insert</span>(<span><span class="RadEWrongWord" id="RadESpellError_65">onConflict</span> = </span><span class="RadEWrongWord" id="RadESpellError_66">OnConflictStrategy</span>.<span>REPLACE</span>)
<span>fun </span><span class="RadEWrongWord" id="RadESpellError_67">insertAll</span>(cars: List)

<span>@Query</span>(<span>"SELECT * FROM cars WHERE manufacturer = :manufacturer ORDER BY model"</span>)
<span>fun </span><span class="RadEWrongWord" id="RadESpellError_68">getCarsByManufacturer</span>(manufacturer: String): <span class="RadEWrongWord" id="RadESpellError_69">LiveData</span>>

<span>@Query</span>(<span>"SELECT * FROM cars WHERE model = :model ORDER BY color"</span>)
<span>fun </span><span class="RadEWrongWord" id="RadESpellError_70">getCarsByModel</span>(model: String): <span class="RadEWrongWord" id="RadESpellError_71">LiveData</span>>

<span>@Query</span>(<span>"SELECT * FROM cars WHERE <span class="RadEWrongWord" id="RadESpellError_72">modelYear</span> = :<span class="RadEWrongWord" id="RadESpellError_73">modelYear</span> ORDER BY manufacturer, model"</span>)
<span>fun </span><span class="RadEWrongWord" id="RadESpellError_74">getCarsByModelYear</span>(<span class="RadEWrongWord" id="RadESpellError_75">modelYear</span>: Int): <span class="RadEWrongWord" id="RadESpellError_76">LiveData</span>>
}

The queries in the DAO that return <span class="RadEWrongWord" id="RadESpellError_78">LiveData</span> types which allow the app to update the UI when the data changes. Room updates the LiveData when the database is changed and also verifies the SQL during compile time.

Database

The database is the main entry point to the underlying connection to the app's SQLite database.

To create a Database a few requirements must be satisfied

  • The class
    • Annotated with @Database
    • Extends <span class="RadEWrongWord" id="RadESpellError_85">RoomDatabase</span>
    • Abstract
  • Includes a list of entities in the annotation that will be stored in the database
  • For each DAO used for the database, an abstract method with no arguments that returns the DAO

<span>/**
</span><span> * The Room database for this app
</span><span> */
</span><span>@Database</span>(<span>entities = </span>[Car::<span>class</span>], <span>version = </span><span>1</span>, <span><span class="RadEWrongWord" id="RadESpellError_89">exportSchema</span> = </span><span>false</span>)
<span>abstract class </span><span class="RadEWrongWord" id="RadESpellError_90">TransportDatabase</span> : <span class="RadEWrongWord" id="RadESpellError_91">RoomDatabase</span>() {
<span> abstract fun </span><span class="RadEWrongWord" id="RadESpellError_92">carDao</span>(): <span class="RadEWrongWord" id="RadESpellError_93">CarDao</span>
}

The <span class="RadEWrongWord" id="RadESpellError_94">TransportDatabase</span> is storing an entity, Car. The entity is accessed through a DAO, <span class="RadEWrongWord" id="RadESpellError_96">CarDao</span>.

Initializing and Pre-populating the Database

With the complete set of Room components, the database needs to be initialized and populated with car information. A "companion object" is added inside <span class="RadEWrongWord" id="RadESpellError_97">TransportDatabase</span> to create a singleton.

A Room database is expensive to create and using a singleton will minimize costs by not having to reinitialize Room every time the app needs access to the persistence layer.

<span>companion object </span>{

<span>//Singleton Instantiation
</span><span> </span><span>@Volatile
</span><span> </span><span>private <span class="RadEWrongWord" id="RadESpellError_100">var</span> </span><span>instance</span>: <span class="RadEWrongWord" id="RadESpellError_101">TransportDatabase</span>? = <span>null
</span><span>
</span><span> fun </span><span class="RadEWrongWord" id="RadESpellError_102">getInstance</span>(context: Context): <span class="RadEWrongWord" id="RadESpellError_103">TransportDatabase</span> {
<span>return </span><span>instance </span>?: <span>synchronized</span>(<span>this</span>) <span>{
</span><span> </span><span>instance </span>?: <span class="RadEWrongWord" id="RadESpellError_104">buildDatabase</span>(context).<span>also </span><span>{ </span><span>instance </span>= <span>it }
</span><span> }
</span><span> </span>}

<span>// Create and pre-populate the database.
</span><span> </span><span>private fun </span><span class="RadEWrongWord" id="RadESpellError_105">buildDatabase</span>(context: Context): <span class="RadEWrongWord" id="RadESpellError_106">TransportDatabase</span> {
<span>return </span>Room.<span class="RadEWrongWord" id="RadESpellError_107">databaseBuilder</span>(context, <span class="RadEWrongWord" id="RadESpellError_108">TransportDatabase</span>::<span>class</span>.<span>java</span>, <span>DATABASE_NAME</span>)
<span>//In this example, migration is not provided. So database will be cleared on upgrade.
</span><span> </span>.<span class="RadEWrongWord" id="RadESpellError_109">fallbackToDestructiveMigration</span>()
 .<span class="RadEWrongWord" id="RadESpellError_110">addCallback</span>(<span>object </span>: <span class="RadEWrongWord" id="RadESpellError_111">RoomDatabase</span>.Callback() {
<span>override fun </span><span class="RadEWrongWord" id="RadESpellError_112">onCreate</span>(db: <span class="RadEWrongWord" id="RadESpellError_113">SupportSQLiteDatabase</span>) {
<span>super</span>.<span class="RadEWrongWord" id="RadESpellError_114">onCreate</span>(db)
<span>val </span>request = <span class="RadEWrongWord" id="RadESpellError_115">OneTimeWorkRequestBuilder</span><<span class="RadEWrongWord" id="RadESpellError_116">PopulateDatabaseWorker</span>>().build()
<span class="RadEWrongWord" id="RadESpellError_117">WorkManager</span>.<span class="RadEWrongWord" id="RadESpellError_118">getInstance</span>().enqueue(request)
 }
 })
 .build()
 }
}

When the <span class="RadEWrongWord" id="RadESpellError_119">TransportDatabase</span> is first initialized and calls <span class="RadEWrongWord" id="RadESpellError_120">buildDatabase</span>(...), a one-time Worker, <span class="RadEWrongWord" id="RadESpellError_121">PopulateDatabaseWorker</span>, is created and passed to the <span class="RadEWrongWord" id="RadESpellError_122">WorkManager</span> for execution.

<strong><span>class </span></strong><span><span class="RadEWrongWord" id="RadESpellError_123">PopulateDatabaseWorker</span> : Worker() {
</span><strong><span>private val </span></strong><strong><span>TAG </span></strong><span>= <span class="RadEWrongWord" id="RadESpellError_124">PopulateDatabaseWorker</span>::</span><strong><span>class</span></strong><span>.</span><em><span>java</span></em><span>.</span><em><span><span class="RadEWrongWord" id="RadESpellError_125">simpleName</span>

</span></em><strong><span>override fun </span></strong><span><span class="RadEWrongWord" id="RadESpellError_126">doWork</span>(): Worker.Result {
</span><strong><span>val </span></strong><span><span class="RadEWrongWord" id="RadESpellError_127">carType</span> = </span><strong><span>object </span></strong><span>: <span class="RadEWrongWord" id="RadESpellError_128">TypeToken</span>>() {}.</span><em><span>type
</span></em><strong><span class="RadEWrongWord" id="RadESpellError_129">var </span></strong><span><span class="RadEWrongWord" id="RadESpellError_130">jsonReader</span>: <span class="RadEWrongWord" id="RadESpellError_131">JsonReader</span>? = </span><strong><span>null

 return try </span></strong><span>{
</span><em><span>//Read in <span class="RadEWrongWord" id="RadESpellError_132">JSON</span> of Cars
</span></em><strong><span>val </span></strong><span><span class="RadEWrongWord" id="RadESpellError_133">inputStream</span> = </span><em><span class="RadEWrongWord" id="RadESpellError_134">applicationContext</span></em><span>.</span><em><span>assets</span></em><span>.open(</span><em><span>CAR_DATA_FILENAME</span></em><span>)
<span class="RadEWrongWord" id="RadESpellError_135">jsonReader</span> = <span class="RadEWrongWord" id="RadESpellError_136">JsonReader</span>(<span class="RadEWrongWord" id="RadESpellError_137">inputStream</span>.<em>reader</em>())
</span><em><span>//Parse <span class="RadEWrongWord" id="RadESpellError_138">JSON</span> to Car objects
</span></em><strong><span>val </span></strong><span>cars: List = <span class="RadEWrongWord" id="RadESpellError_139">Gson</span>().<span class="RadEWrongWord" id="RadESpellError_140">fromJson</span>(<span class="RadEWrongWord" id="RadESpellError_141">jsonReader</span>, <span class="RadEWrongWord" id="RadESpellError_142">carType</span>)
</span><em><span>//Get an instance of the database for insertion
</span></em><strong><span>val </span></strong><span>database = <span class="RadEWrongWord" id="RadESpellError_143">TransportDatabase</span>.<span class="RadEWrongWord" id="RadESpellError_144">getInstance</span>(</span><em><span class="RadEWrongWord" id="RadESpellError_145">applicationContext</span></em><span>)
</span><em><span>//Insert all car objects into the Room database
</span></em><span>database.<span class="RadEWrongWord" id="RadESpellError_146">carDao</span>().<span class="RadEWrongWord" id="RadESpellError_147">insertAll</span>(cars)
 Worker.Result.</span><strong><span>SUCCESS
</span></strong><span>} </span><strong><span>catch </span></strong><span>(ex: Exception) {
 Log.e(</span><strong><span>TAG</span></strong><span>, </span><strong><span>"Error <span class="RadEWrongWord" id="RadESpellError_148">prepopulating</span> database with cars"</span></strong><span>, ex)
 Worker.Result.</span><strong><span>FAILURE
</span></strong><span>} </span><strong><span>finally </span></strong><span>{
<span class="RadEWrongWord" id="RadESpellError_149">jsonReader</span>?.close()
 }

 }
 }</span>

When the worker executes, the app reads in the car.<span class="RadEWrongWord" id="RadESpellError_151">json</span> file located in the Assets directory that is then parsed into a List. The list of cars is inserted into the database using the insertion function in the Dao.

Separation of Concerns

With Room complete and seeded, we can isolate the persistence layer and decouple the application from the data sources by implementing the Repository Pattern. Instead of having direct access to the database, the repository becomes a centralized place to access data and makes the application more testable and less vulnerable to bugs.

<span>/**
 * Repository for handling data operations.
 */
</span><strong><span>class </span></strong><span class="RadEWrongWord" id="RadESpellError_153">CarRepository </span><strong><span>private constructor</span></strong><span>(</span><strong><span>private val </span></strong><strong><span class="RadEWrongWord" id="RadESpellError_154">carDao</span></strong><span>: <span class="RadEWrongWord" id="RadESpellError_155">CarDao</span>) {

</span><strong><span>fun </span></strong><span><span class="RadEWrongWord" id="RadESpellError_156">getCars</span>() = </span><strong><span class="RadEWrongWord" id="RadESpellError_157">carDao</span></strong><span>.<span class="RadEWrongWord" id="RadESpellError_158">getCars</span>()

</span><strong><span>fun </span></strong><span><span class="RadEWrongWord" id="RadESpellError_159">getCarByVin</span>(<span class="RadEWrongWord" id="RadESpellError_160">vin</span>: String) = </span><strong><span class="RadEWrongWord" id="RadESpellError_161">carDao</span></strong><span>.<span class="RadEWrongWord" id="RadESpellError_162">getCarByVin</span>(<span class="RadEWrongWord" id="RadESpellError_163">vin</span>)

</span><strong><span>fun </span></strong><span><span class="RadEWrongWord" id="RadESpellError_164">getFavoriteCars</span>() = </span><strong><span class="RadEWrongWord" id="RadESpellError_165">carDao</span></strong><span>.<span class="RadEWrongWord" id="RadESpellError_166">getCarsByFavorite</span>()

</span><strong><span>fun </span></strong><span><span class="RadEWrongWord" id="RadESpellError_167">updateCarFavoriteFlag</span>(<span class="RadEWrongWord" id="RadESpellError_168">vin</span>: String, favorite: Boolean) {
<em><span class="RadEWrongWord" id="RadESpellError_169">runOnIoThread</span> </em><strong>{
<span> </span></strong></span><strong><span class="RadEWrongWord" id="RadESpellError_170">carDao</span></strong><span>.<span class="RadEWrongWord" id="RadESpellError_171">updateCarFavoriteFlag</span>(<span class="RadEWrongWord" id="RadESpellError_172">vin</span>, </span><strong><span>if </span></strong><span>(favorite) </span><span>0 </span><strong><span>else </span></strong><span>1 </span><span>)
<strong>}
<span> </span></strong>}

</span><em><span>//Extra unused examples of using the <span class="RadEWrongWord" id="RadESpellError_173">DAO</span>.
</span></em><strong><span>fun </span></strong><span><span class="RadEWrongWord" id="RadESpellError_174">getCarsByModelYear</span>(year: Int) = </span><strong><span class="RadEWrongWord" id="RadESpellError_175">carDao</span></strong><span>.<span class="RadEWrongWord" id="RadESpellError_176">getCarsByModelYear</span>(year)

</span><strong><span>fun </span></strong><span><span class="RadEWrongWord" id="RadESpellError_177">getCarsByManufacturer</span>(manufacturer: String) = </span><strong><span class="RadEWrongWord" id="RadESpellError_178">carDao</span></strong><span>.<span class="RadEWrongWord" id="RadESpellError_179">getCarsByManufacturer</span>(manufacturer)

</span><strong><span>fun </span></strong><span><span class="RadEWrongWord" id="RadESpellError_180">getCarsByModel</span>(model: String) = </span><strong><span class="RadEWrongWord" id="RadESpellError_181">carDao</span></strong><span>.<span class="RadEWrongWord" id="RadESpellError_182">getCarsByModel</span>(model)

</span><strong><span>companion object </span></strong><span>{

</span><em><span>// For Singleton instantiation
</span></em><span>@Volatile
<strong>private <span class="RadEWrongWord" id="RadESpellError_183">var</span> </strong></span><strong><span>instance</span></strong><span>: <span class="RadEWrongWord" id="RadESpellError_184">CarRepository</span>? = </span><strong><span>null

 fun </span></strong><span><span class="RadEWrongWord" id="RadESpellError_185">getInstance</span>(<span class="RadEWrongWord" id="RadESpellError_186">carDao</span>: <span class="RadEWrongWord" id="RadESpellError_187">CarDao</span>) =
</span><strong><span>instance </span></strong><span>?: <em>synchronized</em>(</span><strong><span>this</span></strong><span>) <strong>{
<span> </span></strong></span><strong><span>instance </span></strong><span>?: <span class="RadEWrongWord" id="RadESpellError_188">CarRepository</span>(<span class="RadEWrongWord" id="RadESpellError_189">carDao</span>).<em>also </em><strong>{ </strong></span><strong><span>instance </span></strong><span>= <strong>it }
<span> </span>}
<span> </span></strong>}
 }</span>

Structure of the persistence layer

View Models

With the persistence layer isolated, we can setup ViewModels to pull data from the repository to be displayed on the UI. The benefits of ViewModels is its ability to hold UI data while surviving configuration changes. (e.g. screen rotation) It reinforces the Single Responsibility Principle, so the Activities/Fragments are only responsible for drawing the user interface and receiving user interactions.

Since this app has two views, there will also be two ViewModels.

ViewModels:

  • CarListViewModel - A list of all cars for purchase
  • CarDetailViewModel - A detail view when the user taps on a car

For each view, a ViewModel holds data for UI components and data binding allows the UI components to be bound to ViewModels using the layout XMLs.

The car list view shows all the cars for sale by default, but by tapping the star in the top right, only cars marked as favorites will be shown. In order to switch between the two lists, two values are necessary.

<span>private val </span><span class="RadEWrongWord" id="RadESpellError_206">shouldShowFavorites </span>= <span class="RadEWrongWord" id="RadESpellError_207">MutableLiveData</span>()
<span>private val </span><span>carList </span>= <span class="RadEWrongWord" id="RadESpellError_208">MediatorLiveData</span>>()

  1. <span class="RadEWrongWord" id="RadESpellError_209">shouldShowFavorites</span>
    1. Type: <span class="RadEWrongWord" id="RadESpellError_210">MutableLiveData</span>
    2. <span class="RadEWrongWord" id="RadESpellError_211">MutableLiveData</span> is a subclass of <span class="RadEWrongWord" id="RadESpellError_212">LiveData</span> which notifies the UI when the value has changed.
  2. carList
    1. Type: <span class="RadEWrongWord" id="RadESpellError_214">MediatorLiveData</span>>
    2. <span class="RadEWrongWord" id="RadESpellError_215">MediatorLiveData</span> is a subclass of <span class="RadEWrongWord" id="RadESpellError_216">LiveData</span> which may observe other <span class="RadEWrongWord" id="RadESpellError_217">LiveData</span> objects and react to <span class="RadEWrongWord" id="RadESpellError_218">OnChange</span> events.

We want the carList to react to when <span class="RadEWrongWord" id="RadESpellError_219">shouldShowFavorites</span> is changed. To do this we need to create a trigger using Transformations.<span class="RadEWrongWord" id="RadESpellError_220">switchMap</span> and add it the carList as a source on initialization of the <span class="RadEWrongWord" id="RadESpellError_221">ViewModel</span>.

<span class="RadEWrongWord" id="RadESpellError_222">init </span>{

<span class="RadEWrongWord" id="RadESpellError_223">shouldShowFavorites</span>.<span>value </span>= <span>false
</span><span>
</span><span> val </span><span class="RadEWrongWord" id="RadESpellError_224">liveFavCarList</span> = Transformations.<span class="RadEWrongWord" id="RadESpellError_225">switchMap</span>(<span class="RadEWrongWord" id="RadESpellError_226">shouldShowFavorites</span>) <span>{
</span><span> </span><span>if </span>(<span>it</span>) {
<span class="RadEWrongWord" id="RadESpellError_227">carRepository</span>.<span class="RadEWrongWord" id="RadESpellError_228">getFavoriteCars</span>()
 } <span>else </span>{
<span class="RadEWrongWord" id="RadESpellError_229">carRepository</span>.<span class="RadEWrongWord" id="RadESpellError_230">getCars</span>()
 }
<span>}
</span><span>
</span><span> </span><span>carList</span>.<span class="RadEWrongWord" id="RadESpellError_231">addSource</span>(<span class="RadEWrongWord" id="RadESpellError_232">liveFavCarList</span>, <span>carList</span>::<span class="RadEWrongWord" id="RadESpellError_233">setValue</span>)
}

Anytime <span class="RadEWrongWord" id="RadESpellError_235">shouldShowFavorites</span> is changed, the carList is set to the return value of <span class="RadEWrongWord" id="RadESpellError_236">getFavoriteCars</span>() or <span class="RadEWrongWord" id="RadESpellError_237">getCars</span>(). The <span class="RadEWrongWord" id="RadESpellError_238">shouldShowFavorites</span> is set to false by default to trigger the update on initialization. To enable the UI to retrieve data from the <span class="RadEWrongWord" id="RadESpellError_240">ViewModel</span> when the user interacts with the view, the following getter functions are necessary.

<span>fun </span><span class="RadEWrongWord" id="RadESpellError_241">toggleFavorite</span>() {
<span class="RadEWrongWord" id="RadESpellError_242">shouldShowFavorites</span>.<span>value </span>= !<span class="RadEWrongWord" id="RadESpellError_243">shouldShowFavorites</span>()
}

<span>fun </span><span class="RadEWrongWord" id="RadESpellError_244">shouldShowFavorites</span>() = <span class="RadEWrongWord" id="RadESpellError_245">shouldShowFavorites</span>.<span>value </span>== <span>true
</span><span>
</span><span>fun </span><span class="RadEWrongWord" id="RadESpellError_246">getCars</span>() = <span>carList</span>

Adding everything together, we get the complete ViewModel below.

CarListViewModel

<span>/**
</span><span> * The <span class="RadEWrongWord" id="RadESpellError_249">ViewModel</span> for </span><span>[<span class="RadEWrongWord" id="RadESpellError_250">CarListFragment</span>]</span><span>.
</span><span> */
</span><span>class </span><span class="RadEWrongWord" id="RadESpellError_251">CarListViewModel</span> <span>internal constructor</span>(
<span>private val </span><span class="RadEWrongWord" id="RadESpellError_252">carRepository</span>: <span class="RadEWrongWord" id="RadESpellError_253">CarRepository</span>
) : <span class="RadEWrongWord" id="RadESpellError_254">ViewModel</span>() {

<span>private val </span><span class="RadEWrongWord" id="RadESpellError_255">shouldShowFavorites </span>= <span class="RadEWrongWord" id="RadESpellError_256">MutableLiveData</span>()
<span>private val </span><span>carList </span>= <span class="RadEWrongWord" id="RadESpellError_257">MediatorLiveData</span>>()

<span class="RadEWrongWord" id="RadESpellError_258">init </span>{

<span class="RadEWrongWord" id="RadESpellError_259">shouldShowFavorites</span>.<span>value </span>= <span>false
</span><span>
</span><span> val </span><span class="RadEWrongWord" id="RadESpellError_260">liveFavCarList</span> = Transformations.<span class="RadEWrongWord" id="RadESpellError_261">switchMap</span>(<span class="RadEWrongWord" id="RadESpellError_262">shouldShowFavorites</span>) <span>{
</span><span> </span><span>if </span>(<span>it</span>) {
<span class="RadEWrongWord" id="RadESpellError_263">carRepository</span>.<span class="RadEWrongWord" id="RadESpellError_264">getFavoriteCars</span>()
 } <span>else </span>{
<span class="RadEWrongWord" id="RadESpellError_265">carRepository</span>.<span class="RadEWrongWord" id="RadESpellError_266">getCars</span>()
 }

<span>}
</span><span>
</span><span> </span><span>carList</span>.<span class="RadEWrongWord" id="RadESpellError_267">addSource</span>(<span class="RadEWrongWord" id="RadESpellError_268">liveFavCarList</span>, <span>carList</span>::<span class="RadEWrongWord" id="RadESpellError_269">setValue</span>)
 }

<span>fun </span><span class="RadEWrongWord" id="RadESpellError_270">toggleFavorite</span>() {
<span class="RadEWrongWord" id="RadESpellError_271">shouldShowFavorites</span>.<span>value </span>= !<span class="RadEWrongWord" id="RadESpellError_272">shouldShowFavorites</span>()
 }

<span>fun </span><span class="RadEWrongWord" id="RadESpellError_273">shouldShowFavorites</span>() = <span class="RadEWrongWord" id="RadESpellError_274">shouldShowFavorites</span>.<span>value </span>== <span>true
</span><span>
</span><span> fun </span><span class="RadEWrongWord" id="RadESpellError_275">getCars</span>() = <span>carList
</span><span>
</span>}

CarDetailViewModel

The ViewModel for the car detail will be similar, but simpler.

<span>/**
</span><span> * The <span class="RadEWrongWord" id="RadESpellError_278">ViewModel</span> used in </span><span>[<span class="RadEWrongWord" id="RadESpellError_279">CarDetailFragment</span>]</span><span>.
</span><span> */
</span><span>class </span><span class="RadEWrongWord" id="RadESpellError_280">CarDetailViewModel</span>(
<span>private val </span><span class="RadEWrongWord" id="RadESpellError_281">carRepository</span>: <span class="RadEWrongWord" id="RadESpellError_282">CarRepository</span>,
<span>private val </span><span class="RadEWrongWord" id="RadESpellError_283">vin</span>: String
) : <span class="RadEWrongWord" id="RadESpellError_284">ViewModel</span>() {

<span>val </span><span>car</span>: <span class="RadEWrongWord" id="RadESpellError_285">LiveData</span>

<span class="RadEWrongWord" id="RadESpellError_286">init </span>{
<span>car </span>= <span class="RadEWrongWord" id="RadESpellError_287">carRepository</span>.<span class="RadEWrongWord" id="RadESpellError_288">getCarByVin</span>(<span class="RadEWrongWord" id="RadESpellError_289">vin</span>)
 }

<span>fun </span><span class="RadEWrongWord" id="RadESpellError_290">updateCarFavoriteFlag</span>() {
<span>val </span>car = <span>car</span>.<span>value
</span><span> </span><span>if </span>(car != <span>null</span>) {
<span class="RadEWrongWord" id="RadESpellError_291">carRepository</span>.<span class="RadEWrongWord" id="RadESpellError_292">updateCarFavoriteFlag</span>(<span class="RadEWrongWord" id="RadESpellError_293">vin</span>, <span>car</span>.<span>favorite</span>)
 }
 }

<span>fun </span><span class="RadEWrongWord" id="RadESpellError_294">isCarFavorited</span>(): Boolean {
<span>val </span>car = <span>car</span>.<span>value
</span><span> </span><span>if </span>(car != <span>null</span>) {
<span>return </span><span>car</span>.<span>favorite
</span><span> </span>}
<span>return false
</span><span> </span>}
}

View - Fragments

The ViewModels are then associated with its view.

In this case they are connected to Fragments:

  • <span class="RadEWrongWord" id="RadESpellError_296">CarListViewModel</span> -> <span class="RadEWrongWord" id="RadESpellError_297">CarListFragment</span>
  • <span class="RadEWrongWord" id="RadESpellError_298">CarDetailViewModel</span> -> <span class="RadEWrongWord" id="RadESpellError_299">CarDetailFragment</span>

To use a <span class="RadEWrongWord" id="RadESpellError_300">ViewModel</span> in a View, a single line is needed in the <span class="RadEWrongWord" id="RadESpellError_301">onCreate</span> of the Fragment:

<span>val </span><span class="RadEWrongWord" id="RadESpellError_302">carListViewModel</span> = <span class="RadEWrongWord" id="RadESpellError_303">ViewModelProviders</span>.of(<span>this</span>, factory).get(<span class="RadEWrongWord" id="RadESpellError_304">CarListViewModel</span>::<span>class</span>.<span>java</span>) 

When creating the <span class="RadEWrongWord" id="RadESpellError_305">CarListViewModel</span>, an instance of the fragment is passed into <span class="RadEWrongWord" id="RadESpellError_306">ViewModelProviders</span>. This allows the <span class="RadEWrongWord" id="RadESpellError_307">ViewModel</span> to be lifecycle aware and survive configuration changes.

Since we want to use data binding as well, a few extra lines of code are necessary in the fragment and adapter:

CarListFragment

<span>val </span>binding = <span class="RadEWrongWord" id="RadESpellError_310">DataBindingUtil</span>.inflate<<span class="RadEWrongWord" id="RadESpellError_311">FragmentCarListBinding</span>>(
 inflater, R.layout.<span>fragment_car_list</span>, container, <span>false</span>).<span>apply </span><span>{
</span><span> </span><span class="RadEWrongWord" id="RadESpellError_312">viewModel </span>= <span class="RadEWrongWord" id="RadESpellError_313">carListViewModel</span>
<span class="RadEWrongWord" id="RadESpellError_314">setLifecycleOwner</span>(<span>this</span><span>@<span class="RadEWrongWord" id="RadESpellError_315">CarListFragment</span></span>)
<span>}</span>

The UI is now largely handled through Data Binding between the <span class="RadEWrongWord" id="RadESpellError_317">ViewModels</span> and the layout XMLs.

Conclusion

The complete pathway is established starting with passing data from the SQLite database to how it displays and responds to the user-interface. Room is nearly as simple as third-party ORMs to setup but allows us to fully utilize SQLite to create a robust database. The use of Room in concert with MVVM reduces the amount of boilerplate code and provides better separation of duties. This enables our applications to perform faster while minimizing errors.

Hands-on with Android Jetpack

For more information, check out the project on GitHub here.