How to Build a CRUD App With Kotlin and Android Studio

Chapter 16 Preferences: Main User and Deactivated Items

Problems covered in this chapter
  • Deactivate/delete records

  • Show/hide menu items

  • Dark mode

  • Defining attributes in our themes

  • Styles to unify the user interface

  • Reusable XML blocks for insertions and updates

In Chapter 7 we defined the toolbar menu. Until now, only the Help menu item is displayed. In this chapter, we activate the Preferences menu item so that users can set the main person and decide wheter inactivate items get displayed. First, let’s add method updateShowInactive() to our GlobalAppStore class:

16.1 GlobalAppStore.kt

Listing 103: GlobalAppStore.kt
package app.gedama.tutorial.cruddiary
import android.app.Application
import android.content.Context
import android.widget.Filterable
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
class GlobalAppStore: Application() {
    companion object {
        val TAG = this::class.simpleName
        @JvmField
        var mainUserId: Long? = null
        var appCtx: Context? = null
        private const val APP_PREFIX = "crud_diary_"
        const val APP_FILEPATH_PREFIX = APP_PREFIX + "files_"
        const val APP_DB_NAME = APP_PREFIX + "database"
        var _showInactive = MutableLiveData(false)
        val showInactive: LiveData<Boolean>
            get() = _showInactive
        var someFilterableAdapter: Filterable? = null
        fun updateShowInactive(showDeactivated: Boolean) {
            _showInactive.postValue(showDeactivated)
        }
    }
}

Next, we adapt our Person data class to implement the Spinner interface (as we have already done before with the Gender data class) because we want the user to select the persons in our database in a drop down box in the preferences input mask.

16.2 Person.kt

Listing 104: Person.kt
package app.gedama.tutorial.cruddiary.data
import androidx.room.*
import java.text.DateFormat
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
): Spinnable  {
    @Ignore
    var birthDateAsString: String = when (birthDate) {
        null -> ""
        else -> DateFormat.getDateInstance(DateFormat.MEDIUM).format(birthDate)
    }
    override fun itemName() = name
    override fun itemId() = personId
    override fun toString() = name
}

Next, we add a view model called UpdatePreferencesViewModel.kt for our preferences page:

16.3 UpdatePreferencesViewModel.kt

Listing 105: UpdatePreferencesViewModel.kt
package app.gedama.tutorial.cruddiary.viewmodel
import androidx.lifecycle.*
import app.gedama.tutorial.cruddiary.GlobalAppStore
import app.gedama.tutorial.cruddiary.dao.PreferencesDao
import app.gedama.tutorial.cruddiary.data.Person
import app.gedama.tutorial.cruddiary.data.Preferences
import app.gedama.tutorial.cruddiary.data.Spinnable
import kotlinx.coroutines.launch
class UpdatePreferencesViewModel(private val preferencesDao: PreferencesDao) : BaseViewModel() {
    val preferences = preferencesDao.getPreferences()
    val allPersons = preferencesDao.getAllPersons(0L)
    val allPersonsAndPreferencesLiveData: LiveData<Pair<List<Person>, Preferences>> =
        object: MediatorLiveData<Pair<List<Person>, Preferences>>() {
            var allP: List<Person>? = null
            var pref: Preferences? = null
            init {
                addSource( allPersons ) { allP ->
                    this.allP = allP
                    pref?.let { value = allP to it }
                }
                addSource(preferences) { pref ->
                    this.pref = pref
                    allP?.let { value = it to pref }
                }
            }
        }
    private val _navigateToHome = MutableLiveData(false)
    val navigateToHome: LiveData<Boolean>
        get() = _navigateToHome
    fun updatePreferences() {
        viewModelScope.launch {
            preferencesDao.update(preferences.value!!)
            GlobalAppStore.mainUserId = preferences.value!!.mainUserId
            GlobalAppStore.updateShowInactive(preferences.value!!.showInactive)
            _navigateToHome.value = true
        }
    }
    fun updateMainUser(person: Spinnable) {
        preferences.value!!.mainUserId = person.itemId()
    }
    fun onNavigatedToHome() {
        _navigateToHome.value = false
    }
}
class UpdatePreferencesViewModelFactory(private val preferencesDao: PreferencesDao) : ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(UpdatePreferencesViewModel::class.java)) {
            return UpdatePreferencesViewModel(preferencesDao) as T
        }
        throw java.lang.IllegalArgumentException("Unknown ViewModel")
    }
}

We define the two LiveData variables preferences and allPersons to hold the preferences and persons from the database. Variable allPersonsAndPreferencesLiveData is another LiveData variable which triggers its observer in the corresponding fragment whenever both variables preferences and persons are set. The rest of the view model should be pretty familiar to you. Notice however that in addition to updating the preferences in the database we set the user’s preferences in our global utitlity class GlobalAppStore.

Let’s add a couple of colors to our colors.xml file:

16.4 colors.xml

Listing 106: colors.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="purple_200">#FFBB86FC</color>
    <color name="purple_500">#FF6200EE</color>
    <color name="purple_700">#FF3700B3</color>
    <color name="teal_200">#FF03DAC5</color>
    <color name="teal_700">#FF018786</color>
    <color name="black">#FF000000</color>
    <color name="white">#FFFFFFFF</color>
    <color name="colorLightGrey" type="color">#FFD3D3D3</color>
    <color name="colorDarkGrey" type="color">#888888</color>
    <color name="colorVeryDarkGrey" type="color">#2b2b2b</color>
</resources>

Next, we extend our themes.xml files to use these new colors for acitve and inactive items in our lists and to add a new style for checkboxes:

16.5 themes.xml (Light-Mode)

Listing 107: 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_GenericCheckbox" 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>
    </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>

We use different colors for the dark mode:

Listing 108: themes.xml (Dark-Mode)
<resources xmlns:tools="http://schemas.android.com/tools">
    <!-- Base application theme. -->
    <style name="Theme.CrudDiary" parent="Theme.MaterialComponents.DayNight.NoActionBar">
        <!-- Primary brand color. -->
        <item name="colorPrimary">@color/purple_200</item>
        <item name="colorPrimaryVariant">@color/purple_700</item>
        <item name="colorOnPrimary">@color/black</item>
        <!-- Secondary brand color. -->
        <item name="colorSecondary">@color/teal_200</item>
        <item name="colorSecondaryVariant">@color/teal_200</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/teal_700</item>
        <item name="inactive_background_color">@color/colorVeryDarkGrey</item>
        <item name="main_user_color">@color/purple_200</item>
        <item name="regular_user_color">@color/black</item>
        <item name="fab_tint_background">@color/teal_200</item>
        <item name="fab_border_color">@color/white</item>
        <item name="fab_icon_color">@color/black</item>
    </style>
    <style name="ToolbarTheme" parent="ThemeOverlay.MaterialComponents.Dark.ActionBar">
        <item name="android:textColor">@color/white</item>
    </style>
</resources>

Now we add another reusable layout snippet called checkbox_generic.xml:

16.6 checkbox_generic.xml

Listing 109: checkbox_generic.xml
<?xml version="1.0" encoding="utf-8"?>
<layout
    xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable
            name="viewModelValue"
            type="boolean" />
        <variable
            name="hintText"
            type="String" />
    </data>
    <merge>
        <com.google.android.material.checkbox.MaterialCheckBox
            android:id="@+id/checkBox"
            style="@style/CrudDiary_GenericCheckbox"
            android:text="@{hintText}"
            android:checked="@={viewModelValue}" />
    </merge>
</layout>

Next, we add a couple of strings to our string.xml files:

16.7 string.xml (English)

Listing 110: 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="persons_update_menu_title">Update person</string>
    <string name="tags_list_menu_title">Tags</string>
    <string name="tags_add_menu_title">Add tag</string>
    <string name="tags_edit_menu_title">Edit tag</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="save_preferences_button">Save preferences</string>
    <string name="unique_gender_violation">This gender name already exists!</string>
    <string name="unique_person_violation">This person already exists!</string>
    <string name="unique_tag_violation">This tag 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>
    <string name="title_label">Title</string>
    <string name="show_inactive_label">List inactive items</string>
    <string name="person_main_user_label">Main user</string>
</resources>

With all these additions we now define our preferences layout file:

16.8 fragment_update_preferences.xml

Listing 111: fragment_update_preferences.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.EditPreferencesFragment">
    <data>
        <variable
            name="viewModel"
            type="app.gedama.tutorial.cruddiary.viewmodel.UpdatePreferencesViewModel" />
    </data>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">
        <androidx.appcompat.widget.LinearLayoutCompat
            style="@style/CrudDiary_LinearLayoutCompat">
            <include
                android:id="@+id/spinner_main_user_binding"
                app:textHint="@{@string/person_main_user_label}"
                layout="@layout/spinner_generic" />
            <include
                android:id="@+id/inactive_check_box_binding"
                app:viewModelValue="@={viewModel.preferences.showInactive}"
                app:hintText="@{@string/show_inactive_label}"
                layout="@layout/checkbox_generic" />
            <Button
                style="@style/CrudDiary_Button.SaveItem"
                android:text="@string/save_preferences_button"
                android:onClick="@{ () -> viewModel.updatePreferences() }"/>
        </androidx.appcompat.widget.LinearLayoutCompat>
        </LinearLayout>
</layout>

Now we define our fragment for setting the preferences:

16.9 UpdatePreferencesFragment.kt

Listing 112: UpdatePreferencesFragment.kt
package app.gedama.tutorial.cruddiary.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.*
import androidx.navigation.findNavController
import app.gedama.tutorial.cruddiary.R
import app.gedama.tutorial.cruddiary.data.Spinnable
import app.gedama.tutorial.cruddiary.database.CrudDiaryDatabase
import app.gedama.tutorial.cruddiary.databinding.FragmentUpdatePreferencesBinding
import app.gedama.tutorial.cruddiary.viewmodel.UpdatePreferencesViewModel
import app.gedama.tutorial.cruddiary.viewmodel.UpdatePreferencesViewModelFactory
class UpdatePreferencesFragment: BaseFragment() {
    private var _binding: FragmentUpdatePreferencesBinding? = null
    private val binding get() = _binding!!
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentUpdatePreferencesBinding.inflate(inflater, container, false)
        val view = binding.root
        val application = requireNotNull(this.activity).application
        val preferencesDao = CrudDiaryDatabase.getInstance(application).preferencesDao
        val viewModelFactory = UpdatePreferencesViewModelFactory(preferencesDao)
        val viewModel = ViewModelProvider(this, viewModelFactory)[UpdatePreferencesViewModel::class.java]
        binding.viewModel = viewModel
        binding.lifecycleOwner = viewLifecycleOwner
        viewModel.navigateToHome.observe(viewLifecycleOwner, Observer { navigate ->
            if (navigate) {
                view.findNavController()
                    .navigate(R.id.action_updatePreferencesFragment_to_homeFragment)
                viewModel.onNavigatedToHome()
                // this.requireActivity().supportFragmentManager.popBackStackImmediate()
            }
        })
        @Suppress("UNCHECKED_CAST")
        initMaterialSpinnerData(binding.spinnerMainUserBinding.spinnerId,binding.spinnerMainUserBinding.spinnerTextId,
            viewModel.allPersons as LiveData<List<Spinnable>>,{ item: Spinnable -> viewModel.updateMainUser(item)} )
        viewModel.allPersonsAndPreferencesLiveData.observe(viewLifecycleOwner, Observer { (allP,pref) ->
            val personsSpinner = binding.spinnerMainUserBinding.spinnerId
            val mainUserId = pref.mainUserId ?: 1L
            val mainUser = allP.find{ it.personId == mainUserId }
            val obPos = allP.indexOf(mainUser)
            personsSpinner.setSelection(obPos)
        } )
        return view
    }
    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

And of course we have to include our new fragment in our navigation graph:

16.10 nav_graph.xml

Listing 113: 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" />
        <action
            android:id="@+id/action_homeFragment_to_tagsListFragment"
            app:destination="@id/tagsListFragment" />
    </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" />
        <action
            android:id="@+id/action_personsListFragment_to_updatePersonFragment"
            app:destination="@id/updatePersonFragment" />
    </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>
    <fragment
        android:id="@+id/updatePersonFragment"
        android:name="app.gedama.tutorial.cruddiary.fragment.UpdatePersonFragment"
        android:label="@string/persons_update_menu_title" >
        <action
            android:id="@+id/action_updatePersonFragment_to_personsListFragment"
            app:destination="@id/personsListFragment"
            app:popUpTo="@id/homeFragment" />
        <argument
            android:name="personId"
            app:argType="long" />
    </fragment>
    <fragment
        android:id="@+id/tagsListFragment"
        android:name="app.gedama.tutorial.cruddiary.fragment.TagsListFragment"
        android:label="@string/tags_list_menu_title" >
        <action
            android:id="@+id/action_tagsListFragment_to_homeFragment"
            app:destination="@id/homeFragment" />
        <action
            android:id="@+id/action_tagsListFragment_to_addTagFragment"
            app:destination="@id/addTagFragment" />
        <action
            android:id="@+id/action_tagsListFragment_to_updateTagFragment"
            app:destination="@id/updateTagFragment" />
    </fragment>
    <fragment
        android:id="@+id/addTagFragment"
        android:name="app.gedama.tutorial.cruddiary.fragment.AddTagFragment"
        android:label="@string/tags_add_menu_title" >
        <action
            android:id="@+id/action_addTagFragment_to_tagsListFragment"
            app:destination="@id/tagsListFragment"
            app:popUpTo="@id/homeFragment" />
    </fragment>
    <fragment
        android:id="@+id/updateTagFragment"
        android:name="app.gedama.tutorial.cruddiary.fragment.UpdateTagFragment"
        android:label="@string/tags_edit_menu_title" >
        <action
            android:id="@+id/action_updateTagFragment_to_tagsListFragment"
            app:destination="@id/tagsListFragment"
            app:popUpTo="@id/homeFragment" />
        <argument
            android:name="tagId"
            app:argType="long" />
    </fragment>
    <fragment
        android:id="@+id/updatePreferencesFragment"
        android:name="app.gedama.tutorial.cruddiary.fragment.UpdatePreferencesFragment"
        android:label="@string/preferences_menu_title" >
        <action
            android:id="@+id/action_updatePreferencesFragment_to_homeFragment"
            app:destination="@id/homeFragment" />
    </fragment>
</navigation>

Finally, we have to activate the visibility of our Preferences menu item in the toolbar. We do this in our MainActivity file within our onCreateOptionsMenu() method:

16.11 MainActivity.kt

Listing 114: MainActivity.kt
...
    override fun onCreateOptionsMenu(menu: Menu?): Boolean {
        menuInflater.inflate(R.menu.menu_toolbar, menu)
        val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
        val fragment = navHostFragment.childFragmentManager.fragments[0]
        val prefMenuItem = menu!!.findItem(R.id.updatePreferencesFragment)
        val helpMenuItem = menu.findItem(R.id.helpFragment)
        if ( fragment is HomeFragment) {
            Log.d(TAG,"We are in HomeFragment")
            prefMenuItem.isVisible = true
            helpMenuItem.isVisible = true
        } else {
            Log.d(TAG,"We are not in HomeFragment")
            prefMenuItem.isVisible = false
            helpMenuItem.isVisible = false
        }
        return super.onCreateOptionsMenu(menu)
    }
...

Next, we extend our home fragment. First we define a HomeViewModel:

16.12 HomeViewModel.kt

Listing 115: HomeViewModel.kt
package app.gedama.tutorial.cruddiary.viewmodel
import androidx.lifecycle.*
import app.gedama.tutorial.cruddiary.dao.PreferencesDao
class HomeViewModel(preferencesDao: PreferencesDao) : BaseViewModel() {
    val preferences = preferencesDao.getPreferences()
}
class HomeViewModelFactory(private val preferencesDao: PreferencesDao): ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(HomeViewModel::class.java)) {
            return HomeViewModel(preferencesDao) as T
        }
        throw java.lang.IllegalArgumentException("Unknown ViewModel")
    }
}

Then we update the layout of our home screen:

16.13 fragment_home.xml

Listing 116: fragment_home.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"
    tools:context="app.gedama.tutorial.cruddiary.fragment.HomeFragment">
    <data>
        <variable
            name="viewModel"
            type="app.gedama.tutorial.cruddiary.viewmodel.HomeViewModel" />
    </data>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        <TextView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:text="Home" />
    </LinearLayout>
</layout>

And finally, we adapt our HomeFragment:

16.14 FragmentHome.kt

Listing 117: FragmentHome.kt
package app.gedama.tutorial.cruddiary.fragment
import android.os.Bundle
import android.util.Log
import android.view.*
import androidx.lifecycle.ViewModelProvider
import app.gedama.tutorial.cruddiary.GlobalAppStore
import app.gedama.tutorial.cruddiary.database.CrudDiaryDatabase
import app.gedama.tutorial.cruddiary.databinding.FragmentHomeBinding
import app.gedama.tutorial.cruddiary.viewmodel.HomeViewModel
import app.gedama.tutorial.cruddiary.viewmodel.HomeViewModelFactory
class HomeFragment: BaseFragment() {
    private var _binding: FragmentHomeBinding? = null
    private val binding get() = _binding!!
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentHomeBinding.inflate(inflater, container, false)
        val view = binding.root
        val application = requireNotNull(this.activity).application
        val preferencesDao = CrudDiaryDatabase.getInstance(application).preferencesDao
        val viewModelFactory = HomeViewModelFactory(preferencesDao)
        val viewModel = ViewModelProvider(this, viewModelFactory)[HomeViewModel::class.java]
        binding.viewModel = viewModel
        binding.lifecycleOwner = viewLifecycleOwner
        viewModel.preferences.observe(viewLifecycleOwner, {
            if (it != null ) {
                // neccesary because at the first run these data aren’t available yet
                val mainUserId = it.mainUserId ?: 1L
                val showInactive = it.showInactive ?: false
                Log.d("HomeViewModel", "Main user ID: $mainUserId")
                GlobalAppStore.mainUserId = mainUserId
                GlobalAppStore.updateShowInactive(showInactive)
                Log.d("HomeViewModel", "Show inactive: $showInactive")
            }
        })
        return view
    }
    override fun onDestroyView() {
        this.requireActivity().invalidateOptionsMenu()
        super.onDestroyView()
        _binding = null
    }
}

When finished, you will see the Preferences menu item in the start screen of the app (see Figure 16.1).

The preferences menu item is now visible on the start screen
Figure 16.1: The preferences menu item is now visible on the start screen

When you select the Preferences menu item you get to the screen for setting the preferences (see Figure 16.2).

Input mask for setting the preferences
Figure 16.2: Input mask for setting the preferences

Let’s select Lucy as the main user. After clicking the ’Save preferences’ button tap on the main menu and select the Person menu item (see Figure 16.3): As you can see, there’s a differently colored border around Lucy’s card indicating that she is the main user.

Lucy is the main user
Figure 16.3: Lucy is the main user

Next, click on Fred and deactivate him. When you return to the persons list, Fred is gone. (see Figure 16.4).

After deactivating Fred he is no longer visible in the persons list
Figure 16.4: After deactivating Fred he is no longer visible in the persons list

Now let’s return to the Preferences menu item and check the ’List inactive items’ checkbox. After returning to the persons list, you will see Fred again (see Figure 16.5). To indicate that he is a deactivated item, the card is displayed with a grey background.

Fred is shown as a deactivated person
Figure 16.5: Fred is shown as a deactivated person

Deactivation also works for tags and genders - give it a try! And switch your device to dark mode to see its different color scheme (see Figure 16.6).

Persons list in dark mode
Figure 16.6: Persons list in dark mode