Institute of Computer Science
  1. Courses
  2. 2021/22 fall
  3. Mobile Computing and Internet of Things (LTAT.06.009)
ET
Log in

Mobile Computing and Internet of Things 2021/22 fall

  • Main
  • Lectures
  • Labs
  • Homeworks & Assignments
  • Results
  • Task submission
  • Extra Materials
  • Best Practices
  • Projects
    • Teams & Topics
    • Presentations & Report

Plants App with Local Storage

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

The project has 3 activities:

  1. MainActivity - displaying list of plant names in a RecyclerView, using a custom PlantsAdapter
  2. NewPlantActivity - a set of EditTexts for creating a new plant
  3. PlantDetailsActivity - for showing full details of a Plant

Additionally, the project has:

  • PlantViewModel, which extends AndroidViewModel, a ViewModel variant that also can hold application context.
    • It currently holds some placeholder values in its Array
  • PlantsAdapter, that pulls data from a PlantViewModel's PlantArray property . This adapter is used in MainActivity.
  • room.PlantEntity - an empty placeholder class for Plants

Currently, the application is lacking in features and doesn't do much interesting. Your task is to update the app so that Plants 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:

  • At the start of file:
    • apply plugin: 'kotlin-kapt'
  • Inside dependencies { .. } :
    def room_version = "2.3.0"

    implementation "androidx.room:room-runtime:$room_version"
    kapt "androidx.room:room-compiler:$room_version"

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

1.1 Entity

Modify the existing PlantEntity 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)
    • latinName, description - of type String?
    • plantedTime of type Date?
  • Manually set the table name to be 'plant', by changing the @Entity annotation so that includes argument @Entity(tableName = "plant")

After modifying the Entitiy, the original code in ViewModel will no longer work due to the signature changing. You can replace the value of PlantArray with an empty array (arrayOf() with no arguments).

Room can automatically convert primary types like Int, String, Long etc to DB table columns. But for types like Date, we need to specify a Type converter.

  • Create a new class for converting Dates to Long like below. We will later tell the room DB to make use of this converter.
    class DateTypeConverter {
        @TypeConverter
        fun toDate(date: Long) = Date(date)

        @TypeConverter
        fun fromDate(date: Date?) = date?.time ?: Calendar.getInstance().timeInMillis
    }

1.2 Data Access Object

  • Create a new interface, called PlantDao, the empty class should look like
    @Dao
    interface PlantDao {
    }
  • Let's add one query to the Dao - to fetch all Plant titles:
    @Query("SELECT name FROM plant")
    fun loadPlantTitles(): Array<String>
  • Add a 2nd query for inserting Plants:
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insertPlants(vararg plants: PlantEntity)

1.3 Database

  • Finally, create the database abstract class, LocalPlantDb.kt :
    @TypeConverters(DateTypeConverter::class)
    @Database(entities = [ ..... ], version = 1)
    abstract  class LocalPlantDb : RoomDatabase() {
        abstract fun getPlantDao(): PlantDao
    }
  • The array elements for entities should correspond to classes of the Entities you have defined, in our case, it's only PlantEntity (use <<Myclassname>>::class) ).
  • The @TypeConverters annotation tells Room how to handle types which aren't supported by the standard Room library. In this case, DateTypeConverter tells Room how to map a Date object to Long, the latter is a type Room can already handle itself.

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, LocalPlantDb::class.java, "myPlants")
            .fallbackToDestructiveMigration() // each time schema changes, data is lost!
            .allowMainThreadQueries() // if possible, use background thread instead
            .build()
  • Now, for testing, let's create a PlantEntity object instance: and
    val newPlant = PlantEntity(0, //0 corresponds to 'no value', autogenerate handles it for us
            "Thyme",
            "The genus Thymus contains about 350 species of aromatic perennial herbaceous plants",
            "Thymus",
            SimpleDateFormat(PlantEntity.DATEFORMAT).parse("30/09/2021"))
  • Try to insert the entity to the DB using the insert method provided by our DAO: db.getPlantsDao().<<someMethodHere>>
  • To verify that the insert worked, add code after the insert which fetches all the Plant titles from 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 use the singleton pattern - we create an object of which there can only be a single instance. In Kotlin, this can be done using object declaration. 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 the companion object of our Room DB class and access it through a method getInstance( .. ) which avoids rebuilding the DB object each time.

  • Modify your LocalPlantDb class, adding the following:
    companion object {
        private lateinit var plantDb : LocalPlantDb

        @Synchronized fun getInstance(context: Context) : LocalPlantDb {

            if (!this::plantDb.isInitialized) {
                //TODO: build the database
            }
            return plantDb

        }
    }
  • The above code allows us to call LocalPlantDb.getInstance(context) from anywhere, returning the singleton instance of the DB.
  • Move the DB building code from your MainActivity to the TODO block above, assigning the object it returns to PlantDb, which the method returns.

Now you should be able to get access to the DB from MainActivity using the singleton instead.

3. Showing a list of Plants from DB

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

    init {
        val localDb = LocalPlantDb.getInstance(application)
    }
  • Now, fill the refresh() function of PlantViewModel so that it reads all Plants (not just titles) from the DB and sets the value of PlantArray to the DB result.
    • First, you need to add a new query to DAO for getting entire Plants!
      • instead of selecting titles, you return all columns (with *) and instead of returning Array<String>, you should be returning Array<PlantEntity>
    • Now, you can use the new DAO method in the refresh() function
  • Update the PlantsAdapter to also show Plant latin name to title (check single_plant.xml)

4. Adding new Plants to DB

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

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

5. Optional Bonus Task: Displaying Plant details from DB

Try to implement fetching of all details for PlantDetailsActivity and showing them in the UI. Note that PlantsAdapter sets up the click listener for list items, but the new activity launching logic is defined in MainActivity.

6. Optional Bonus Task: Using DatePicker

Can you replace the manual DD/MM/YYYY date entry to use a Date Picker Dialog instead?

  • Institute of Computer Science
  • Faculty of Science and Technology
  • University of Tartu
In case of technical problems or questions write to:

Contact the course organizers with the organizational and course content questions.
The proprietary copyrights of educational materials belong to the University of Tartu. The use of educational materials is permitted for the purposes and under the conditions provided for in the copyright law for the free use of a work. When using educational materials, the user is obligated to give credit to the author of the educational materials.
The use of educational materials for other purposes is allowed only with the prior written consent of the University of Tartu.
Terms of use for the Courses environment