How to Build a CRUD App With Kotlin and Android Studio

Chapter 13 Add Persons

Problems covered in this chapter
  • Drop-down menus with database values

  • Saving images and PDF files

  • Reusable XML blocks for insertions and updates

  • Styles to unify the user interface

  • Basic fragment class with useful methods for all further fragments

  • Form validations in the user interface

  • Generic date-time picker for date values

In the last chapter we covered the listing of persons stored in the database. In order to actually see persons in this list, we now enable the user to add new persons (see Figure 13.1).

Add new person
Figure 13.1: Add new person

We start by adding an interface to our data-package:

13.1 Spinnable.kt

Listing 77: Spinnable.kt
package app.gedama.tutorial.cruddiary.data
interface Spinnable {
    fun itemName(): String
    fun itemId(): Long
}

We use this simple interface for all data classes whose values are displayed in drop down boxes in the UI. For example, when the user adds a person, the person’s gender should be selectable from a drop down box providing all possible genders. Let’s extend our existing Gender data class by this interface:

13.2 Gender.kt

Listing 78: 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
): Spinnable {
    override fun itemId() = genderId
    override fun itemName() = name
    override fun toString() = name
}

Later on we will also extend the Person data class since persons should be selectable in a drop down box in the UI when adding new diary entries. Using an interface for all classes that can be selected in a drop down box lets us use a more concise code for this task.

13.3 BaseViewModel.kt

Since the user can add images to persons, we add a couple of variables and methods in our BaseViewModel class that deal with image handling:

Listing 79: BaseViewModel.kt
package app.gedama.tutorial.cruddiary.viewmodel
import android.content.Context
import android.graphics.Bitmap
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import app.gedama.tutorial.cruddiary.GlobalAppStore
import java.io.ByteArrayOutputStream
import java.io.File
import java.util.Date
abstract class BaseViewModel: ViewModel() {
    val TAG = this::class.simpleName
    val _navigateToList = MutableLiveData(false)
    val navigateToList: LiveData<Boolean>
        get() = _navigateToList
    val _navigateToAddItem = MutableLiveData(false)
    val navigateToAddItem: LiveData<Boolean>
        get() = _navigateToAddItem
    val _navigateToEditItem= MutableLiveData<Long?>()
    val navigateToEditItem: LiveData<Long?>
        get() = _navigateToEditItem
    val _insertSuccess = MutableLiveData<Boolean?>(null)
    val insertSuccess: LiveData<Boolean?>
        get() = _insertSuccess
    val _deactivateSuccess = MutableLiveData(false)
    val deactivateSuccess: LiveData<Boolean>
        get() = _deactivateSuccess
    protected val _doValidation = MutableLiveData<Boolean>(false)
    val doValidation: LiveData<Boolean>
        get() = _doValidation
    private val _oldImage = MutableLiveData<String>("")
    val oldImage: LiveData<String>
        get() = _oldImage
    var imageBitmap: Bitmap? = null
    var defaultImage = true
    fun addItem() {
        _navigateToAddItem.value = true
    }
    fun onNavigatedToList() {
        _navigateToList.value = false
    }
    fun onItemClicked(itemId: Long) {
        _navigateToEditItem.value = itemId
    }
    fun onItemAddNavigated() {
        _navigateToAddItem.value = false
    }
    fun onItemEditNavigated() {
        _navigateToEditItem.value = null
    }
    fun onValidatedAndUpdated() {
        _doValidation.value = false
    }
    fun setOldImage(oi: String) {
        _oldImage.value = oi
    }
    fun writeImage(filenameprefix: String): String {
        val currentTimeInMillis = System.currentTimeMillis()
        val fileName = GlobalAppStore.APP_FILEPATH_PREFIX + filenameprefix + currentTimeInMillis + ".jpg"
        val stream = ByteArrayOutputStream()
        imageBitmap!!.compress(Bitmap.CompressFormat.JPEG, 100, stream)
        val bitmapdata: ByteArray = stream.toByteArray()
        val context = GlobalAppStore.appCtx
        context!!.openFileOutput(fileName, Context.MODE_PRIVATE).use {
            it.write(bitmapdata)
        }
        return fileName
    }
    private fun deleteOldImage(filename: String) {
        val context = GlobalAppStore.appCtx
        val file = File(context!!.filesDir, filename)
        val result = file.delete()
        Log.d(TAG, "Result from deleting $filename: $result")
    }
    fun checkNewImagePath(filenameprefix: String): String? {
        var newValue: String? = null
        // handle several cases
        if ( oldImage.value!!.isNotBlank() && defaultImage ) {
            // a former image was deleted
            deleteOldImage(oldImage.value!!)
            newValue = ""
        } else if ( oldImage.value!!.isNotBlank() && imageBitmap != null ) {
            // a former image was replaced by a new one
            deleteOldImage(oldImage.value!!)
            newValue = writeImage(filenameprefix)
        } else if ( oldImage.value!!.isBlank() && imageBitmap != null ) {
            // a new image was added
            newValue = writeImage(filenameprefix)
        }
        return newValue
    }
    open fun updateEntryDate(date: Date) {
        // to be overridden in view models with a date property
    }
    open fun updateEntryAfterValidation() {
        // to be overridden by all view models that do validation
    }
}

The method writeImage() saves images to the app’s private internal storage. That is, we do not store the image data in the database but in the device’s file system. When the user updates the image of an existing person (which we handle in a later chapter), we therefore have to check if there already exists an image file and if so we have to delete it: methods deleteOldImage() and checkNewImagePath() handle the logic for doing so. We also added the variables imageBitmap and defaultImage which hold the image’s bitmap data and a flag if the UI shoul display a default image in the persons list if the user didn’t enter the person’s (optional) image.

With all these extensions we are now able to present our view model:

13.4 AddPersonViewModel.kt

Listing 80: AddPersonViewModel.kt
package app.gedama.tutorial.cruddiary.viewmodel
import android.database.sqlite.SQLiteConstraintException
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import app.gedama.tutorial.cruddiary.dao.PersonDao
import app.gedama.tutorial.cruddiary.data.Person
import app.gedama.tutorial.cruddiary.data.Spinnable
import kotlinx.coroutines.launch
import java.util.Date
class AddPersonViewModel(val personDao: PersonDao)  : BaseViewModel()  {
    var newPerson = Person()
    var allGenders = personDao.getAllGenders(0L,false)
    private val _newBirthdate = MutableLiveData<Date>()
    val newBirthdate: LiveData<Date>
        get() = _newBirthdate
    fun addPerson() {
        _doValidation.value = true
    }
    override fun updateEntryAfterValidation() {
        viewModelScope.launch {
            if ( imageBitmap != null ) {
                newPerson.imagePath = writeImage("person_")
            }
            try {
                personDao.insert(newPerson)
                _navigateToList.value = true
            } catch(sqlExc: SQLiteConstraintException) {
                _insertSuccess.value = false
            }
        }
    }
    fun updateBirthdate(newDate: Date) {
        _newBirthdate.value = newDate
        newPerson.birthDate = newDate
    }
    fun updateGender(item: Spinnable) {
        newPerson.genderId = item.itemId()
    }
}
class AddPersonViewModelFactory(private val personDao: PersonDao) : ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(AddPersonViewModel::class.java)) {
            return AddPersonViewModel(personDao) as T
        }
        throw java.lang.IllegalArgumentException("Unknown ViewModel")
    }
}

As with our GenderListViewModel from the previous chapter, we have a separate Factory-class which is passed a PersonDao object at initialization. The variable newPerson holds our new person object that gets inserted into the database after data validation and database constraint checking (the name of the person has to be unique, a gender has to be set and the birthdate is mandatory, see the definition of the Person data class).

Let’s move on to the layout part. First, we add a couple of new vector assets to our drawables folder:

  • baseline_calendar_month_24

  • baseline_add_photo_alternate_24

  • baseline_photo_size_select_large_24

  • baseline_hide_image_24

  • baseline_keyboard_arrow_down_24

And we add a transparent shape for spacing buttons to the drawables folder:

13.5 button_space.xml

Listing 81: button_space.xml
<?xml version="1.0" encoding="utf-8"?>
    <shape xmlns:android="http://schemas.android.com/apk/res/android"
        android:shape="rectangle" >
        <size android:width="5dp"
            android:height="1dp" />
        <solid android:color="@android:color/transparent" />
</shape>

13.6 string.xml (English)

For our user interface, we have to add a couple of new string values to our string.xml files:

Listing 82: string.xml (English)
<resources>
    <string name="app_name">CrudDiary</string>
    <string name="nothing_selected" translatable="false">---</string>
    <string name="male_gender">male</string>
    <string name="female_gender">female</string>
    <string name="diverse_gender">diverse</string>
    <string name="main_menu_title">Home</string>
    <string name="persons_list_menu_title">Persons</string>
    <string name="persons_add_menu_title">Add person</string>
    <string name="tags_list_menu_title">Tags</string>
    <string name="diary_entries_list_menu_title">Diary entries</string>
    <string name="genders_list_menu_title">Genders</string>
    <string name="genders_add_menu_title">Add gender</string>
    <string name="genders_update_menu_title">Update gender</string>
    <string name="help_menu_title">Help</string>
    <string name="support_section">Support</string>
    <string name="preferences_menu_title">Preferences</string>
    <string name="import_export_menu_title">Import/Export</string>
    <string name="search_view_hint">Search entries</string>
    <string name="fab_add_description">Add entry</string>
    <string name="fab_filter_description">Filter entries</string>
    <string name="fab_csv_description">Export entries to CSV file</string>
    <string name="name_label">Name</string>
    <string name="save_button">Save</string>
    <string name="deactivate_button">Deactivate</string>
    <string name="delete_button">Delete</string>
    <string name="update_button">Update</string>
    <string name="unique_gender_violation">This gender name already exists!</string>
    <string name="unique_person_violation">This person already exists!</string>
    <string name="validation_no_name">Name is required</string>
    <string name="validation_no_title">Title is required</string>
    <string name="validation_no_birthdate">Birthdate is required</string>
    <string name="validation_no_gender">Gender is required</string>
    <string name="inactive_label">Inactive</string>
    <string name="inactivity_safety_dialog_title">Safety check</string>
    <string name="inactivity_safety_check">Actually deactivate entry?</string>
    <string name="yes">Yes</string>
    <string name="no">No</string>
    <string name="no_entries">You either have not created any entries yet or there are no entries matching your search/filter criteria.</string>
    <string name="item_image_description">Assigned image</string>
    <string name="add_image_button">Add image</string>
    <string name="change_image_button">Change image</string>
    <string name="delete_image_button">Delete image</string>
    <string name="image_picking_canceled">Image picking canceled</string>
    <string name="person_gender_label">Gender</string>
    <string name="person_birthdate_label">Birthdate</string>
</resources>

Next, we extend our themes.xml file with our previously added drawables and a couple of new styles for handling images and drop down boxes:

13.7 themes.xml (Light-Mode)

Listing 83: themes.xml (Light-Mode)
<resources xmlns:tools="http://schemas.android.com/tools">
    <drawable name="saveItemButtonIcon">@drawable/baseline_save_24</drawable>
    <drawable name="deactivateItemButtonIcon">@drawable/baseline_do_not_disturb_alt_24</drawable>
    <drawable name="deleteItemButtonIcon">@drawable/baseline_delete_24</drawable>
    <drawable name="calendarSelectIcon">@drawable/baseline_calendar_month_24</drawable>
    <drawable name="addImageButtonIcon">@drawable/baseline_add_photo_alternate_24</drawable>
    <drawable name="changeImageButtonIcon">@drawable/baseline_photo_size_select_large_24</drawable>
    <drawable name="deleteImageButtonIcon">@drawable/baseline_hide_image_24</drawable>
    <item type="id" name="no_entries_text_id" />
    <!-- Base application theme. -->
    <style name="Theme.CrudDiary" parent="Theme.MaterialComponents.DayNight.NoActionBar">
        <!-- Primary brand color. -->
        <item name="colorPrimary">@color/purple_500</item>
        <item name="colorPrimaryVariant">@color/purple_700</item>
        <item name="colorOnPrimary">@color/white</item>
        <!-- Secondary brand color. -->
        <item name="colorSecondary">@color/teal_200</item>
        <item name="colorSecondaryVariant">@color/teal_700</item>
        <item name="colorOnSecondary">@color/black</item>
        <!-- Status bar color. -->
        <item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
        <!-- Customize your theme here. -->
        <item name="active_background_color">@color/white</item>
        <item name="inactive_background_color">@color/colorLightGrey</item>
        <item name="main_user_color">@color/teal_700</item>
        <item name="regular_user_color">@color/black</item>
        <item name="fab_tint_background">@color/teal_700</item>
        <item name="fab_border_color">@color/teal_700</item>
        <item name="fab_icon_color">@color/white</item>
    </style>
    <attr name="active_background_color" format="reference" />
    <attr name="inactive_background_color" format="reference" />
    <attr name="fab_tint_background" format="reference" />
    <attr name="fab_border_color" format="reference" />
    <attr name="fab_icon_color" format="reference" />
    <attr name="main_user_color" format="reference" />
    <attr name="regular_user_color" format="reference" />
    <style name="CrudDiary_LinearLayoutCompat" parent="Theme.CrudDiary">
        <item name="android:layout_width">match_parent</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:clipToPadding">false</item>
        <item name="android:orientation">vertical</item>
        <item name="android:padding">0dp</item>
        <item name="divider">@drawable/divider</item>
        <item name="showDividers">middle</item>
    </style>
    <style name="CrudDiary_TextInputLayout" parent="Widget.MaterialComponents.TextInputLayout.OutlinedBox">
        <item name="android:layout_width">match_parent</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:layout_marginBottom">10dp</item>
    </style>
    <style name="CrudDiary_LinearLayoutCompat_CardView" parent="Widget.MaterialComponents.CardView">
        <item name="android:layout_width">match_parent</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:layout_margin">8dp</item>
        <item name="cardElevation">4dp</item>
        <item name="cardCornerRadius">4dp</item>
    </style>
    <style name="CrudDiary_LinearLayoutCompat_CardView_Single_Text" parent="Theme.CrudDiary">
        <item name="android:layout_width">match_parent</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:minHeight">50dp</item>
        <item name="android:gravity">center_vertical</item>
        <item name="android:textStyle">bold</item>
        <item name="android:padding">8dp</item>
        <item name="android:textSize">16sp</item>
    </style>
    <style name="CrudDiary_LinearLayoutCompat_CardView_Text" parent="Theme.CrudDiary">
        <item name="android:layout_width">0dp</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:layout_gravity">fill_horizontal|start</item>
        <item name="android:padding">8dp</item>
        <item name="android:textSize">16sp</item>
        <item name="android:layout_row">1</item>
        <item name="android:layout_column">0</item>
    </style>
    <style name="CrudDiary_LinearLayoutCompat_CardView_Title" parent="CrudDiary_LinearLayoutCompat_CardView_Text">
        <item name="android:textStyle">bold</item>
        <item name="android:layout_row">0</item>
    </style>
    <style name="CrudDiary_LinearLayoutCompat_CardView_Image" parent="Theme.CrudDiary">
        <item name="android:layout_width">70dp</item>
        <item name="android:layout_height">70dp</item>
        <item name="android:minWidth">70dp</item>
        <item name="android:layout_gravity">center_vertical</item>
        <item name="android:padding">8dp</item>
        <item name="android:adjustViewBounds">true</item>
        <item name="android:layout_row">0</item>
        <item name="android:layout_column">1</item>
        <item name="android:layout_rowSpan">2</item>
        <item name="android:contentDescription">@string/item_image_description</item>
    </style>
    <style name="CrudDiary_LinearLayoutCompat_CardView_Grid" parent="Theme.CrudDiary">
        <item name="android:layout_width">match_parent</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:rowCount">2</item>
        <item name="android:columnCount">2</item>
        <item name="android:orientation">horizontal</item>
    </style>
    <style name="CrudDiary_ImageView_Items" parent="Theme.CrudDiary">
        <item name="android:id">@id/imageView</item>
        <item name="android:layout_width">70dp</item>
        <item name="android:layout_height">70dp</item>
        <item name="android:layout_gravity">center</item>
        <item name="android:adjustViewBounds">true</item>
        <item name="android:layout_margin">8dp</item>
    </style>
    <style name="CrudDiary_InactivityCheckbox" parent="Theme.CrudDiary">
        <item name="android:layout_width">wrap_content</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:layout_gravity">center</item>
        <item name="android:text">@string/inactive_label</item>
        <item name="android:visibility">gone</item>
    </style>
    <style name="CrudDiary_Button" parent="Widget.MaterialComponents.Button.OutlinedButton">
        <item name="android:minHeight">60dp</item>
        <item name="android:layout_width">wrap_content</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:layout_gravity">center</item>
    </style>
    <style name="CrudDiary_SpinnerTextInputLayout" parent="Widget.MaterialComponents.TextInputLayout.OutlinedBox">
        <item name="android:layout_width">match_parent</item>
        <item name="android:layout_height">wrap_content</item>
    </style>
    <style name="CrudDiary_SpinnerTextInputEditText" parent="Theme.CrudDiary">
        <item name="android:layout_width">match_parent</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:focusable">false</item>
        <item name="android:inputType">none</item>
    </style>
    <style name="CrudDiary_Spinner" parent="Theme.CrudDiary">
        <item name="android:layout_width">0dp</item>
        <item name="android:layout_height">0dp</item>
        <item name="android:layout_marginTop">10dp</item>
        <item name="android:background">@android:color/transparent</item>
        <item name="android:spinnerMode">dialog</item>
    </style>
    <style name="CrudDiary_Button.SaveItem">
        <item name="android:text">@string/save_button</item>
        <item name="icon">@drawable/saveItemButtonIcon</item>
    </style>
    <style name="CrudDiary_Button.DeactivateItem">
        <item name="android:text">@string/deactivate_button</item>
        <item name="icon">@drawable/deactivateItemButtonIcon</item>
    </style>
    <style name="CrudDiary_Button.DeleteItem">
        <item name="android:text">@string/delete_button</item>
        <item name="icon">@drawable/deleteItemButtonIcon</item>
    </style>
    <style name="CrudDiary_Button.AddImage">
        <item name="android:text">@string/add_image_button</item>
        <item name="icon">@drawable/addImageButtonIcon</item>
    </style>
    <style name="CrudDiary_Button.DeleteImage">
        <item name="android:text">@string/delete_image_button</item>
        <item name="icon">@drawable/deleteImageButtonIcon</item>
        <item name="android:visibility">gone</item>
    </style>
    <style name="CrudDiary_NoEntries" parent="Theme.CrudDiary">
        <item name="android:layout_width">match_parent</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:layout_gravity">start</item>
        <item name="android:visibility">gone</item>
        <item name="android:padding">8dp</item>
    </style>
    <style name="ToolbarTheme" parent="ThemeOverlay.MaterialComponents.Dark.ActionBar">
        <item name="android:textColor">@color/black</item>
    </style>
</resources>

Next, we add a couple of reusable layout files that we then can include in our fragment layouts (by using the ¡include¿ element). For drop down boxes we use a pretty cool solution found on stackoverflow.com:

13.8 text_input_date.xml

The following layout snippet is used for date input fields. We use an end icon to display a small calendar icon in the TextInputLayout element.

Listing 84: text_input_date.xml
<?xml version="1.0" encoding="utf-8"?>
<layout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <data>
        <variable
            name="viewModelValue"
            type="String" />
        <variable
            name="hintText"
            type="String" />
        <variable
            name="iconDescription"
            type="String" />
    </data>
    <merge>
        <com.google.android.material.textfield.TextInputLayout
            style="@style/CrudDiary_TextInputLayout"
            android:id="@+id/date_layout"
            app:endIconMode="custom"
            app:endIconDrawable="@drawable/calendarSelectIcon"
            app:endIconContentDescription="@{iconDescription}"
            android:hint="@{hintText}">
            <com.google.android.material.textfield.TextInputEditText
                android:id="@+id/date"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:focusable="false"
                android:inputType="none"
                android:hint="@{hintText}"
                android:text="@={viewModelValue}"/>
        </com.google.android.material.textfield.TextInputLayout>
    </merge>
</layout>

13.9 spinner_generic.xml

The following code snippet is a combination of a regular TextInputEditText field with a Spinner element that together behave as a drop down box. Since the user’s input is stored in the TextInputEditText field we can use our form field validation mechanism as with any regular text field.

Listing 85: spinner_generic.xml
<?xml version="1.0" encoding="utf-8"?>
<layout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <!-- spinner solution see https://stackoverflow.com/questions/53198894/material-design-spinner-using-textinputlayout-outlinedbox-styling -->
    <data>
        <variable
            name="textHint"
            type="String" />
    </data>
    <merge>
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">
            <com.google.android.material.textfield.TextInputLayout
                android:id="@+id/spinner_input_layout_id"
                style="@style/CrudDiary_SpinnerTextInputLayout"
                app:endIconMode="custom"
                app:endIconDrawable="@drawable/baseline_keyboard_arrow_down_24"
                android:hint="@{textHint}">
                <com.google.android.material.textfield.TextInputEditText
                    android:id="@+id/spinner_text_id"
                    style="@style/CrudDiary_SpinnerTextInputEditText" />
            </com.google.android.material.textfield.TextInputLayout>
            <Spinner
                android:id="@+id/spinner_id"
                style="@style/CrudDiary_Spinner" />
        </LinearLayout>
    </merge>
</layout>

13.10 image_area.xml

The following layout snippet provides an area for the person’s image and an area for buttons to handle image input (Add image, Update image, Delete image). Visibility and labels are controlled dynamically depending on the user’s actual input.

Listing 86: image_area.xml
<?xml version="1.0" encoding="utf-8"?>
<layout
    xmlns:android="http://schemas.android.com/apk/res/android">
    <merge>
    <LinearLayout
        android:id="@+id/image_view_container"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:visibility="gone"
        android:orientation="vertical">
        <ImageView
            android:id="@+id/imageView"
            android:contentDescription="@string/item_image_description"
            style="@style/CrudDiary_ImageView_Items" />
    </LinearLayout>
    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:divider="@drawable/button_space"
        android:orientation="horizontal"
        android:showDividers="middle">
        <com.google.android.material.button.MaterialButton
            android:id="@+id/buttonLoadPicture"
            style="@style/CrudDiary_Button.AddImage" />
        <com.google.android.material.button.MaterialButton
            android:id="@+id/buttonDefaultPicture"
            style="@style/CrudDiary_Button.DeleteImage" />
    </LinearLayout>
    </merge>
</layout>

With the above layout snippets in place we now define the actual layout for adding new persons:

13.11 fragment_add_person.xml

Listing 87: fragment_add_person.xml
<?xml version="1.0" encoding="utf-8"?>
<layout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    tools:context=".framgent.AddPersonFragment">
    <data>
        <variable
            name="viewModel"
            type="app.gedama.tutorial.cruddiary.viewmodel.AddPersonViewModel" />
    </data>
    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <androidx.appcompat.widget.LinearLayoutCompat
            style="@style/CrudDiary_LinearLayoutCompat">
            <include
                android:id="@+id/name_text_field_binding"
                app:viewModelValue="@={viewModel.newPerson.name}"
                layout="@layout/text_input_name" />
            <include
                android:id="@+id/birthdate_text_field_binding"
                app:hintText="@{@string/person_birthdate_label}"
                app:viewModelValue="@={viewModel.newPerson.birthDateAsString}"
                layout="@layout/text_input_date" />
            <include
                android:id="@+id/spinner_genders_binding"
                app:textHint="@{@string/person_gender_label}"
                layout="@layout/spinner_generic" />
            <include
                android:id="@+id/image_area_binding"
                layout="@layout/image_area" />
            <include
                app:viewModelFunc="@{ () -> viewModel.addPerson() }"
                android:id="@+id/crud_buttons"
                app:visibility="@{android.view.View.GONE}"
                layout="@layout/crud_buttons" />
        </androidx.appcompat.widget.LinearLayoutCompat>
    </ScrollView>
</layout>

As you can see, we have also reused the layout snippets text_input_name and crud_buttons: like genders, persons have a name, and we need the same buttons for the CRUD actions.

For image input, we use a very nice library called Image Picker that enables us to pick an image either from the device’s file storage or by using the camera app. To use this library, we have to enable the jitpack.io Maven repository in our gradle.settings file:

13.12 settings.gradle

Listing 88: settings.gradle
pluginManagement {
    repositories {
        google()
        mavenCentral()
        gradlePluginPortal()
    }
}
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
        maven { url "https://jitpack.io" }
    }
}
rootProject.name = "CrudDiary"
include ’:app’

Next we add the Image Picker library to our build.xml file:

13.13 build.xml (App)

Listing 89: build.xml
...
    // picasso is for correctly handling images and caching in recyclerviews
    implementation ’com.squareup.picasso:picasso:2.8’
    // image picking and cropping
    implementation ’com.github.dhaval2404:imagepicker:2.1’
    testImplementation ’junit:junit:4.13.2’
...
}

Because we need two additional form field validations for persons, let’s extend our existing Validation.kt class with the new variables birthdayValidation and genderValidation:

13.14 Validation.kt

Listing 90: Validation.kt
package app.gedama.tutorial.cruddiary.data
import app.gedama.tutorial.cruddiary.GlobalAppStore
import app.gedama.tutorial.cruddiary.R
class Validation {
    companion object {
        val nameValidation = { value: String? ->
            when {
                value.isNullOrBlank() -> getResourceText(R.string.validation_no_name)
                else -> null
            }
        }
        val birthdayValidation = { value: String? ->
            when {
                value.isNullOrBlank() -> getResourceText(R.string.validation_no_birthdate)
                else -> null
            }
        }
        val genderValidation = { value: String? ->
            when {
                value.equals(getResourceText(R.string.nothing_selected)) -> getResourceText(R.string.validation_no_gender)
                else -> null
            }
        }
        private fun getResourceText(id: Int) =
            GlobalAppStore.appCtx!!.resources.getText(id).toString()
    }
}

For entering date values, our tutorial app uses the Date Time Picker mechanism. We define a generic class that works for any fragment/view model:

13.15 LambdaDatePicker.kt

Listing 91: LambdaDatePicker.kt
package app.gedama.tutorial.cruddiary.ui
import android.app.DatePickerDialog
import android.app.TimePickerDialog
import android.content.Context
import android.widget.DatePicker
import android.widget.TimePicker
import java.util.*
class LambdaDatePicker(val context: Context, val showTimePicker: Boolean = false, val viewModelFunc: (Date) -> Unit) : DatePickerDialog.OnDateSetListener, TimePickerDialog.OnTimeSetListener {
    var myDay = 0
    var myMonth: Int = 0
    var myYear: Int = 0
    var myHour: Int = 0
    var myMinute: Int = 0
    override fun onDateSet(view: DatePicker?, year: Int, month: Int, dayOfMonth: Int) {
        myDay = dayOfMonth
        myYear = year
        myMonth = month
        val calendar = Calendar.getInstance()
        calendar.set(myYear,myMonth,myDay)
        if ( showTimePicker ) {
            val hour = calendar.get(Calendar.HOUR)
            val minute = calendar.get(Calendar.MINUTE)
            val timePickerDialog = TimePickerDialog(context, this, hour, minute, true)
            timePickerDialog.show()
        } else {
            val cal = Calendar.getInstance()
            cal.set(myYear, myMonth, myDay)
            val date: Date = cal.time
            viewModelFunc(date)
        }
    }
    override fun onTimeSet(view: TimePicker?, hourOfDay: Int, minute: Int) {
        myHour = hourOfDay
        myMinute = minute
        val calendar = Calendar.getInstance()
        calendar.set(myYear,myMonth,myDay,myHour,myMinute)
        val date: Date = calendar.time
        viewModelFunc(date)
    }
}

As you can see, the class has a lambda parameter viewModelFunc in its constructor: This view model method gets called when the user has selected a date (and opional a time).

We need image and drop down box handling in several fragments, therefore we extract the reusable parts to our BaseFragment class:

13.16 BaseFragment.kt

Listing 92: BaseFragment.kt
package app.gedama.tutorial.cruddiary.fragment
import android.app.Activity
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.util.Log
import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Button
import android.widget.CheckBox
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.Spinner
import android.widget.TableRow
import android.widget.Toast
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.core.graphics.drawable.toBitmap
import androidx.core.view.isInvisible
import app.gedama.tutorial.cruddiary.R
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.lifecycle.LiveData
import androidx.lifecycle.lifecycleScope
import app.gedama.tutorial.cruddiary.GlobalAppStore
import app.gedama.tutorial.cruddiary.data.Spinnable
import app.gedama.tutorial.cruddiary.ui.formfields.FormFieldText
import app.gedama.tutorial.cruddiary.ui.formfields.validate
import app.gedama.tutorial.cruddiary.viewmodel.BaseViewModel
import com.google.android.material.button.MaterialButton
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.textfield.TextInputEditText
import kotlinx.coroutines.launch
import java.io.File
import com.github.dhaval2404.imagepicker.ImagePicker
abstract class BaseFragment: Fragment() {
    val TAG = this::class.simpleName
    fun saveToUpdateButton(saveButton: Button) {
        saveButton.text = resources.getString(R.string.update_button)
    }
    fun setDeactivateButton(deactivateButton: MaterialButton, viewModelFunc: () -> Unit ) {
        deactivateButton.setOnClickListener {
            val builder = AlertDialog.Builder(this.requireContext())
            builder.setTitle(resources.getText(R.string.inactivity_safety_dialog_title))
            builder.setMessage(resources.getText(R.string.inactivity_safety_check))
            builder.setPositiveButton(R.string.yes) { _, _ ->
                viewModelFunc()
            }
            builder.setNegativeButton(R.string.no) { _, _ ->
                // do nothing, but we need this statement to show the No-Button
            }
            builder.show()
        }
    }
    fun setInactivityButton(inactive: Boolean, deactivateButton: MaterialButton, checkBoxInactive: CheckBox) {
        if ( inactive ) {
            deactivateButton.visibility = View.GONE
            checkBoxInactive.isVisible = true
        }
    }
    fun activateAddFAB(viewModelFunc: () -> Unit ) {
        val fab = this.requireActivity().findViewById<FloatingActionButton>(R.id.add_floating_action_button)
        fab.isVisible = true
        fab.setOnClickListener { viewModelFunc() }
    }
    fun deactivateAddFAB() {
        val fab = this.requireActivity().findViewById<FloatingActionButton>(R.id.add_floating_action_button)
        fab.isVisible = false
    }
    fun formValidation(actionButton: Button, formFields: List<FormFieldText>, viewModel: BaseViewModel) = lifecycleScope.launch {
        actionButton.isEnabled = false
        // formFields.disable()
        if (formFields.validate(validateAll = true)) {
            viewModel.updateEntryAfterValidation()
            viewModel.onValidatedAndUpdated()
        } else {
            viewModel._deactivateSuccess.value = false
        }
        // formFields.enable()
        actionButton.isEnabled = true
    }
    fun initMaterialSpinnerData(spinner: Spinner, textEdit: TextInputEditText, listOfItems: LiveData<List<Spinnable>>, viewModelFunc: (item: Spinnable) -> Unit ) {
        textEdit.setOnClickListener {
            spinner.performClick()
        }
        val allItems = context?.let {
            ArrayAdapter<Any>(it, android.R.layout.simple_dropdown_item_1line)
        }
        listOfItems
            .observe(viewLifecycleOwner, { items ->
                items?.forEach {
                    allItems?.add(it)
                }
            })
        spinner.adapter = allItems
        spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
            override fun onItemSelected(
                parent: AdapterView<*>?,
                view: View?,
                position: Int,
                id: Long
            ) {
                val selectedItem = parent!!.adapter.getItem(position) as Spinnable
                textEdit.setText(selectedItem.itemName())
                viewModelFunc(selectedItem)
            }
            override fun onNothingSelected(parent: AdapterView<*>?) {
                TODO("Not yet implemented")
            }
        }
    }
    // from https://github.com/Dhaval2404/ImagePicker
    fun initImagePicking(iv: ImageView, ib: Button, viewModel: BaseViewModel, standardImageButton: Button? = null,
                         maxSize: Int = 1024, maxWidth: Int = 1080, maxHeight: Int = 1080, cropX: Float = 1F, cropY: Float = 1F) {
        val startForProfileImageResult =
            registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
                val resultCode = result.resultCode
                val data = result.data
                if (resultCode == Activity.RESULT_OK) {
                    //Image Uri will not be null for RESULT_OK
                    val fileUri = data?.data!!
                    iv.setImageURI(fileUri)
                    Log.d(TAG,"URI of selected image: $fileUri")
                    val bitmap = iv.drawable.toBitmap()
                    viewModel.imageBitmap = bitmap
                    viewModel.defaultImage = false
                    ib.text = resources.getText(R.string.change_image_button)
                    var parentView = iv.parent
                    if ( parentView is TableRow) {
                        parentView = iv.parent as TableRow
                        parentView.isInvisible = false
                    }
                    if ( parentView is LinearLayout) {
                        parentView = iv.parent as LinearLayout
                        parentView.isInvisible = false
                    }
                    standardImageButton?.let {
                        it.isInvisible = false
                    }
                } else if (resultCode == ImagePicker.RESULT_ERROR) {
                    Log.e(TAG, ImagePicker.getError(data))
                    Toast.makeText(this.requireContext(), ImagePicker.getError(data), Toast.LENGTH_SHORT).show()
                } else {
                    Toast.makeText(this.requireContext(), resources.getText(R.string.image_picking_canceled), Toast.LENGTH_SHORT).show()
                }
            }
        ib.setOnClickListener {
            ImagePicker.with(this)
                .compress(maxSize)         //Final image size will be less than 1 MB(Optional)
                .maxResultSize(maxWidth, maxHeight)  //Final image resolution will be less than 1080 x 1080(Optional)
                .crop(cropX,cropY)
                .createIntent { intent ->
                    startForProfileImageResult.launch(intent)
                }
        }
    }
    private fun loadImage(imagePath: String): Bitmap? {
        var bm: Bitmap? = null
        if (imagePath.isNotBlank()) {
            bm = getBitmap(imagePath)
        }
        return bm
    }
    private fun getBitmap(imagePath: String): Bitmap? {
        val file = File(GlobalAppStore.appCtx!!.filesDir, imagePath)
        return BitmapFactory.decodeFile(file.absolutePath)
    }
    fun setDeleteImageButton(deleteImageButton: MaterialButton, iv: ImageView, ivC: LinearLayout, buttonLoadPicture: MaterialButton, viewModel: BaseViewModel) {
        deleteImageButton.setOnClickListener {
            viewModel.imageBitmap = null
            iv.setImageBitmap(null)
            ivC.visibility = View.GONE
            buttonLoadPicture.text = resources.getText(R.string.add_image_button)
            buttonLoadPicture.setIconResource(R.drawable.addImageButtonIcon)
            deleteImageButton.visibility = View.GONE
            viewModel.defaultImage = true
        }
    }
    fun setChangeImageButton(view: View, imagePath: String, viewModel: BaseViewModel) {
        val iv = view.findViewById<ImageView>(R.id.imageView)
        val ivC = view.findViewById<LinearLayout>(R.id.image_view_container)
        val buttonLoadPicture = view.findViewById<MaterialButton>(R.id.buttonLoadPicture)
        val buttonDefaultPicture = view.findViewById<Button>(R.id.buttonDefaultPicture)
        viewModel.setOldImage( imagePath )
        val bm = loadImage( imagePath )
        if ( bm != null ) {
            iv.setImageBitmap(bm)
            ivC.isInvisible = false
            buttonLoadPicture.text = resources.getText(R.string.change_image_button)
            buttonLoadPicture.setIconResource(R.drawable.changeImageButtonIcon)
            buttonDefaultPicture.isInvisible = false
            viewModel.defaultImage = false
        }
    }
}

Method initMaterialSpinnerData() initializes a drop down box with the passed in database items (listOfItems) - here we use our previously defined Spinnable interface, so we can use this method for genders, persons, tags and so on. The lambda viewModelFunc is a method from the corresponding view model that gets called when an item in the drop down box is selected by the user.

Method initImagePicking() sets an onClickListener for our ’Add image’ button. Basically, when the button is clicked, the Image Picker logic gets triggered and the user either selects an image from the device’s file storage or takes a picture with the device’s camera app.

Methods setDeleteImageButton() and setChangeImageButton() are used to toggle the labels of image handling buttons and set the visibility of the image area where we display the person’s image.

Now it is time to present our AddPersonFragment class using lots of the stuff we have presented above:

13.17 AddPersonFragment.kt

Listing 93: AddPersonFragment.kt
package app.gedama.tutorial.cruddiary.fragment
import android.app.DatePickerDialog
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.findNavController
import app.gedama.tutorial.cruddiary.data.Validation
import app.gedama.tutorial.cruddiary.database.CrudDiaryDatabase
import app.gedama.tutorial.cruddiary.databinding.FragmentAddPersonBinding
import app.gedama.tutorial.cruddiary.ui.formfields.FormFieldText
import app.gedama.tutorial.cruddiary.viewmodel.AddPersonViewModelFactory
import app.gedama.tutorial.cruddiary.viewmodel.AddPersonViewModel
import com.google.android.material.snackbar.Snackbar
import java.text.DateFormat
import app.gedama.tutorial.cruddiary.R
import app.gedama.tutorial.cruddiary.data.Spinnable
import app.gedama.tutorial.cruddiary.ui.LambdaDatePicker
import java.util.*
class AddPersonFragment : BaseFragment() {
    private var _binding: FragmentAddPersonBinding? = null
    private val binding get() = _binding!!
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentAddPersonBinding.inflate(inflater, container, false)
        val view = binding.root
        val application = requireNotNull(this.activity).application
        val personDao = CrudDiaryDatabase.getInstance(application).personDao
        val viewModelFactory = AddPersonViewModelFactory(personDao)
        val viewModel = ViewModelProvider(this, viewModelFactory)[AddPersonViewModel::class.java]
        binding.viewModel = viewModel
        binding.lifecycleOwner = viewLifecycleOwner
        // form validation, see https://medium.com/@asissuthar/simplify-form-validation-using-kotlin-flow-on-android-16c718e3efaa
        val fieldName = FormFieldText(
            scope = lifecycleScope,
            textInputLayout = binding.nameTextFieldBinding.nameLayout,
            textInputEditText = binding.nameTextFieldBinding.name,
            Validation.nameValidation
        )
        val fieldBirthday = FormFieldText(
            scope = lifecycleScope,
            textInputLayout = binding.birthdateTextFieldBinding.dateLayout,
            textInputEditText = binding.birthdateTextFieldBinding.date,
            Validation.birthdayValidation
        )
        val fieldGender = FormFieldText(
            scope = lifecycleScope,
            textInputLayout = binding.spinnerGendersBinding.spinnerInputLayoutId,
            textInputEditText = binding.spinnerGendersBinding.spinnerTextId,
            Validation.genderValidation
        )
        val formFields = listOf(fieldName,fieldBirthday,fieldGender)
        initImagePicking(binding.imageAreaBinding.imageView, binding.imageAreaBinding.buttonLoadPicture,viewModel, binding.imageAreaBinding.buttonDefaultPicture)
        viewModel.navigateToList.observe(viewLifecycleOwner, { navigate ->
            if (navigate) {
                view.findNavController()
                    .navigate(R.id.action_addPersonFragment_to_personsListFragment)
                viewModel.onNavigatedToList()
            }
        })
        viewModel.newBirthdate.observe(viewLifecycleOwner, { item ->
            binding.birthdateTextFieldBinding.date.setText(DateFormat.getDateInstance(DateFormat.MEDIUM).format(item))
        })
        viewModel.doValidation.observe(viewLifecycleOwner, { doValidate ->
            if (doValidate) {
                formValidation(binding.crudButtons.actionButton, formFields, viewModel)
            }
        })
        viewModel.insertSuccess.observe(viewLifecycleOwner, { isSuccess ->
            if (isSuccess != null && !isSuccess ) {
                Snackbar.make(binding.crudButtons.actionButton,resources.getText(R.string.unique_person_violation),
                    Snackbar.LENGTH_INDEFINITE).show()
                Log.d(TAG,"There was a problem when inserting person data to the database ...")
            }
        })
        val handleIconClick = object : View.OnClickListener {
            override fun onClick(v: View?) {
                val picker = LambdaDatePicker(context!!, showTimePicker = false, { item: Date -> viewModel.updateBirthdate(item) })
                val currentBirthdate = Calendar.getInstance()
                val datePickerDialog =
                    DatePickerDialog(
                        context!!,
                        picker,
                        currentBirthdate.get(Calendar.YEAR),
                        currentBirthdate.get(
                            Calendar.MONTH
                        ),
                        currentBirthdate.get(Calendar.DAY_OF_MONTH)
                    )
                datePickerDialog.datePicker.maxDate = Date().time
                datePickerDialog.show()
            }
        }
        binding.birthdateTextFieldBinding.date.setOnClickListener(handleIconClick)
        binding.birthdateTextFieldBinding.dateLayout.setEndIconOnClickListener(handleIconClick)
        @Suppress("UNCHECKED_CAST")
        initMaterialSpinnerData(binding.spinnerGendersBinding.spinnerId,binding.spinnerGendersBinding.spinnerTextId,
            viewModel.allGenders as LiveData<List<Spinnable>>,{ item: Spinnable -> viewModel.updateGender(item)} )
        setDeleteImageButton(binding.imageAreaBinding.buttonDefaultPicture,binding.imageAreaBinding.imageView,
            binding.imageAreaBinding.imageViewContainer,binding.imageAreaBinding.buttonLoadPicture, viewModel)
        return view
    }
    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

********************* To Do: Explain code in above fragment

For getting from the persons list page to the ’Add person’ page in our UI, two more tasks have to be done. First, we have to extend our existing PersonsListFragment:

13.18 PersonsListFragment.kt

Listing 94: PersonsListFragment.kt
...
        adapter.adapterSize.observe( viewLifecycleOwner, { it ->
            if ( it == 0 ) {
                binding.noEntriesTextId.visibility = View.VISIBLE
                binding.personsList.visibility = View.GONE
            } else {
                binding.noEntriesTextId.visibility = View.GONE
                binding.personsList.visibility = View.VISIBLE
            }
        })
        viewModel.navigateToAddItem.observe(viewLifecycleOwner, Observer { navigate ->
            if ( navigate ) {
                val action = PersonsListFragmentDirections
                    .actionPersonsListFragmentToAddPersonFragment()
                this.findNavController().navigate(action)
                viewModel.onItemAddNavigated()
            }
        })
        return view
...

And second we have to extend our navigation graph with our new fragment. On this occasion we have provided some readable labels with the android:label-attribute so that our menu titles look nice:

13.19 nav_graph.xml

Listing 95: nav_graph.xml
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/nav_graph"
    app:startDestination="@id/homeFragment">
    <fragment
        android:id="@+id/homeFragment"
        android:name="app.gedama.tutorial.cruddiary.fragment.HomeFragment"
        android:label="@string/main_menu_title" >
        <action
            android:id="@+id/action_homeFragment_to_gendersListFragment"
            app:destination="@id/gendersListFragment" />
        <action
            android:id="@+id/action_homeFragment_to_personsListFragment"
            app:destination="@id/personsListFragment" />
    </fragment>
    <fragment
        android:id="@+id/gendersListFragment"
        android:name="app.gedama.tutorial.cruddiary.fragment.GendersListFragment"
        android:label="@string/genders_list_menu_title"
        tools:layout="@layout/fragment_genders_list" >
        <action
            android:id="@+id/action_gendersListFragment_to_homeFragment"
            app:destination="@id/homeFragment" />
        <action
            android:id="@+id/action_gendersListFragment_to_addGenderFragment"
            app:destination="@id/addGenderFragment" />
        <action
            android:id="@+id/action_gendersListFragment_to_updateGenderFragment"
            app:destination="@id/updateGenderFragment" />
    </fragment>
    <fragment
        android:id="@+id/addGenderFragment"
        android:name="app.gedama.tutorial.cruddiary.fragment.AddGenderFragment"
        android:label="@string/genders_add_menu_title" >
        <action
            android:id="@+id/action_addGenderFragment_to_gendersListFragment"
            app:destination="@id/gendersListFragment"
            app:popUpTo="@id/homeFragment" />
    </fragment>
    <fragment
        android:id="@+id/updateGenderFragment"
        android:name="app.gedama.tutorial.cruddiary.fragment.UpdateGenderFragment"
        android:label="@string/genders_update_menu_title" >
        <action
            android:id="@+id/action_updateGenderFragment_to_gendersListFragment"
            app:destination="@id/gendersListFragment"
            app:popUpTo="@id/homeFragment" />
        <argument
            android:name="genderId"
            app:argType="long" />
    </fragment>
    <fragment
        android:id="@+id/personsListFragment"
        android:name="app.gedama.tutorial.cruddiary.fragment.PersonsListFragment"
        android:label="@string/persons_list_menu_title"
        tools:layout="@layout/fragment_persons_list" >
        <action
            android:id="@+id/action_personsListFragment_to_homeFragment"
            app:destination="@id/homeFragment" />
        <action
            android:id="@+id/action_personsListFragment_to_addPersonFragment"
            app:destination="@id/addPersonFragment" />
    </fragment>
    <fragment
        android:id="@+id/addPersonFragment"
        android:name="app.gedama.tutorial.cruddiary.fragment.AddPersonFragment"
        android:label="@string/persons_add_menu_title" >
        <action
            android:id="@+id/action_addPersonFragment_to_personsListFragment"
            app:destination="@id/personsListFragment"
            app:popUpTo="@id/homeFragment" />
    </fragment>
</navigation>

With all these new files and extensions of existing files, the code should now rebuild and run on a (virtual) device. If you tap on the FAB at the bottom of the persons list, you get to the input mask for adding a new person (see Figure 13.1).

Date picker for setting the birthdate
Figure 13.2: Date picker for setting the birthdate

If you tap into the Birthdate input field, the date picker is opened and the user can set the date using this calendar widget (see Figure 13.2).

Drop down box for selecting the person’s gender
Figure 13.3: Drop down box for selecting the person’s gender

If you tap onto the Gender drop down box, you are provided with the genders list from the database (see Figure 13.3). If your provided all the necessary data and there was no database constraint violation (the person’s name has to be unique), the person gets added to the database and the list of persons shows the new person (see Figure 13.4).

After adding person Lucy
Figure 13.4: After adding person Lucy