How to Build a CRUD App With Kotlin and Android Studio

Chapter 11 Update Genders

In the last chapter we covered the Create-part of our CRUD app for genders. In this chapter we now want to enable the user to update existing genders (for example to correct a typo). Let’s start with a view model for our update page:

11.1 UpdateGenderViewModel.kt

Listing 58: UpdateGenderViewModel.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 kotlinx.coroutines.launch
class UpdateGenderViewModel(genderId: Long, val genderDao: GenderDao)  : BaseViewModel() {
var gender = genderDao.get(genderId)
    fun updateGender() {
        _doValidation.value = true
    }
    override fun updateEntryAfterValidation() {
        viewModelScope.launch {
            if ( deactivateSuccess.value!! || gender.value!!.inactive ) {
                gender.value!!.inactive = true
            } else {
                gender.value!!.inactive = false
            }
            try {
                genderDao.update(gender.value!!)
                _navigateToList.value = true
            } catch(sqlExc: SQLiteConstraintException) {
                _deactivateSuccess.value = false
                _insertSuccess.value = false
            }
        }
    }
    fun deactivateGender() {
        _deactivateSuccess.value = true
        _doValidation.value = true
    }
}
class UpdateGenderViewModelFactory(private val genderId: Long, private val genderDao: GenderDao) : ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(UpdateGenderViewModel::class.java)) {
            return UpdateGenderViewModel(genderId, genderDao) as T
        }
        throw java.lang.IllegalArgumentException("Unknown ViewModel")
    }
}

As with our view models from the previous chapters, we have a separate Factory-class which is passed a GenderDao object at initialization. However, in this case we additionally pass the ID of the gender item that gets updated. The variable gender gets our existing gender object from the database. As with our view model for adding new gender items, we have to validate the user input and check for database constraint violations.

11.2 string.xml (English)

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

Listing 59: 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="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="validation_no_name">Name is required</string>
    <string name="validation_no_title">Title is required</string>
    <string name="inactive_label">Inactive</string>
</resources>

Since we want a consistent look across our app, we add a new style definition for inactive items to our themes.xml file:

11.3 themes.xml (Light-Mode)

Listing 60: themes.xml (Light-Mode)
...
    <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_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>
...

11.4 checkbox_inactive.xml

Listing 61: checkbox_inactive.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>

11.5 fragment_update_gender.xml

Listing 62: fragment_update_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=".framgent.UpdateGenderFragment">
    <data>
        <variable
            name="viewModel"
            type="app.gedama.tutorial.cruddiary.viewmodel.UpdateGenderViewModel" />
    </data>
    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <androidx.appcompat.widget.LinearLayoutCompat
            style="@style/CrudDiary_LinearLayoutCompat">
            <include
                android:id="@+id/title_text_field_binding"
                app:viewModelValue="@={viewModel.gender.name}"
                layout="@layout/text_input_name" />
            <include
                android:id="@+id/check_box_binding"
                app:viewModelValue="@={viewModel.gender.inactive}"
                layout="@layout/checkbox_inactive" />
            <include
                app:viewModelFunc="@{ () -> viewModel.updateGender() }"
                android:id="@+id/crud_buttons"
                layout="@layout/crud_buttons" />
        </androidx.appcompat.widget.LinearLayoutCompat>
    </ScrollView>
</layout>

Next we have to extend our navigation graph with our new fragment:

11.6 nav_graph.xml

Listing 63: 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" />
        <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>
</navigation>

Note that we added the argument genderId to our new fragment because we have to pass the ID so we know which gender item gets updated. Next we add new methods to our BaseFragment class. Method saveToUpdateButton() simply changes the text of our CRUD-button in the UI from ’save’ to ’update’. Method setInactivityButton() is used to change the visibility of the inactivity button in the UI. And method setDeactivateButton() presents a safety check dialog to the user asking if she really wants to deactivate the given object.

11.7 BaseFragment.kt

Listing 64: BaseFragment.kt
package app.gedama.tutorial.cruddiary.fragment
import android.view.View
import android.widget.Button
import android.widget.CheckBox
import androidx.appcompat.app.AlertDialog
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.button.MaterialButton
import com.google.android.material.floatingactionbutton.FloatingActionButton
import kotlinx.coroutines.launch
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
    }
}

Now it is time to present our UpdateGenderFragment class:

11.8 UpdateGenderFragment.kt

Listing 65: UpdateGenderFragment.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.R
import app.gedama.tutorial.cruddiary.data.Validation
import app.gedama.tutorial.cruddiary.database.CrudDiaryDatabase
import app.gedama.tutorial.cruddiary.databinding.FragmentUpdateGenderBinding
import app.gedama.tutorial.cruddiary.ui.formfields.FormFieldText
import app.gedama.tutorial.cruddiary.viewmodel.UpdateGenderViewModel
import app.gedama.tutorial.cruddiary.viewmodel.UpdateGenderViewModelFactory
import com.google.android.material.snackbar.Snackbar
class UpdateGenderFragment: BaseFragment() {
    private var _binding: FragmentUpdateGenderBinding? = null
    private val binding get() = _binding!!
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentUpdateGenderBinding.inflate(inflater, container, false)
        val view = binding.root
        val vaccinationTypeId = UpdateGenderFragmentArgs.fromBundle(requireArguments()).genderId
        val application = requireNotNull(this.activity).application
        val genderDao = CrudDiaryDatabase.getInstance(application).genderDao
        val viewModelFactory = UpdateGenderViewModelFactory(vaccinationTypeId,genderDao)
        val viewModel = ViewModelProvider(this, viewModelFactory)[UpdateGenderViewModel::class.java]
        binding.viewModel = viewModel
        binding.lifecycleOwner = viewLifecycleOwner
        val fieldTitle = FormFieldText(
            scope = lifecycleScope,
            textInputLayout = binding.titleTextFieldBinding.nameLayout,
            textInputEditText = binding.titleTextFieldBinding.name,
            Validation.nameValidation
        )
        val formFields = listOf(fieldTitle)
        saveToUpdateButton(binding.crudButtons.actionButton)
        viewModel.navigateToList.observe(viewLifecycleOwner, Observer { navigate ->
            if (navigate) {
                view.findNavController()
                    .navigate(R.id.action_updateGenderFragment_to_gendersListFragment)
                viewModel.onNavigatedToList()
            }
        })
        viewModel.gender.observe(viewLifecycleOwner, Observer { item ->
            setInactivityButton(item.inactive,binding.crudButtons.deactivateButton,binding.checkBoxBinding.checkBoxInactive)
        })
        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 updating gender data in the database ...")
            }
        })
        setDeactivateButton(binding.crudButtons.deactivateButton, { viewModel.deactivateGender() } )
        return view
    }
    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

Once again, for getting from the genders list page to the ’Update gender’ page, we have to extend our existing GendersListFragment:

11.9 GendersListFragment.kt

Listing 66: GendersListFragment.kt
...
        viewModel.navigateToAddItem.observe(viewLifecycleOwner, Observer { navigate ->
            if (navigate) {
                val action = GendersListFragmentDirections
                    .actionGendersListFragmentToAddGenderFragment()
                this.findNavController().navigate(action)
                viewModel.onItemAddNavigated()
            }
        })
        viewModel.navigateToEditItem.observe(viewLifecycleOwner, Observer { genderId ->
            genderId?.let {
                val action = GendersListFragmentDirections
                    .actionGendersListFragmentToUpdateGenderFragment(genderId)
                this.findNavController().navigate(action)
                viewModel.onItemEditNavigated()
            }
        })
        return view
...

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 one of the items in the genders list, you get to the input mask for updating the selected gender (see Figure 11.1). If the validation of our updated gender was successful, the logic returns to the genders list and displays the updated gender value (see Figure 11.2).

Update existing gender value
Figure 11.1: Update existing gender value
After updating ’diverse’ to ’diverse gender’
Figure 11.2: After updating ’diverse’ to ’diverse gender’

Have you noticed the ’Deactivate’ button below the ’Save’ button? It already works, but please ignore this button for now since we have to tackle the settings menu first in a later chapter (see Chapter 16) so you can actually see the effects of deactivating database items!