Arvutiteaduse instituut
  1. Kursused
  2. 2019/20 sügis
  3. Mobiiliarvutus ja asjade internet (LTAT.06.009)
EN
Logi sisse

Mobiiliarvutus ja asjade internet 2019/20 sügis

  • Main
  • Lectures
  • Labs
  • Homeworks & Home Assignments
  • Projects
    • Teams & Topics
    • Presentations & Report
    • Grading & Submission
  • Google Group
  • Task submission
  • Results
  • Other

Recipes App with Local Storage

In this lab, we will be modifying a base project which you can download here Attach: lab5base.zip .

The project has 3 activities:

  1. MainActivity - displaying list of recipe titles in a ListView, using a custom RecipesAdapter
  2. NewRecipeActivity - a set of EditTexts for creating a new recipe
  3. RecipeDetailsActivity - for showing full details of a recipe

Additionally, the project has:

  • RecipeViewModel, which extends AndroidViewModel, a ViewModel variant that also can hold application context.
    • It currently holds some placeholder values in its Array
  • RecipesAdapter, that pulls data from a RecipeViewModels entityArray member variable. This adapter is used in MainActivity.
  • room.RecipeEntity - an empty placeholder class for Recipes

Currently, the application is lacking in features and doesn't do much interesting. Your task is to update the app so that recipes are stored to and loaded from a Room DB.

0. Room dependencies and annotation processing tools

To use Room, you need to add the following to module-level build.gradle:

  • Inside dependencies { .. } :
    • implementation 'android.arch.persistence.room:runtime:1.1.1'
    • kapt 'android.arch.persistence.room:compiler:1.1.1'
  • At the start of file:
    • apply plugin: 'kotlin-kapt'

1. Set up Room objects: Entity, DAO, and Database

  1. Modify the existing RecipeEntity class in package room of the project to make it a proper Room Entity.
    • Use the @Entity annotation on the class, the class should also be a Kotlin data class instead of a normal class. With data class, you can declare all fields within the constructor, meaning we don't need to write any code within the class body (within the curly braces ).
    • Add the following fields:
      • id - type Int, with annotation @PrimaryKey(autoGenerate = true)
      • title, content, author - of type String?
      • preparationTime of type Double?
    • Manually set the table name to be 'recipe', by changing the @Entity annotation so that includes argument @Entity(tableName = "recipe")
    • After modifying the Entitiy, the original code in RecipesAdapter will no longer work due to the signature changing. You can replace the value of entityArray with an empty array (arrayOf() with no arguments).
  2. Create Data Access Object
  • Create a new interface, called RecipeDao, the empty class should look like
    @Dao
    interface RecipeDao {
    }
  • Let's add one query to the Dao - to fetch all recipe titles:
    @Query("SELECT title FROM recipe")
    fun loadRecipeTitles(): Array<String>
  • Add a second query for inserting recipes:
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insertRecipes(vararg recipes: RecipeEntity)
  1. Finally, create the database abstract class, LocalRecipeDb.kt :
    @Database(entities = arrayOf( ..... ), version = 1)
    abstract  class LocalRecipeDb : RoomDatabase() {
        abstract fun getRecipesDao(): RecipeDao
    }
  • The array elements of entities should correspond to the Entities you have defined, in our case, it's only RecipeEntity (use arrayOf(<<Myclassname>>::class) ).

Now we should have our database established. Note: Whenever you modify the Entity schema after a database exists on the device, you have to update your database version in the RoomDatabase subclass.

Let's test the database in MainActivity.onCreate():

    val db = Room.databaseBuilder(
            applicationContext, LocalRecipeDb::class.java, "myRecipes")
            .fallbackToDestructiveMigration() // each time schema changes, data is lost!
            .allowMainThreadQueries() // if possible, use background thread instead
            .build()

Now create a RecipeEntity object instance and try to insert it to the DB using the methods provided by our DAO db.getRecipesDao().<<someMethodHere>>

    val newRecipe = RecipeEntity(0, //0 correspond to 'no value', autogenerate handles it for us
            "Peeled banana",
            "Take a banana and peel it.",
            "John",
            1.0)

When using the insert method, don't forget to pass the recipe object as an argument! After inserting it, try to fetch all the recipe titles in the DB and see if you can log their contents.

2. Using the database from multiple Activities

Since we want to access the database from multiple activities and calling Room's database builder is expensive, we want to avoid re-calling that build procedure multiple times in different activities. We will create singleton class - a class of which there can only be a single instance. In Kotlin, this type of class is called object. When some component first calls the object, it is initialized. When subsequent components call it, the existing instance is re-used.

We will build the database object inside an object and access it through a method getDatabase( .. ) which avoids recreating the db object each time.

  • Create a new Kotlin File,
    object LocalDbClient {
        var recipeDb : LocalRecipeDb? = null
        fun getDatabase(context: Context) : LocalRecipeDb? {

            if (recipeDb == null){
                // TODO: initialize the recipeDB object using applicationContext attached to the context
            }
            return recipeDb
        }

    }
  • Move the DB building code from your MainActivity to the TODO block above, assigning the object it returns to recipeDb, which the method returns. Now you should be able to call val db = LocalDbClient.getDatabase(this)? in MainActivity instead.

3. Showing a list of recipes from DB

Update RecipeViewModel, so that it has a reference to the database through LocalDbClient. We do this using Kotlin init clause, which is run when the class instance is created.

    init {
        localDb = LocalDbClient.getDatabase(application)!! // NPE danger
    }
  • Now, fill the refresh() function of RecipeViewModel so that it reads all recipes (not just titles) from the DB and sets the value of entityArray to the DB result.
    • First, you need to add a new query to DAO for getting entire recipes!
      • instead of selecting titles, you return all columns (with *) and instead of returning Array<String>, you should be returning Array<RecipeEntity>
    • Now, you can use the new DAO method in the refresh() function
  • Update the RecipesAdapter to also show recipe author next to title (check single_recipe.xml)

4. Adding new Recipes to DB

  • Finish the saveButton click listener in NewRecipeActivity
    • get a reference to the DB using the LocalDbClient object
    • construct a RecipeEntity object and insert it into the DB using a Dao method
    • The for inserting we defined above supports passing a single or multiple Recipes!

Since MainActivity onResume() method calls the ViewModel refresh() method and notifies the adapter, the new recipe should become visible in the main list.

5. Optional Bonus Task: Displaying Recipe details from DB

Try to implement fetching of all details for RecipeDetailsActivity and showing them in the UI. Note that RecipesAdapter calls activity.openDetails(recipe.id)

  • Arvutiteaduse instituut
  • Loodus- ja täppisteaduste valdkond
  • Tartu Ülikool
Tehniliste probleemide või küsimuste korral kirjuta:

Kursuse sisu ja korralduslike küsimustega pöörduge kursuse korraldajate poole.
Õppematerjalide varalised autoriõigused kuuluvad Tartu Ülikoolile. Õppematerjalide kasutamine on lubatud autoriõiguse seaduses ettenähtud teose vaba kasutamise eesmärkidel ja tingimustel. Õppematerjalide kasutamisel on kasutaja kohustatud viitama õppematerjalide autorile.
Õppematerjalide kasutamine muudel eesmärkidel on lubatud ainult Tartu Ülikooli eelneval kirjalikul nõusolekul.
Courses’i keskkonna kasutustingimused