How to Build a CRUD App With Kotlin and Android Studio

Chapter 3 Data Classes

Problems covered in this chapter
  • 1:n relations

  • m:n relations

The database tables and their associated Kotlin counterparts are defined in the data classes.

3.1 Gender.kt

Listing 1: Gender.kt
package app.gedama.tutorial.cruddiary.data
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
// sets UNIQUE constraint to name column,
// see https://stackoverflow.com/questions/48962106/add-unique-constraint-in-room-database-to-multiple-column
@Entity(tableName = "genders", indices = [Index(value=["name"], unique = true)])
data class Gender (
    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name="gender_id")
    var genderId: Long = 0L,
    var name: String = "",
    @ColumnInfo(defaultValue = "0")
    var inactive: Boolean = false
)

Gender.kt is a data class whose associated database table is called genders. By means of the index to the name column and the addition unique=true, we ensure that the designations for genders must be unique. For example, there cannot be two entries with the same designation ’female’. The column gender_id is our primary key, for which we let the database automatically generate consecutive ID values during insertion using autoGenerate=true. If the column inactive is not set when inserting it into the database, it receives the default value 0 in the database.

3.2 Person.kt

Listing 2: Person.kt
package app.gedama.tutorial.cruddiary.data
import androidx.room.*
import java.util.Date
// sets UNIQUE constraint to name column,
// see https://stackoverflow.com/questions/48962106/add-unique-constraint-in-room-database-to-multiple-column
@Entity(tableName = "persons", foreignKeys = [ForeignKey(
    entity = Gender::class,
    childColumns = ["gender_id"],
    parentColumns = ["gender_id"])], indices = [Index(value=["gender_id"]), Index(value=["name"], unique = true)] )
data class Person(
    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name="person_id")
    var personId: Long = 0L,
    var name: String = "",
    @ColumnInfo(name="gender_id")
    var genderId: Long? = null,
    @ColumnInfo(name = "birth_date")
    var birthDate: Date? = null,
    @ColumnInfo(name = "image_path")
    var imagePath: String = "",
    @ColumnInfo(defaultValue = "0")
    var inactive: Boolean = false
)  {
    @Ignore
    var birthDateAsString: String = when (birthDate) {
        null -> ""
        else -> DateFormat.getDateInstance(DateFormat.MEDIUM).format(birthDate)
    }
}

Person.kt is another data class whose associated database table is called persons. By means of the index to the name column and the addition unique=true, we ensure that the name must be unique for our persons. For example, there cannot be two entries with the same name ’Anne’. The column person_id is our primary key, for which we let the database automatically generate consecutive ID values when inserting it using autoGenerate=true. The column gender_id is a foreign key that refers to the table genders defined above. birth_date is the optional date of birth of the person. image_path is the file name for an optional image of the person. If the column inactive is not set when it is inserted into the database, it receives the default value 0 in the database. The variable birthDateAsString has the annotation @Ignore - thus there is no corresponding column in the database table persons. It is used to display the date of birth in the user interface, where the date must be displayed as a string.

3.3 Tag.kt

Listing 3: Tag.kt
package app.gedama.tutorial.cruddiary.data
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
// sets UNIQUE constraint to title column,
// see https://stackoverflow.com/questions/48962106/add-unique-constraint-in-room-database-to-multiple-column
@Entity(tableName = "tags", indices = [Index(value=["title"], unique = true)] )
data class Tag (
    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "tag_id")
    var tagId: Long = 0L,
    var title: String = "",
    @ColumnInfo(defaultValue = "0")
    var inactive: Boolean = false
)

The data class Tag.kt is another data class that stores our keywords in the database table tags. Its content should be clear based on the descriptions of the previous two data classes.

3.4 Preferences.kt

Listing 4: Preferences.kt
package app.gedama.tutorial.cruddiary.data
import androidx.room.*
@Entity(tableName = "preferences", foreignKeys = [ForeignKey(
    entity = Person::class,
    childColumns = ["main_user_id"],
    parentColumns = ["person_id"]) ] , indices = [Index(value=["main_user_id"])] )
data class Preferences (
    @PrimaryKey()
    @ColumnInfo(name = "preference_id")
    var preferenceId: Int = 1,
    @ColumnInfo(name = "main_user_id", defaultValue="1")
    var mainUserId: Long = 1,
    @ColumnInfo(name = "show_inactive", defaultValue = "0")
    var showInactive: Boolean = false
)

The data class Preferences.kt stores settings in the associated database table preferences that the user can set in the preferences menu. The column main_user_id is a foreign key to the persons table, which can be used to set the main user of the diary app. The column show_inactive defines whether inactive entries should be displayed in the list view (greyed out).

3.5 DiaryEntry.kt

Listing 5: DiaryEntry.kt
package app.gedama.tutorial.cruddiary.data
import androidx.room.*
import java.text.DateFormat
import java.util.*
@Entity(tableName = "diary_entries", foreignKeys = [ForeignKey(
    entity = Person::class,
    childColumns = ["person_id"],
    parentColumns = ["person_id"]) ] , indices = [Index(value=["person_id"])] )
data class DiaryEntry(
    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name="entry_id")
    var entryId: Long = 0L,
    var title: String = "",
    var date: Date = Date(),
    var text: String = "",
    @ColumnInfo(name="person_id")
    var personId: Long? = null,
    @ColumnInfo(name = "image_path")
    var imagePath: String = "",
    @ColumnInfo(name = "pdf_path")
    var pdfPath: String = "",
    @ColumnInfo(name = "pdf_file_name")
    var pdfFileName: String = "",
    var relevance: Int = 3,
    @ColumnInfo(defaultValue = "0")
    var inactive: Boolean = false
) {
    @Ignore
    var dateAsString: String = DateFormat.getDateInstance(DateFormat.MEDIUM).format(date)
}

Our diary entries are stored in the data class DiaryEntry.kt and the associated database table diary_entries. The code should be self-explanatory by now due to the description of the previous data classes.

3.6 DiaryEntriesTagsCrossRef.kt

Listing 6: DiaryEntriesTagsCrossRef.kt
package app.gedama.tutorial.cruddiary.data
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
// for info on indices see
// https://stackoverflow.com/questions/54886250/room-database-what-is-index-specific-columns-indices-and-index-and-how-to-us
@Entity(tableName="diary_entries_tags", primaryKeys = ["entry_id", "tag_id"], foreignKeys = [ForeignKey(
    entity = DiaryEntry::class,
    childColumns = ["entry_id"],
    parentColumns = ["entry_id"]), ForeignKey(
    entity = Tag::class,
    childColumns = ["tag_id"],
    parentColumns = ["tag_id"]) ] , indices = [Index(value=["tag_id"]), Index(value=["entry_id"])] )
class DiaryEntriesTagsCrossRef(
    @ColumnInfo(name="entry_id")
    val entryId: Long,
    @ColumnInfo(name="tag_id")
    val tagId: Long
)

The data class DiaryEntriesTagsCrossRef.kt stores the assignments of diary entries to keywords and is an m:n relation between the two tables diary_entries and tags.

3.7 DiaryEntryWithEverything.kt

Listing 7: DiaryEntryWithEverything.kt
package app.gedama.tutorial.cruddiary.data
import androidx.room.Embedded
import androidx.room.Junction
import androidx.room.Relation
// see https://medium.com/@jaclync/android-room-with-nested-relationships-803dad19a500
// see https://stackoverflow.com/questions/64356315/entity-with-lots-of-relationships-with-android-room
data class DiaryEntryWithEverything(
    @Embedded val entry: DiaryEntry,
    @Relation(
        parentColumn = "person_id",
        entityColumn = "person_id",
        entity = Person::class
    )
    val person: Person,
    @Relation(
        parentColumn = "entry_id",
        entityColumn = "tag_id",
        associateBy = Junction(DiaryEntriesTagsCrossRef::class)
    )
    val tags: List<Tag>
)

The data class DiaryEntryWithEverything.kt extends our data class DiaryEntry.kt in that additional properties are defined there with which we can access the person assigned by foreign key and all keywords that are entered in the table diary_entries_tags with the ID entry_id.