How to Build a CRUD App With Kotlin and Android Studio

Chapter 10 Add Genders

Problems covered in this chapter
  • Form validations in the user interface

  • Interception of SQL constraint violations

  • Basic viewmodel class with useful methods for all other view models

  • Basic fragment class with useful methods for all further fragments

  • Reusable XML blocks for insertions and updates

  • Styles to unify the user interface

In the last chapter we covered the listing of all genders stored in the database. In this chapter we now want to enable the user to add new genders (e.g. ”inter” or ”open”). First, we extend our BaseViewModel class.

10.1 BaseViewModel.kt

Listing 42: BaseViewModel.kt
...
    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
...
    fun onItemEditNavigated() {
        _navigateToEditItem.value = null
    }
    fun onValidatedAndUpdated() {
        _doValidation.value = false
    }
    open fun updateEntryAfterValidation() {
        // to be overridden by all view models that do validation
    }
}

We add new LiveData-variables insertSuccess and deactivateSuccess: the first one is set to true whenever inserting a new gender into the database was successfull (there was no constraint violation), the second one is set to true whenever deactivating an existing gender was successfull (there were no validation errors). Another LiveData-variable, doValidation, is set to true when the user taps the Add-button to insert a new gender and we trigger form field validation to check if the user’s input data are valid. The method onValidatedAndUpdated() is called when when validation and an insert or update action in the database were both successfull. The method updateEntryAfterValidation() is defined as open and has to be implemented in our view model AddGenderViewModel.kt, which we cover next.

10.2 AddGenderViewModel.kt

Listing 43: AddGenderViewModel.kt
package app.gedama.tutorial.cruddiary.viewmodel
import android.database.sqlite.SQLiteConstraintException
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import app.gedama.tutorial.cruddiary.dao.GenderDao
import app.gedama.tutorial.cruddiary.data.Gender
import kotlinx.coroutines.launch
class AddGenderViewModel(val genderDao: GenderDao)  : BaseViewModel()  {
    var newGender = Gender()
    fun addGender() {
        _doValidation.value = true
    }
    override fun updateEntryAfterValidation() {
        viewModelScope.launch {
            try {
                genderDao.insert(newGender)
                _navigateToList.value = true
            } catch(sqlExc: SQLiteConstraintException) {
                _insertSuccess.value = false
            }
        }
    }
}
class AddGenderViewModelFactory(private val genderDao: GenderDao) : ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(AddGenderViewModel::class.java)) {
            return AddGenderViewModel(genderDao) 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 GenderDao object at initialization. The variable newGender holds our new gender object that gets inserted into the database after data validation and database constraint checking (the name of the gender has to be unique, see the definition of the Gender data class).

10.3 string.xml (English)

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

Listing 44: 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="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="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="unique_gender_violation">This gender name already exists!</string>
    <string name="validation_no_name">Name is required</string>
    <string name="validation_no_title">Title is required</string>
</resources>

Next we add the vector clip arts baseline_delete_24, baseline_do_not_disturb_alt_24 and a simple divider shape to our drawables folder:

10.4 divider.xml

Listing 45: divider.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <size android:height="1dp" />
    <solid android:color="#44000000"/>
</shape>

This shape is used to divide input fields in our user interface. Since we want a consistent look across our app, we add a new style definition to our themes.xml file:

10.5 themes.xml (Light-Mode)

Listing 46: 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>
    <!-- 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_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_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="ToolbarTheme" parent="ThemeOverlay.MaterialComponents.Dark.ActionBar">
        <item name="android:textColor">@color/black</item>
    </style>
</resources>

10.6 text_input_name.xml

Listing 47: text_input_name.xml
<?xml version="1.0" encoding="utf-8"?>
<layout
    xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable
            name="viewModelValue"
            type="String" />
    </data>
    <merge>
        <com.google.android.material.textfield.TextInputLayout
            android:id="@+id/nameLayout"
            style="@style/CrudDiary_TextInputLayout"
            android:hint="@string/name_label">
            <com.google.android.material.textfield.TextInputEditText
                android:id="@+id/name"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:inputType="textCapWords"
                android:text="@={viewModelValue}" />
        </com.google.android.material.textfield.TextInputLayout>
    </merge>
</layout>

10.7 crud_buttons.xml

Listing 48: crud_buttons.xml
<?xml version="1.0" encoding="utf-8"?>
<layout
    xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <import type="kotlin.jvm.functions.Function0" />
        <import type="kotlin.Unit" />
        <variable
            name="viewModelFunc"
            type="Function0&lt;Unit>" />
        <variable
            name="visibility"
            type="int" />
    </data>
    <merge>
        <com.google.android.material.button.MaterialButton
            android:id="@+id/action_button"
            android:onClick="@{() -> viewModelFunc.invoke()}"
            style="@style/CrudDiary_Button.SaveItem" />
        <com.google.android.material.button.MaterialButton
            android:id="@+id/deactivate_button"
            android:visibility="@{visibility}"
            style="@style/CrudDiary_Button.DeactivateItem" />
    </merge>
</layout>

10.8 fragment_add_gender.xml

Listing 49: fragment_add_gender.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="app.gedama.tutorial.cruddiary.framgent.AddGenderFragment">
    <data>
        <variable
            name="viewModel"
            type="app.gedama.tutorial.cruddiary.viewmodel.AddGenderViewModel" />
    </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.newGender.name}"
                layout="@layout/text_input_name" />
            <include
                app:viewModelFunc="@{ () -> viewModel.addGender() }"
                android:id="@+id/crud_buttons"
                app:visibility="@{android.view.View.GONE}"
                layout="@layout/crud_buttons" />
        </androidx.appcompat.widget.LinearLayoutCompat>
    </ScrollView>
</layout>

In this chapter, we start form validiation. For this purpose we use a solution by Ashish Suthar as described in his online article Simplify Form Validation using Kotlin Flow on Android. Since this approach uses FlowBinding, we need to update our build.xml file:

10.9 build.xml (App)

Listing 50: build.xml
...
    implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
    // used for form validation, see https://medium.com/@asissuthar/simplify-form-validation-using-kotlin-flow-on-android-16c718e3efaa
    implementation ’io.github.reactivecircus.flowbinding:flowbinding-android:1.2.0’
    testImplementation ’junit:junit:4.13.2’
...

We only slightly modify the two class files FormField.kt and FormFieldText.kt for our purposes and save them in our ui package folder:

10.10 FormField.kt

Listing 51: FormField.kt
package app.gedama.tutorial.cruddiary.ui.formfields
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
abstract class FormField<T> {
    protected val _state = MutableStateFlow<T?>(null)
    // state is StateFlow. It will be helpful for collecting any change in current value.
    val state = _state.asStateFlow()
    protected val _isValid = MutableStateFlow(true)
    // isValid is StateFlow. It will be helpful for collecting any change in validation process.
    val isValid = _isValid.asStateFlow()
    // We will call validate method, when we want to perform validation.
    abstract suspend fun validate(focusIfError: Boolean = true): Boolean
    open fun clearError() {}
    open fun clearFocus() {}
    open fun disable() {}
    open fun enable() {}
}
// Validate extension function for collection of FormFields. It will execute validate method of each field and return boolean result.
suspend fun Collection<FormField<*>>.validate(validateAll: Boolean = false) = coroutineScope {
    if (validateAll) {
        map { formField -> async { formField.validate(focusIfError = false) } }.awaitAll().all { result -> result }
    } else {
        all { formField -> formField.validate() }
    }
}
fun Collection<FormField<*>>.clearError() {
    forEach { formField -> formField.clearError() }
}
fun Collection<FormField<*>>.clearFocus() {
    forEach { formField -> formField.clearFocus() }
}
fun Collection<FormField<*>>.disable() {
    forEach { formField -> formField.disable() }
}
fun Collection<FormField<*>>.enable() {
    forEach { formField -> formField.enable() }
}

10.11 FormFieldText.kt

Listing 52: FormFieldText.kt
package app.gedama.tutorial.cruddiary.ui.formfields
import android.util.Log
import androidx.core.view.isVisible
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import reactivecircus.flowbinding.android.widget.textChanges
class FormFieldText(
    scope: CoroutineScope,
    private val textInputLayout: TextInputLayout,
    private val textInputEditText: TextInputEditText,
    private val validation: suspend (String?) -> String? = { null }
) : FormField<String>() {
    var isEnabled: Boolean
        get() = textInputLayout.isEnabled
        set(value) {
            textInputLayout.isEnabled = value
        }
    var isVisible: Boolean
        get() = textInputLayout.isVisible
        set(value) {
            textInputLayout.isVisible = value
        }
    var value: String?
        get() = _state.value
        set(value) {
            textInputEditText.setText(value)
        }
    init {
        textInputEditText.textChanges().skipInitialValue().onEach { text ->
            clearError()
            _state.update { text.toString() }
            Log.d("FormFieldText","Text of textInputEditText: ${text.toString()}")
        }.launchIn(scope)
    }
    override fun clearError() {
        if (textInputLayout.error != null) {
            textInputLayout.error = null
            textInputLayout.isErrorEnabled = false
        }
    }
    override fun clearFocus() {
        textInputEditText.clearFocus()
    }
    override fun disable() {
        isEnabled = false
    }
    override fun enable() {
        isEnabled = true
    }
    override suspend fun validate(focusIfError: Boolean): Boolean {
        if (!isVisible) {
            return true
        }
        val errorValue = try {
            validation(_state.value)
        } catch (error: Throwable) {
            error.message
        }
        val result = errorValue == null
        if (result) {
            clearError()
        } else {
            textInputLayout.error = errorValue
            if (focusIfError) {
                textInputEditText.requestFocus()
            }
        }
        _isValid.update { result }
        return result
    }
}

Instead of using explicit validation statements in the constructor of the FormFieldText variables (as done in Ashish Suthar’s code example) we define an additional Validation class with reusable validation variables. For the moment, there is only one variable called nameValidation which ensures that a name input field is not null or blank:

10.12 Validation.kt

Listing 53: 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
            }
        }
        private fun getResourceText(id: Int) =
            GlobalAppStore.appCtx!!.resources.getText(id).toString()
    }
}

Next, we add the method formValidation() in our BaseFragment class. We pass it the button that triggered the validation, a list of form fields that should be validated and the view model where we set LiveData variables depending on the outcome of our validation:

10.13 BaseFragment.kt

Listing 54: BaseFragment.kt
package app.gedama.tutorial.cruddiary.fragment
import android.widget.Button
import app.gedama.tutorial.cruddiary.R
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
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.floatingactionbutton.FloatingActionButton
import kotlinx.coroutines.launch
abstract class BaseFragment: Fragment() {
    val TAG = this::class.simpleName
    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
    }
}

Now it is time to present our AddGenderFragment class:

10.14 AddGenderFragment.kt

Listing 55: AddGenderFragment.kt
package app.gedama.tutorial.cruddiary.fragment
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.findNavController
import app.gedama.tutorial.cruddiary.database.CrudDiaryDatabase
import app.gedama.tutorial.cruddiary.databinding.FragmentAddGenderBinding
import app.gedama.tutorial.cruddiary.viewmodel.AddGenderViewModel
import app.gedama.tutorial.cruddiary.viewmodel.AddGenderViewModelFactory
import com.google.android.material.snackbar.Snackbar
import app.gedama.tutorial.cruddiary.R
import app.gedama.tutorial.cruddiary.data.Validation
import app.gedama.tutorial.cruddiary.ui.formfields.FormFieldText
class AddGenderFragment : BaseFragment() {
    private var _binding: FragmentAddGenderBinding? = null
    private val binding get() = _binding!!
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentAddGenderBinding.inflate(inflater, container, false)
        val view = binding.root
        val application = requireNotNull(this.activity).application
        val genderDao = CrudDiaryDatabase.getInstance(application).genderDao
        val viewModelFactory = AddGenderViewModelFactory(genderDao)
        val viewModel = ViewModelProvider(this, viewModelFactory)[AddGenderViewModel::class.java]
        binding.viewModel = viewModel
        binding.lifecycleOwner = viewLifecycleOwner
        val fieldName = FormFieldText(
            scope = lifecycleScope,
            textInputLayout = binding.nameTextFieldBinding.nameLayout,
            textInputEditText = binding.nameTextFieldBinding.name,
            Validation.nameValidation
        )
        val formFields = listOf(fieldName)
        viewModel.navigateToList.observe(viewLifecycleOwner, Observer { navigate ->
            if (navigate) {
                view.findNavController()
                    .navigate(R.id.action_addGenderFragment_to_gendersListFragment)
                viewModel.onNavigatedToList()
            }
        })
        viewModel.doValidation.observe(viewLifecycleOwner, Observer { doValidate ->
            if (doValidate) {
                formValidation(binding.crudButtons.actionButton, formFields, viewModel)
            }
        })
        viewModel.insertSuccess.observe(viewLifecycleOwner, Observer { isSuccess ->
            if (isSuccess != null && !isSuccess ) {
                Snackbar.make(binding.crudButtons.actionButton,resources.getText(R.string.unique_gender_violation),
                    Snackbar.LENGTH_INDEFINITE).show()
                Log.d(TAG,"There was a problem when inserting gender data to the database ...")
            }
        })
        return view
    }
    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

Most of the code should already be familiar from the previous chapter where we introduced the GendersListFragment (see Section 9.10). What’s new in this fragment is the definition of the fieldName variable: we specify the TextInputLayout and TextInputEditText fields to which our validation (nameValidation) applies. Then we define a list formFields containing FormFieldText variables (in our case there is only one input field), which we pass on in the BaseFragment’s formValidation method which is triggered as soon as the view model’s doValidation variable is set to true. If form field validation was successfull, our view model’s method updateEntryAfterValidation is called, where we try to insert the new gender value into the database. If there is no database constraint violation (the name of the gender has to be unique), the new value is inserted and we jump back to the genders list which now contains the new value. If there was a database constraint violation, the view model’s variable insertSuccess is set to false and the fragment’s observation code presents a snackbar at the bottom of the screen telling the user about the violation (see Figure 10.3). If form field validation fails, an error message is presented at the input field (see Figure 10.2).

However, for getting from the genders list page to the ’Add gender’ page in our UI, two more tasks have to be done. First, we have to extend our existing GendersListFragment:

10.15 GendersListFragment.kt

Listing 56: GendersListFragment.kt
...
        viewModel.genders.observe(viewLifecycleOwner, Observer {
            it?.let {
                adapter.submitList(it)
            }
        })
        viewModel.navigateToAddItem.observe(viewLifecycleOwner, Observer { navigate ->
            if (navigate) {
                val action = GendersListFragmentDirections
                    .actionGendersListFragmentToAddGenderFragment()
                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:

10.16 nav_graph.xml

Listing 57: 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" />
    </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" />
    </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>
</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 genders list, you get to the input mask for adding a new gender (see Figure 10.1).

Add new gender
Figure 10.1: Add new gender
Form field validation error
Figure 10.2: Form field validation error
Database constraint violation error
Figure 10.3: Database constraint violation error