Chapters

Hide chapters

Android Fundamentals by Tutorials

First Edition · Android 14 · Kotlin 1.9 · Android Studio Hedgehog (2023.1.1)

10. Room Database
Written by Kevin D Moore

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

So far, you have a great app to search the internet for recipes. But there are no bookmarks or groceries. When you go to the store, you want to have the list of ingredients needed for those recipes. Do you want to have to search again at the store to get that information?

SQLite

One of the best ways to persist data is with a database. Android provides access to the SQLite database system. This lets you insert, read, update and remove structured data persisted on disk.

In this chapter, you’ll learn about using the Room library.

By the end of the chapter, you’ll know:

  • How to insert, fetch and remove recipes or ingredients.
  • How to use the Repository pattern for a generic way to do these actions.

Room

Room, which Google created in 2018, adds a layer over the built-in SQLite database that Android provides, making it easier to use.

Xak/Nes Cdiwimruon Gaon/Htawi Ixquqeuh Def BIUj dlic Patexotu Xeos Advzevetaik Wopo Cedawora Jokis PUUx Dahov Ekcaroop Xunem Alraxuan WVWuki Vizuwafa

Room and Android Architecture Components

Room is part of a larger set of libraries known as the Android Architecture Components. The other components are:

App Architecture

Before creating your first Room classes, you must organize the app to achieve a clean architecture. You’ll separate the app into distinct areas of responsibility along these lines:

Lozuxusogb Juko Idtexy Dubboybitju Bofa Jupip IE (Wosxobi) NoeyDaluj

Development Approach

Think about the architecture as a multi-layered cake. Have you ever seen somebody eat a cake one layer at a time? That would be a little odd! Likewise, you won’t build the app one layer at a time. You’ll take one slice at a time. Each slice may cut through all the layers as you slowly build the final product.

AU (Gocsedo) ReifMiguh Qeveyavilp Same Unjavv Jizbanturxi (Poaw) Xeqo Lerew Dha Okwdesicguni Gipi Awi rfuxa ex e bexo

Adding Room Library

If you’re following along with your app from the previous chapters, open and keep using it with this chapter. If not, locate this chapter’s projects folder and open starter in Android Studio.

room="2.5.2"
# Room
room = { module= "androidx.room:room-ktx", version.ref="room" }
room-runtime ={ module= "androidx.room:room-runtime", version.ref="room" }
room-compiler = { module = "androidx.room:room-compiler", version.ref="room" }
id("kotlin-parcelize")
// Room
implementation(libs.room)
implementation(libs.room.runtime)
ksp (libs.room.compiler)

Room Classes

Now, you’re ready to add the basic classes required by Room. This includes the Entities, DAOs and the Database. Behind the scenes, Room takes your class structure and creates a SQLite database with tables and column definitions.

Metolafa LapuviFou EwqyujiojhTeo YebequCemohama es:Evx qofwe: Wtderk igodo: Mbyiwx? lohwary: Nblocl entpjaznuivl:Gymipz? goowtoUmr: Trpunv tkonovomiuqKeyopul: Upt toenifsQolufuv: Ihg guajfUrKopapoc: Akj zahxisjt: Awt Sikis Ekkaziip SolaboPg op:Ujs yasegeEb: Emq? poye: Zlpihj eurka:Pwback? uyido: Hgziwj? omarusih: Tvrimw ahoary: Yeutro oked: Qvpuzd EjtqucuipvMc tidoye_tuxajomi oc pictu ixoxe qaqnayb etyzxucpuint vuomjaEfz Fedaxi Hanlo oz wesuraIw savi oetco ujaqa ojeyoway Ekzcigiepn Wodce Siaf Wiuw Weuz Liem Bikiwoti Zloepaem Fbabimj

Entities

Recipe Finder requires two entity types to store recipes: RecipeDb and IngredientDb.

RecipeDb

Create a package called database in the data package. Inside this package, create a Kotlin file named RecipeDb.kt and replace the contents with the following:

import android.os.Parcelable
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import kotlinx.parcelize.Parcelize

// 1
@Parcelize
// 2
@Entity(tableName = "recipes")
// 3
data class RecipeDb(
  // 4
  @PrimaryKey(autoGenerate = false)
  // 5
  @ColumnInfo(name = "id")
  var id: Int,
  @ColumnInfo(name = "title")
  var title: String,
  @ColumnInfo(name = "image")
  var image: String?,
  @ColumnInfo(name = "summary")
  var summary: String = "",
  @ColumnInfo(name = "instructions")
  var instructions: String? = "",
  @ColumnInfo(name = "sourceUrl")
  var sourceUrl: String = "",
  @ColumnInfo(name = "preparationMinutes")
  var preparationMinutes: Int = 0,
  @ColumnInfo(name = "cookingMinutes")
  var cookingMinutes: Int = 0,
  @ColumnInfo(name = "readyInMinutes")
  var readyInMinutes: Int = 0,
  @ColumnInfo(name = "servings")
  var servings: Int = 0,
) : Parcelable

IngredientDb

Create a Kotlin file named IngredientDb.kt in the data/database package and replace the contents with the following:

import android.os.Parcelable
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import kotlinx.parcelize.Parcelize

@Parcelize
@Entity(tableName = "ingredients")
data class IngredientDb(
  @PrimaryKey(autoGenerate = false)
  @ColumnInfo(name = "id")
  var id: Int,
  @ColumnInfo(name = "recipeId")
  var recipeId: Int?,
  @ColumnInfo(name = "name")
  var name: String,
  @ColumnInfo(name = "aisle")
  var aisle: String? = "",
  @ColumnInfo(name = "image")
  var image: String? = "",
  @ColumnInfo(name = "original")
  var original: String = "",
  @ColumnInfo(name = "amount")
  var amount: Double = 0.0,
  @ColumnInfo(name = "unit")
  var unit: String = "",
) : Parcelable

DAOs

Next, you’ll define the data access object that reads and writes from the database.

import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update

// 1
@Dao
interface RecipeDao {
  // 2
  @Insert(onConflict = OnConflictStrategy.IGNORE)
  suspend fun addRecipe(recipe: RecipeDb)

  // 3
  @Query("SELECT * FROM recipes WHERE id = :id")
  suspend fun findRecipeById(id: Int): RecipeDb

  // 4
  @Query("SELECT * FROM recipes")
  suspend fun getAllRecipes(): List<RecipeDb>

  // 5
  @Update
  suspend fun updateRecipeDetails(recipe: RecipeDb)

  // 6
  @Delete
  suspend fun deleteRecipe(recipe: RecipeDb)

  @Query("DELETE FROM recipes WHERE id = :recipeId")
  suspend fun deleteRecipeById(recipeId: Int)
}
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update

@Dao
interface IngredientDao {
  @Insert(onConflict = OnConflictStrategy.IGNORE)
  suspend fun addIngredient(ingredientDb: IngredientDb)

  @Query("SELECT * FROM ingredients WHERE id = :id")
  suspend fun findIngredientById(id: Int): IngredientDb

  @Query("SELECT * FROM ingredients WHERE recipeId = :id")
  suspend fun findIngredientsByRecipe(id: Int): List<IngredientDb>

  @Query("SELECT * FROM ingredients")
  suspend fun getAllIngredients(): List<IngredientDb>

  @Update
  suspend fun updateIngredientDetails(ingredientDb: IngredientDb)

  @Delete
  suspend fun deleteIngredient(ingredientDb: IngredientDb)
}

Database

The last piece needed to complete the Room classes is the Database.

import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase

// 1
@Database(entities = [RecipeDb::class, IngredientDb::class], version = 1, exportSchema = false)
abstract class RecipeDatabase : RoomDatabase() {
  // 2
  abstract fun recipeDao(): RecipeDao
  abstract fun ingredientDao(): IngredientDao

  // 3
  companion object {
      /*The value of a volatile variable will never be cached, and all writes and reads will be done to and from the main memory.
      This helps make sure the value of INSTANCE is always up-to-date and the same for all execution threads.
      It means that changes made by one thread to INSTANCE are visible to all other threads immediately.*/
      @Volatile
      // 4
      private var INSTANCE: RecipeDatabase? = null

      // 5
      fun getInstance(context: Context): RecipeDatabase {
        // only one thread of execution at a time can enter this block of code
        synchronized(this) {
          var instance = INSTANCE

         // 6
          if (instance == null) {
            instance = Room.databaseBuilder(
                context.applicationContext,
                RecipeDatabase::class.java,
                "recipe_database"
            ).fallbackToDestructiveMigration()
                .build()

            INSTANCE = instance
          }
          // 7
          return instance
        }
      }
  }
}

Creating the Repository

Your basic Room classes are ready to go. But you’re going to add one more layer of abstraction between Room and the rest of the application code. Doing this makes changing how and where you store the app data easy. This abstraction layer will be provided using a Repository pattern. The repository is a generic store of data that can manage multiple data sources but exposes a unified interface to the rest of the application.

import com.kodeco.recipefinder.data.database.IngredientDao
import com.kodeco.recipefinder.data.database.IngredientDb
import com.kodeco.recipefinder.data.database.RecipeDao
import com.kodeco.recipefinder.data.database.RecipeDatabase
import com.kodeco.recipefinder.data.database.RecipeDb

// 1
class RecipeRepository(recipeDatabase: RecipeDatabase) {
  // 2
  private val recipeDao: RecipeDao = recipeDatabase.recipeDao()
  private val ingredientDao: IngredientDao = recipeDatabase.ingredientDao()

  // 3
  suspend fun findAllRecipes(): List<RecipeDb> {
    return recipeDao.getAllRecipes()
  }

  suspend fun findBookmarkById(id: Int): RecipeDb {
    return recipeDao.findRecipeById(id)
  }

  suspend fun findRecipeById(id: Int): RecipeDb {
    return recipeDao.findRecipeById(id)
  }

  suspend fun findAllIngredients(): List<IngredientDb> {
    return ingredientDao.getAllIngredients()
  }

  suspend fun findRecipeIngredients(recipeId: Int): List<IngredientDb> {
    return ingredientDao.findIngredientsByRecipe(recipeId)
  }

 // 4
 suspend fun insertRecipe(recipe: RecipeDb) {
    recipeDao.addRecipe(recipe)
  }

  suspend fun insertIngredients(ingredients: List<IngredientDb>) {
    ingredients.forEach {
        ingredientDao.addIngredient(it)
    }
  }

  // 5
  suspend fun deleteRecipe(recipe: RecipeDb) {
    recipeDao.deleteRecipe(recipe)
  }

  suspend fun deleteRecipeById(recipeId: Int) {
    recipeDao.deleteRecipeById(recipeId)
  }

  suspend fun deleteIngredient(ingredient: IngredientDb) {
    ingredientDao.deleteIngredient(ingredient)
  }

  suspend fun deleteIngredients(ingredients: List<IngredientDb>) {
    ingredients.forEach {
        ingredientDao.deleteIngredient(it)
    }
  }

  suspend fun deleteRecipeIngredients(recipeId: Int) {
    val ingredients = findRecipeIngredients(recipeId)
    ingredients.forEach {
        ingredientDao.deleteIngredient(it)
    }
  }
}

ViewModels

Now that you’ve built the repository, you’ll use it in a ViewModel, a perfect place for it.

class RecipeViewModel(
  private val prefs: Prefs,
  private val repository: RecipeRepository,
) : ViewModel() {
suspend fun getBookmarks() {
  withContext(Dispatchers.IO) {
    val allRecipes = repository.findAllRecipes()
    _bookmarksState.value = recipeDbsToRecipes(allRecipes).toMutableList()
  }
}
suspend fun getIngredients() {
  withContext(Dispatchers.IO) {
    val allIngredients = repository.findAllIngredients()
    _ingredientsState.value = ingredientDbsToIngredients(allIngredients).toMutableList()
  }
}
suspend fun getBookmark(bookmarkId: Int) {
  withContext(Dispatchers.IO) {
    val recipe = repository.findRecipeById(bookmarkId)
    val ingredients = repository.findRecipeIngredients(bookmarkId)
    _recipeState.value =
        recipeDbToRecipeInformation(recipe, ingredientDbsToExtendedIngredients(ingredients))
  }
}
suspend fun bookmarkRecipe(recipe: RecipeInformationResponse) {
  withContext(Dispatchers.IO) {
    repository.insertRecipe(recipeInformationToRecipeDb(recipe))
    repository.insertIngredients(
      extendedIngredientsToIngredientDbs(
        recipe.id,
        recipe.extendedIngredients
      )
    )
  }
}
suspend fun deleteBookmark(recipe: Recipe) {
  withContext(Dispatchers.IO) {
    // 1
    val recipeDb = recipeToDb(recipe)
    // 2
    repository.deleteRecipe(recipeDb)
    repository.deleteRecipeIngredients(recipe.id)
    // 3
    val localList = _bookmarksState.value.toMutableList()
    // 4
    localList.remove(recipe)
    _bookmarksState.value = localList
  }
}
suspend fun deleteBookmark(recipeId: Int) {
  withContext(Dispatchers.IO) {
    repository.deleteRecipeById(recipeId)
    repository.deleteRecipeIngredients(recipeId)
    val localList = _bookmarksState.value.toMutableList()
    localList.removeIf {
      it.id == recipeId
    }
    _bookmarksState.value = localList
  }
}

Instantiating the Repository

Now that you’ve set up repository usage, it’s time to create it. Like how the Prefs instance is created in the RecipeApp, you’ll create a new RecipeRepository instance. Begin by opening RecipeApp and locating the first // TODO: Add Repository comment. Replace the comment with:

lateinit var repository: RecipeRepository
repository = RecipeRepository(
  Room.databaseBuilder(
    this,
    RecipeDatabase::class.java,
    "Recipes"
  ).build()
)

Local Repository Provider

A lot of classes use the repository. How can you provide that repository to all composables in the UI? By using the Local Provider concept. This is a way to provide classes to other composables. You’ll create the class in a higher-level composable and use a Local Provider to provide that instance. Open MainActivity.kt and, after the LocalNavigatorProvider global variable, add:

val LocalRepositoryProvider =
    compositionLocalOf<RecipeRepository> { error("No repository provided") }
import com.kodeco.recipefinder.data.RecipeRepository
LocalRepositoryProvider provides (application as RecipeApp).repository,

Bookmarks

It’s time to update the ui/recipes/ShowBookmarks.kt file. Find the // TODO: Provide current item comment and replace the method call below it with:

viewModel.deleteBookmark(currentItem)

Recipe Details

The last file to update is ui/RecipeDetails.kt. Find the first // TODO: Add Repository and replace it with:

val repository = LocalRepositoryProvider.current
RecipeViewModel(prefs, repository)
viewModel.getBookmark(databaseRecipeId)
 viewModel.deleteBookmark(recipe.id)
viewModel.bookmarkRecipe(recipe)

Updating the ViewModel References

Because you updated the RecipeViewModel to take in a repository, you must fix the instantiation in the GroceryList and RecipeList composable functions. To fix that, open the following files

val repository = LocalRepositoryProvider.current
RecipeViewModel(prefs, repository)

Updating Previews

The last requirement before building and running the changes is to update the preview composables. Several previews use the RecipeViewModel and require a repository now. In each of the following classes:

val repository = LocalRepositoryProvider.current
RecipeViewModel(prefs, repository)

Groceries

If you tap the groceries bottom button, you see there are no groceries. To fix that, open ui/groceries/GroceryList.kt. Find // TODO: Get Ingredients and replace it with:

scope.launch {
  recipeViewModel.getIngredients()
}
scope.launch {
  recipeViewModel.ingredientsState.collect { ingredients ->
    groceryListViewModel.setIngredients(ingredients)
  }
}

Alternatives

You could use SQLite and the classes that surround it, but it takes a bit of work to create and maintain the database. Here are a few alternatives:

Key Points

  • Room is a great way to create databases and save data.
  • It’s easy to create a database and store recipes.

Where to Go From Here?

In this chapter, you learned how to store data in a database.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now