How to Build a CRUD App With Kotlin and Android Studio

Chapter 21 Update Diary Entries

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

  • Basic fragment class with useful methods for all further fragments

  • Basic view model class with useful methods for all further view models

In this chapter we cover updating diary entries. In the last chapter we enabled the user to add a PDF to diary entries. These (optional) PDF files are stored in the app’s internal storage. In order to display the PDF contents of such a file, we need to first get the file’s URI. For this purpose we add a FileProvider to our AndroidManifest.xml file:

21.1 AndroidManifest.xml

Listing 147: AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/Theme.CrudDiary"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="${applicationId}.provider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/provider_paths" />
        </provider>
    </application>
</manifest>

As you can see in the above provider section, we also have to add a file called provider_paths.xml to our xml folder:

21.2 provider_paths.xml

Listing 148: provider_paths.xml
<?xml version="1.0" encoding="utf-8"?>
<paths>
    <cache-path
        name="cached_files"
        path="." />
    <files-path
        name="images"
        path="." />
</paths>

Next we have to extend our BaseViewModel.kt:

21.3 BaseViewModel.kt

Listing 149: BaseViewModel.kt
package app.gedama.tutorial.cruddiary.viewmodel
import android.content.Context
import android.graphics.Bitmap
import android.net.Uri
import android.provider.OpenableColumns
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
    private val _oldPdf = MutableLiveData<String>("")
    val oldPdf: LiveData<String>
        get() = _oldPdf
    var imageBitmap: Bitmap? = null
    var defaultImage = true
    var pdfFileName: String = ""
    var _pdfUri = MutableLiveData<Uri?>()
    val pdfUri: LiveData<Uri?>
        get() = _pdfUri
    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 setOldPdf(op: String) {
        _oldPdf.value = op
    }
    fun setPdfUri(newPdfUri: Uri?) {
        _pdfUri.value = newPdfUri
    }
    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
    }
    fun writePdf(filenameprefix: String): String {
        val currentTimeInMillis = System.currentTimeMillis()
        val fileName = GlobalAppStore.APP_FILEPATH_PREFIX  + filenameprefix + currentTimeInMillis + ".pdf"
        val context = GlobalAppStore.appCtx
        val inputStream = context!!.contentResolver.openInputStream(pdfUri.value!!)
        val fos = context.openFileOutput(fileName, Context.MODE_PRIVATE)
        fos.write(inputStream!!.readBytes())
        inputStream.close()
        return fileName
    }
    fun getFilenameFromUri(uri: Uri) : String {
        var fn = "file.pdf"
        GlobalAppStore.appCtx!!.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
            /*
             * Get the column indexes of the data in the Cursor,
             * move to the first row in the Cursor, get the data,
             * and display it.
             */
            val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
            val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
            cursor.moveToFirst()
            fn = cursor.getString(nameIndex)
            Log.d(TAG,"Real file name: ${fn}")
            Log.d(TAG,"File size: ${cursor.getLong(sizeIndex).toString()}")
        }
        // fn = File("" + Uri.parse(pdfUri.value!!.lastPathSegment)).name
        Log.d(TAG,"Obtained filename: $fn")
        return fn
    }
    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
    }
    fun checkNewPdfPath(filenameprefix: String): String? {
        var newValue: String? = null
        // handle several cases
        if ( oldPdf.value!!.isNotBlank() && pdfUri.value == null ) {
            // a former PDF was deleted
            deleteOldPdf(oldPdf.value!!)
            newValue = ""
        } else if ( oldPdf.value!!.isNotBlank() && pdfUri.value != null ) {
            // a former PDF was replaced by a new one
            deleteOldPdf(oldPdf.value!!)
            newValue = writePdf(filenameprefix)
        } else if ( oldPdf.value!!.isBlank() && pdfUri.value != null ) {
            // a new PDF was added
            newValue = writePdf(filenameprefix)
        }
        return newValue
    }
    fun deleteOldPdf(filename: String) {
        val context = GlobalAppStore.appCtx
        val file = File(context!!.filesDir, filename)
        val result = file.delete()
        Log.d(TAG, "Result from deleting $filename: $result")
    }
    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
    }
}

We also have to extend our BaseFragment.kt class:

21.4 BaseFragment.kt

Listing 150: BaseFragment.kt
package app.gedama.tutorial.cruddiary.fragment
import android.app.Activity
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Build
import android.speech.RecognizerIntent
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.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.core.content.FileProvider
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
import com.google.android.material.textfield.TextInputLayout
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
        }
    }
    protected fun initPDFFromStorage(pdfB: MaterialButton, pdfDeleteB: MaterialButton, pvC: LinearLayout, viewModel: BaseViewModel) {
        val selectPDFFromStorageResult = registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
            uri?.let {
                Log.d(TAG,"URI of selected PDF: $uri")
                viewModel.setPdfUri(uri)
                viewModel.pdfFileName = viewModel.getFilenameFromUri(uri)
                pvC.isVisible = true
                pdfB.text = getString(R.string.change_pdf_button)
                pdfDeleteB.isVisible = true
                setDeletePdfButton(pdfDeleteB,pvC,pdfB,viewModel)
            }
        }
        pdfB.setOnClickListener {
            selectPDFFromStorage(selectPDFFromStorageResult)
        }
    }
    private fun selectPDFFromStorage(selectPDFFromStorageResult: ActivityResultLauncher<String>) = selectPDFFromStorageResult.launch("application/pdf")
    private fun setDeletePdfButton(deletePdfButton: MaterialButton, pvC: LinearLayout, buttonAddPdf: MaterialButton, viewModel: BaseViewModel) {
        deletePdfButton.setOnClickListener {
            viewModel.setPdfUri(null)
            pvC.visibility = View.GONE
            buttonAddPdf.text = resources.getText(R.string.add_pdf_button)
            deletePdfButton.visibility = View.GONE
        }
    }
    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
        }
    }
    fun setChangePdfButton(view: View, pdfPath: String, viewModel: BaseViewModel, fileName: String) {
        val pvC = view.findViewById<LinearLayout>(R.id.pdf_view_container)
        val buttonAddPdf = view.findViewById<MaterialButton>(R.id.add_pdf_button)
        val buttonDeletePdf = view.findViewById<MaterialButton>(R.id.delete_pdf_button)
        viewModel.setOldPdf( pdfPath )
        if ( pdfPath.isNotBlank() ) {
            pvC.isInvisible = false
            buttonAddPdf.text = resources.getText(R.string.change_pdf_button)
            buttonDeletePdf.isInvisible = false
            setDeletePdfButton(buttonDeletePdf,pvC,buttonAddPdf,viewModel)
            val file = File(GlobalAppStore.appCtx!!.filesDir, pdfPath)
            val fileUri = FileProvider.getUriForFile(GlobalAppStore.appCtx!!,"app.gedama.tutorial.cruddiary.provider",file)
            if ( fileUri != null ) {
                viewModel.pdfFileName = fileName
                viewModel.setPdfUri(fileUri)
            }
        }
    }
    fun doSpeechRecognition(layoutEndIcon: TextInputLayout, textField: TextInputEditText ) {
        val startForSpeechRecognitionResult =
            registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
                val resultCode = result.resultCode
                val data = result.data
                if (resultCode == Activity.RESULT_OK) {
                    val spokenText: String? =
                        data!!.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS).let { results ->
                            results!![0]
                        }
                    spokenText?.let {
                        var newText: String = textField.text.toString()
                        if ( newText.isBlank() ) {
                            newText = it
                        } else {
                            newText += " " + it
                        }
                        Log.d(TAG,"Recognized text: $it")
                        textField.setText(newText)
                    }
                } else {
                    Log.d(TAG,"Speech recogintion was not successful: $resultCode")
                }
            }
        layoutEndIcon.setEndIconOnClickListener {
            val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply {
                putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM)
                // from API level 33 on this should improve the formatting (capitalization, punctuation, etc.) of the recognized text
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                    putExtra(
                        RecognizerIntent.EXTRA_ENABLE_FORMATTING,
                        RecognizerIntent.FORMATTING_OPTIMIZE_QUALITY
                    )
                }
            }
            startForSpeechRecognitionResult.launch(intent)
        }
    }
}

Now we can define our view model:

21.5 UpdateDiaryEntryViewModel.kt

Listing 151: UpdateDiaryEntryViewModel.kt
package app.gedama.tutorial.cruddiary.viewmodel
import android.util.Log
import androidx.databinding.ObservableField
import androidx.lifecycle.*
import app.gedama.tutorial.cruddiary.GlobalAppStore
import app.gedama.tutorial.cruddiary.dao.DiaryEntryDao
import app.gedama.tutorial.cruddiary.data.DiaryEntriesTagsCrossRef
import app.gedama.tutorial.cruddiary.data.DiaryEntry
import app.gedama.tutorial.cruddiary.data.DiaryEntryWithEverything
import app.gedama.tutorial.cruddiary.data.Person
import app.gedama.tutorial.cruddiary.data.Spinnable
import app.gedama.tutorial.cruddiary.data.Tag
import kotlinx.coroutines.launch
import java.util.*
class UpdateDiaryEntryViewModel(val diaryEntryId: Long, val diaryEntryDao: DiaryEntryDao) : BaseViewModel() {
    val diaryEntry = diaryEntryDao.getDiaryEntryWithEverything(diaryEntryId)
    val allPersons = diaryEntryDao.getAllPersons(0L)
    val allTags = diaryEntryDao.getAllTags(1L)
    var relevance = DiaryEntry().relevance
    var relevanceDataBindingVariable =
        object: ObservableField<String>(relevance.toString()) {
            override fun set(value: String?) {
                super.set(value)
                // a value has been set
                relevance = value?.toIntOrNull() ?: relevance
            }
        }
    private val _selectedTags = MutableLiveData<Set<Tag>>(mutableSetOf())
    val selectedTags: LiveData<Set<Tag>>
        get() = _selectedTags
    private val _selectableTags = MutableLiveData<Set<Tag>>(mutableSetOf())
    val selectableTags: LiveData<Set<Tag>>
        get() = _selectableTags
    private val _entryDate = MutableLiveData<Date>()
    val entryDate: LiveData<Date>
        get() = _entryDate
    val allPersonsAndDiaryEntryLiveData: LiveData<Pair<List<Person>, DiaryEntryWithEverything>> =
        object: MediatorLiveData<Pair<List<Person>, DiaryEntryWithEverything>>() {
            var allP: List<Person>? = null
            var diEent: DiaryEntryWithEverything? = null
            init {
                addSource( allPersons ) { allP ->
                    this.allP = allP
                    diEent?.let { value = allP to it }
                }
                addSource(diaryEntry) { diaryEntry ->
                    this.diEent = diaryEntry
                    allP?.let { value = it to diaryEntry }
                }
            }
        }
    val allTagsAndDiaryEntryLiveData: LiveData<Pair<List<Tag>, DiaryEntryWithEverything>> =
        object: MediatorLiveData<Pair<List<Tag>, DiaryEntryWithEverything>>() {
            var allP: List<Tag>? = null
            var diEent: DiaryEntryWithEverything? = null
            init {
                addSource( allTags ) { allP ->
                    this.allP = allP
                    diEent?.let { value = allP to it }
                }
                addSource(diaryEntry) { diaryEntry ->
                    this.diEent = diaryEntry
                    allP?.let { value = it to diaryEntry }
                }
            }
        }
    fun updateDiaryEntry() {
        _doValidation.value = true
    }
    override fun updateEntryAfterValidation() {
        viewModelScope.launch {
            if ( deactivateSuccess.value!! || diaryEntry.value!!.entry.inactive ) {
                diaryEntry.value!!.entry.inactive = true
            } else {
                diaryEntry.value!!.entry.inactive = false
            }
            val newImagePath = checkNewImagePath("diary_entry_")
            newImagePath?.let {
                diaryEntry.value!!.entry.imagePath = it
            }
            val newPdfPath = checkNewPdfPath("pdf_")
            newPdfPath?.let {
                if ( it.isNotBlank() ) {
                    diaryEntry.value!!.entry.pdfFileName = getFilenameFromUri(pdfUri.value!!)
                    diaryEntry.value!!.entry.pdfPath = it
                } else {
                    diaryEntry.value!!.entry.pdfFileName = ""
                    diaryEntry.value!!.entry.pdfPath = ""
                }
            }
            diaryEntryDao.update(diaryEntry.value!!.entry)
            // simply delete all entries in cross reference table
            diaryEntryDao.deleteAllTagsForEntry(diaryEntryId)
            val selTagList = selectedTags.value
            if ( selTagList != null && selTagList.isNotEmpty() ) {
                selTagList.forEach {
                    val diaryEntryPair = DiaryEntriesTagsCrossRef(diaryEntryId,it.tagId)
                    diaryEntryDao.insertDiaryEntryTag(diaryEntryPair)
                }
            }
            else {
                // if no tag is assigned, insert dummy tag (---) so that filtering works
                val entryTagPair = DiaryEntriesTagsCrossRef(diaryEntryId,1L)
                diaryEntryDao.insertDiaryEntryTag(entryTagPair)
            }
            _navigateToList.value = true
        }
    }
    fun updateDiaryPerson(item: Spinnable) {
        diaryEntry.value!!.entry.personId = item.itemId()
    }
    override fun updateEntryDate(date: Date) {
        _entryDate.value = date
        diaryEntry.value!!.entry.date = date
    }
    fun setSelectedTag(tag: Tag) {
        _selectedTags.postValue(selectedTags.value!!.plus(tag))
        _selectableTags.postValue(selectableTags.value!!.minus(tag))
        Log.d(TAG,"Tag ${tag.title} was selected ...")
        Log.d(TAG,"Selected tags: ${selectedTags.value}")
    }
    fun deSelectTag(tag: Tag) {
        _selectedTags.postValue(selectedTags.value!!.minus(tag))
        _selectableTags.postValue(selectableTags.value!!.plus(tag))
        Log.d(TAG,"Tag ${tag.title} was deselected ...")
        Log.d(TAG,"Selected tags: ${selectedTags.value}")
    }
    fun setSelectableTags(set: Set<Tag>) {
        _selectableTags.postValue(set)
    }
    fun setSelectedTags(set: Set<Tag>) {
        // if dummy tag (---) is set, remove it
        val dummyReducedSet = set.filter{ it.tagId != 1L }.toSet()
        _selectedTags.postValue(dummyReducedSet)
    }
    fun deactivateDiaryEntry() {
        _deactivateSuccess.value = true
        _doValidation.value = true
    }
}
class UpdateDiaryEntryViewModelFactory(private val diaryEntryId: Long, private val diaryEntryDao: DiaryEntryDao) : ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(UpdateDiaryEntryViewModel::class.java)) {
            return UpdateDiaryEntryViewModel(diaryEntryId, diaryEntryDao) as T
        }
        throw java.lang.IllegalArgumentException("Unknown ViewModel")
    }
}

Next we define the layout for our new fragment.

21.6 fragment_update_diary_entry.xml

Listing 152: fragment_update_diary_entry.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.UpdateDiaryEntryFragment">
    <data>
        <variable
            name="viewModel"
            type="app.gedama.tutorial.cruddiary.viewmodel.UpdateDiaryEntryViewModel" />
    </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.diaryEntry.entry.title}"
                app:textHint="@{@string/diary_title_label}"
                layout="@layout/text_input_title" />
            <include
                android:id="@+id/date_text_field_binding"
                app:hintText="@{@string/date_label}"
                app:viewModelValue="@={viewModel.diaryEntry.entry.dateAsString}"
                layout="@layout/text_input_date" />
            <include
                android:id="@+id/text_text_field_binding"
                app:viewModelValue="@={viewModel.diaryEntry.entry.text}"
                app:hintText="@{@string/text_label}"
                layout="@layout/text_input_diary_text" />
            <include
                android:id="@+id/spinner_persons_binding"
                app:textHint="@{@string/person_label}"
                layout="@layout/spinner_generic" />
            <include
                android:id="@+id/relevance_text_field_binding"
                app:textInputFormat="@{android.text.InputType.TYPE_CLASS_NUMBER|android.text.InputType.TYPE_NUMBER_FLAG_SIGNED}"
                app:viewModelValue="@={viewModel.relevanceDataBindingVariable}"
                app:textHint="@{@string/relevance_label}"
                app:possibleDigits="@{@string/possible_digits}"
                layout="@layout/text_input_generic" />
            <LinearLayout
                android:id="@+id/tags_area"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="vertical"
                android:visibility="gone">
                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_gravity="start"
                    android:padding="8dp"
                    android:text="@string/tags_label"
                    android:textSize="16sp" />
                <com.google.android.flexbox.FlexboxLayout
                    android:id="@+id/chip_area"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    app:flexWrap="wrap"
                    app:alignItems="stretch"
                    app:alignContent="stretch" />
            </LinearLayout>
            <com.google.android.material.button.MaterialButton
                android:id="@+id/add_tag_button"
                style="@style/CrudDiary_Button.AddTag"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:text="@string/add_tag_button" />
            <include
                android:id="@+id/pdf_area_binding"
                app:viewModelValue="@={viewModel.diaryEntry.entry.pdfFileName}"
                app:hintText="@{@string/pdf_label}"
                layout="@layout/pdf_area" />
            <include
                android:id="@+id/check_box_binding"
                app:viewModelValue="@={viewModel.diaryEntry.entry.inactive}"
                layout="@layout/checkbox_inactive" />
            <include
                android:id="@+id/image_area_binding"
                layout="@layout/image_area" />
            <include
                app:viewModelFunc="@{ () -> viewModel.updateDiaryEntry() }"
                android:id="@+id/crud_buttons"
                app:visibility="@{android.view.View.VISIBLE}"
                layout="@layout/crud_buttons" />
        </androidx.appcompat.widget.LinearLayoutCompat>
    </ScrollView>
</layout>

Notice the similarity of this file to the one we used for adding new diary entries! Next we want to extend our navigation graph with our new fragment:

21.7 nav_graph.xml

Listing 153: 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" />
        <action
            android:id="@+id/action_homeFragment_to_diaryEntriesListFragment"
            app:destination="@id/diaryEntriesListFragment" />
    </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"
            app:popUpTo="@id/homeFragment"
            app:popUpToInclusive="true" />
    </fragment>
    <fragment
        android:id="@+id/diaryEntriesListFragment"
        android:name="app.gedama.tutorial.cruddiary.fragment.DiaryEntriesListFragment"
        android:label="@string/diary_entries_list_menu_title"
        tools:layout="@layout/fragment_diary_entries_list" >
        <action
            android:id="@+id/action_diaryEntriesListFragment_to_homeFragment"
            app:destination="@id/homeFragment" />
        <action
            android:id="@+id/action_diaryEntriesListFragment_to_addDiaryEntryFragment"
            app:destination="@id/addDiaryEntryFragment" />
        <action
            android:id="@+id/action_diaryEntriesListFragment_to_updateDiaryEntryFragment"
            app:destination="@id/updateDiaryEntryFragment" />
    </fragment>
    <fragment
        android:id="@+id/addDiaryEntryFragment"
        android:name="app.gedama.tutorial.cruddiary.fragment.AddDiaryEntryFragment"
        android:label="@string/diary_entries_add_menu_title" >
        <action
            android:id="@+id/action_addDiaryEntryFragment_to_diaryEntriesListFragment"
            app:destination="@id/diaryEntriesListFragment"
            app:popUpTo="@id/homeFragment" />
    </fragment>
    <fragment
        android:id="@+id/updateDiaryEntryFragment"
        android:name="app.gedama.tutorial.cruddiary.fragment.UpdateDiaryEntryFragment"
        android:label="@string/diary_entries_edit_menu_title" >
        <action
            android:id="@+id/action_updateDiaryEntryFragment_to_diaryEntriesListFragment"
            app:destination="@id/diaryEntriesListFragment"
            app:popUpTo="@id/homeFragment" />
        <argument
            android:name="diaryEntryId"
            app:argType="long" />
    </fragment>
</navigation>

Note that we added the argument diaryEntryId to our new fragment because we have to pass the ID so we know which diary entry item gets updated. Now it is time to present our UpdateDiaryEntryFragment class:

21.8 UpdateDiaryEntryFragment.kt

Listing 154: UpdateDiaryEntryFragment.kt
package app.gedama.tutorial.cruddiary.fragment
import android.app.DatePickerDialog
import android.app.Dialog
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.appcompat.widget.SearchView
import androidx.core.content.FileProvider
import androidx.core.view.isVisible
import androidx.lifecycle.*
import androidx.lifecycle.Observer
import androidx.navigation.findNavController
import androidx.recyclerview.widget.RecyclerView
import app.gedama.tutorial.cruddiary.R
import app.gedama.tutorial.cruddiary.adapter.TagItemAdapter
import app.gedama.tutorial.cruddiary.data.DiaryEntryWithEverything
import app.gedama.tutorial.cruddiary.data.Spinnable
import app.gedama.tutorial.cruddiary.data.Tag
import app.gedama.tutorial.cruddiary.data.Validation
import app.gedama.tutorial.cruddiary.database.CrudDiaryDatabase
import app.gedama.tutorial.cruddiary.databinding.FragmentUpdateDiaryEntryBinding
import app.gedama.tutorial.cruddiary.ui.LambdaDatePicker
import app.gedama.tutorial.cruddiary.ui.formfields.FormFieldText
import app.gedama.tutorial.cruddiary.viewmodel.UpdateDiaryEntryViewModel
import app.gedama.tutorial.cruddiary.viewmodel.UpdateDiaryEntryViewModelFactory
import com.google.android.material.chip.Chip
import java.io.File
import java.text.DateFormat
import java.util.*
class UpdateDiaryEntryFragment: BaseFragment() {
    private var _binding: FragmentUpdateDiaryEntryBinding? = null
    private val binding get() = _binding!!
    private var diaryEntry: DiaryEntryWithEverything? = null
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentUpdateDiaryEntryBinding.inflate(inflater, container, false)
        val view = binding.root
        val diaryEntryId = UpdateDiaryEntryFragmentArgs.fromBundle(requireArguments()).diaryEntryId
        val application = requireNotNull(this.activity).application
        val diaryEntryDao = CrudDiaryDatabase.getInstance(application).diaryEntryDao
        val viewModelFactory = UpdateDiaryEntryViewModelFactory(diaryEntryId,diaryEntryDao)
        val viewModel = ViewModelProvider(this, viewModelFactory)[UpdateDiaryEntryViewModel::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 fieldTitle = FormFieldText(
            scope = lifecycleScope,
            textInputLayout = binding.titleTextFieldBinding.titleLayout,
            textInputEditText = binding.titleTextFieldBinding.title,
            Validation.titleValidation
        )
        val fieldText = FormFieldText(
            scope = lifecycleScope,
            textInputLayout = binding.textTextFieldBinding.diaryTextLayout,
            textInputEditText = binding.textTextFieldBinding.diaryText,
            Validation.textValidation
        )
        val fieldPerson = FormFieldText(
            scope = lifecycleScope,
            textInputLayout = binding.spinnerPersonsBinding.spinnerInputLayoutId,
            textInputEditText = binding.spinnerPersonsBinding.spinnerTextId,
            Validation.personValidation
        )
        val fieldRelevance = FormFieldText(
            scope = lifecycleScope,
            textInputLayout = binding.relevanceTextFieldBinding.textInputLayout,
            textInputEditText = binding.relevanceTextFieldBinding.textInputEditText,
            Validation.relevanceValidation
        )
        val formFields = listOf(fieldTitle,fieldText,fieldPerson,fieldRelevance)
        initImagePicking(binding.imageAreaBinding.imageView, binding.imageAreaBinding.buttonLoadPicture,viewModel, binding.imageAreaBinding.buttonDefaultPicture)
        saveToUpdateButton(binding.crudButtons.actionButton)
        val dialogBox = Dialog(this.requireContext())
        val adapter = TagItemAdapter { tag ->
            viewModel.setSelectedTag(tag)
            dialogBox.cancel()
        }
        viewModel.allTags.observe(viewLifecycleOwner, Observer {
            it?.let {
                val mutList = mutableListOf<Tag>()
                mutList.addAll(it.toList())
                adapter.setData(mutList)
                viewModel.setSelectableTags(it.toSet())
            }
        })
        viewModel.pdfUri.observe(viewLifecycleOwner, { pdfUri ->
            if ( pdfUri != null ) {
                binding.pdfAreaBinding.pdfFileName.setText(viewModel.pdfFileName)
                binding.pdfAreaBinding.pdfLayout.setEndIconOnClickListener {
                    val intent = Intent(Intent.ACTION_VIEW).apply {
                        addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
                        addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
                        setDataAndType(pdfUri, "application/pdf")
                    }
                    this.requireContext()
                        .startActivity(Intent.createChooser(intent, getString(R.string.select_app)))
                }
            }
        })
        // when a tag is added to the diary entry, the list of selectable tags
        // has to be reduced by this added tag and the adapter has to be updated correspondingly
        viewModel.selectableTags.observe(viewLifecycleOwner, Observer {
            Log.d(TAG, "selectableTags was changed ...")
            it?.let {
                Log.d(TAG, "New selectable tag list: $it")
                val mutList = mutableListOf<Tag>()
                mutList.addAll(it.toList())
                adapter.setData(mutList)
            }
        })
        viewModel.selectedTags.observe(viewLifecycleOwner, Observer {
            Log.d(TAG, "selectedTags was changed ...")
            it?.let {
                Log.d(TAG, "New selected tag list: $it")
                if ( it.isNotEmpty() ) {
                    binding.tagsArea.isVisible = true
                    binding.chipArea.removeAllViews()
                    it.forEach { tag ->
                        val chip = Chip(this.requireContext())
                        chip.text = tag.title
                        chip.isCloseIconVisible = true
                        chip.setOnClickListener { view ->
                            viewModel.deSelectTag(tag)
                            binding.chipArea.removeView(view)
                        }
                        binding.chipArea.addView(chip)
                    }
                } else {
                    binding.tagsArea.visibility = View.GONE
                }
            }
        })
        viewModel.navigateToList.observe(viewLifecycleOwner, Observer { navigate ->
            if (navigate) {
                view.findNavController()
                    .navigate(R.id.action_updateDiaryEntryFragment_to_diaryEntriesListFragment)
                viewModel.onNavigatedToList()
            }
        })
        viewModel.diaryEntry.observe(viewLifecycleOwner, Observer { item ->
            diaryEntry = item
            setChangeImageButton(view, item.entry.imagePath, viewModel)
            setChangePdfButton(view, item.entry.pdfPath, viewModel, item.entry.pdfFileName)
            setInactivityButton(item.entry.inactive,binding.crudButtons.deactivateButton,binding.checkBoxBinding.checkBoxInactive)
        })
        viewModel.doValidation.observe(viewLifecycleOwner, Observer { doValidate ->
            if (doValidate) {
                formValidation(binding.crudButtons.actionButton, formFields, viewModel)
            }
        })
        viewModel.entryDate.observe(viewLifecycleOwner, Observer { item ->
            binding.dateTextFieldBinding.date.setText(DateFormat.getDateInstance(DateFormat.MEDIUM).format(item))
        })
        val handleIconClick = object : View.OnClickListener {
            override fun onClick(v: View?) {
                val picker = LambdaDatePicker(context = context!!, showTimePicker = false, { item: Date -> viewModel.updateEntryDate(item) })
                val currentEntryDate = Calendar.getInstance()
                diaryEntry?.entry?.date?.let {
                    currentEntryDate.time = diaryEntry!!.entry.date
                }
                val datePickerDialog =
                    DatePickerDialog(
                        context!!,
                        picker,
                        currentEntryDate.get(Calendar.YEAR),
                        currentEntryDate.get(
                            Calendar.MONTH
                        ),
                        currentEntryDate.get(Calendar.DAY_OF_MONTH)
                    )
                datePickerDialog.datePicker.maxDate = Date().time
                datePickerDialog.show()
            }
        }
        binding.dateTextFieldBinding.date.setOnClickListener(handleIconClick)
        binding.dateTextFieldBinding.dateLayout.setEndIconOnClickListener(handleIconClick)
        doSpeechRecognition(binding.textTextFieldBinding.diaryTextLayout, binding.textTextFieldBinding.diaryText)
        @Suppress("UNCHECKED_CAST")
        initMaterialSpinnerData(binding.spinnerPersonsBinding.spinnerId,binding.spinnerPersonsBinding.spinnerTextId,
            viewModel.allPersons as LiveData<List<Spinnable>>,{ item: Spinnable -> viewModel.updateDiaryPerson(item)} )
        viewModel.allPersonsAndDiaryEntryLiveData.observe(viewLifecycleOwner, Observer { (allP,diaEnt) ->
            val currentPersonId = diaEnt.person.personId ?: 1L
            val currentPerson = allP.find{ it.personId == currentPersonId }
            val currentPersonPos = allP.indexOf(currentPerson)
            binding.spinnerPersonsBinding.spinnerId.setSelection(currentPersonPos)
        } )
        viewModel.allTagsAndDiaryEntryLiveData.observe(viewLifecycleOwner, Observer { (allT,dewt) ->
            dewt.let { diaryEntryWithTags ->
                viewModel.setSelectedTags(diaryEntryWithTags.tags.filter{!it.inactive}.toSet())
                val difference = allT.minus(dewt.tags.toSet())
                val mutList = mutableListOf<Tag>()
                mutList.addAll(difference)
                adapter.setData(mutList)
                viewModel.setSelectableTags(difference.toSet())
            }
        })
        // see https://github.com/zinedineBenkhider/android-tutos/blob/main/CustomizedDialogBox/app/src/main/java/com/example/customizeddialogbox/MainActivity.kt
        val tagButton = binding.addTagButton
        tagButton.setOnClickListener {
            dialogBox.setContentView(R.layout.dialog_select_tag)
            val searchView = dialogBox.findViewById<SearchView>(R.id.searchView)
            val sac = searchView.findViewById<SearchView.SearchAutoComplete>(androidx.appcompat.R.id.search_src_text)
            sac.setHint(resources.getString(R.string.search_view_hint))
            val headline = dialogBox.findViewById<TextView>(R.id.tags_headline)
            val listSize = adapter.itemCount
            if ( listSize < 1 && viewModel.selectedTags.value!!.isEmpty() ) {
                headline.text = getString(R.string.no_tags_available)
            } else if ( listSize < 1 && viewModel.selectedTags.value!!.size > 0 ) {
                headline.text = getString(R.string.no_further_tags_available)
            }
            val tagRV = dialogBox.findViewById<RecyclerView>(R.id.tags_list)
            tagRV.adapter = adapter
            searchView.setOnQueryTextListener(object: SearchView.OnQueryTextListener {
                override fun onQueryTextSubmit(p0: String?): Boolean {
                    adapter.filter.filter(p0)
                    return false
                }
                override fun onQueryTextChange(p0: String?): Boolean {
                    adapter.filter.filter(p0)
                    return false
                }
            })
            dialogBox.show()
        }
        setDeactivateButton(binding.crudButtons.deactivateButton, { viewModel.deactivateDiaryEntry() } )
        setDeleteImageButton(binding.imageAreaBinding.buttonDefaultPicture,binding.imageAreaBinding.imageView,
            binding.imageAreaBinding.imageViewContainer,binding.imageAreaBinding.buttonLoadPicture, viewModel)
        val pdfButton = binding.pdfAreaBinding.addPdfButton
        val pdfDeleteButton = binding.pdfAreaBinding.deletePdfButton
        val pdfViewContainer = binding.pdfAreaBinding.pdfViewContainer
        initPDFFromStorage(pdfButton, pdfDeleteButton, pdfViewContainer, viewModel)
        return view
    }
    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

Once again, for getting from the diary entries list page to the ’Update diary entry’ page, we have to extend our existing DiaryEntriesListFragment:

21.9 PersonsListFragment.kt

Listing 155: PersonsListFragment.kt
...
        viewModel.navigateToAddItem.observe(viewLifecycleOwner, { navigate ->
            if (navigate) {
                val action = DiaryEntriesListFragmentDirections
                    .actionDiaryEntriesListFragmentToAddDiaryEntryFragment()
                this.findNavController().navigate(action)
                viewModel.onItemAddNavigated()
            }
        })
        viewModel.navigateToEditItem.observe(viewLifecycleOwner, { diaryEntryId ->
            diaryEntryId?.let {
                val action = DiaryEntriesListFragmentDirections
                    .actionDiaryEntriesListFragmentToUpdateDiaryEntryFragment(diaryEntryId)
                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 diary entries list, you get to the input mask for updating the selected diary entry (see Figure 21.1). Notice the image buttons below Lucy’s picture: We have a ’Change image’ button and a ’Delete image’ button.

Update diary entry
Figure 21.1: Update diary entry