Search code examples
androidkotlinandroid-alertdialogandroid-dialogfragment

Change name of positive/negative button in AlertDialog wrapped inside a DialogFragment


I have an custom DialogFragment class ListWorkoutTypeDialog, which opens a dialog to delete or edit workout types for my gym app via an AlertDialog inside the class. The goal is to be able to change the text of the positive and negative buttons on this AlertDialog based on if the user has deleted an entry. ie. if the user hasn't changed anything it will show one single negative button labelled "dismiss" otherwise if anything has changed it will display a positive "save" button and a negative "cancel" button. Currently I am just trying to change the "Dismiss" button to "Cancel", which even that I'm having troubles with.

I'm having problems setting a reference to the AlertDialog and getting that reference in the calling class (my MainActivity). It always returns null, even when I create() and show() the AlertDialog before the call to get the reference.

I've tried setting the button text in the ListWorkoutTypeDialog class but was still causing null pointer exceptions.

Observe that alertDialog is set in the onCreateDialog overridden function, so should be set. However even after this function is supposedly called and I try to get a reference to alertDialog with getAlertDialog it is still null.

Here is my ListWorkoutTypeDialog class:

package com.example.workoutbuddy.dialogs

import android.app.Activity
import android.app.AlertDialog
import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
import android.util.Log
import android.widget.AdapterView
import android.widget.ArrayAdapter
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.lifecycleScope
import com.example.workoutbuddy.MainActivity
import com.example.workoutbuddy.R
import com.example.workoutbuddy.db.WorkoutType
import com.example.workoutbuddy.databinding.DialogWorkoutTypeListBinding
import com.example.workoutbuddy.util.findById
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

// display all workoutTypes in a list where user can long press to remove them or tap to edit them
// takes in a list of workoutTypes to display, activity context and a FragmentManager
class ListWorkoutTypeDialog(workoutTypes: MutableList<WorkoutType?>?, private var context: Activity, private var fm: FragmentManager) : DialogFragment() {

    // copy of workoutTypes
    private var realWorkoutTypesList = workoutTypes?.toMutableList()
    // true when we want to save (ie. save button clicked)
    private var save = false
    // list of workoutTypes to be displayed
    private var arrayOfWorkoutTypes = mutableListOf<String>();
    // adaptor for workoutTypes array
    private var adapter = ArrayAdapter(context, R.layout.blank_text_view_black_text, arrayOfWorkoutTypes)
    // tempMap for mapping position in list to primary ID keys of workoutTypes
    private var tempMap: MutableMap<Int, Int> = mutableMapOf<Int, Int>()
    // if firstRun
    private var firstRun = true
    // mutableList of IDs to be removed
    private var idsToRemove = mutableListOf<Int>()
    // reference to self (this)
    private var self = this

    private lateinit var alertDialog: AlertDialog



    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        return activity?.let {
            val builder = AlertDialog.Builder(it)

            val binding = DialogWorkoutTypeListBinding.inflate(layoutInflater)

            // clear the array of workoutTypes
            arrayOfWorkoutTypes.clear()
            // null check
            if (realWorkoutTypesList != null) {
                // for each workoutType (wt) add them to the array list to be displayed
                for (wt in realWorkoutTypesList!!) {
                    arrayOfWorkoutTypes.add("Workout ${wt!!.identifier} - ${wt.name}")
                }
            }


            // setup adaptor
            binding.workoutTypeList.adapter = adapter

            // on long click we delete the long clicked item
            binding.workoutTypeList.onItemLongClickListener = AdapterView.OnItemLongClickListener {
                    _,_,_,position ->
                // if position is 0 (placeholder no workout item) don't remove as it is the default
                if (position.toInt() != 0) {
                    // removes item from listview array
                    arrayOfWorkoutTypes.removeAt(position.toInt())

                    if (firstRun) {
                        // remove from lists
                        val num = findById(realWorkoutTypesList, (context as MainActivity).getWorkoutIDByPosition(position.toInt()))
                        realWorkoutTypesList?.remove(num)
                        idsToRemove.add((context as MainActivity).getWorkoutIDByPosition(position.toInt()))
                        firstRun = false
                    } else {
                        // tempMap now set so use that as positions will change as we remove items
                        idsToRemove.add(tempMap[position.toInt()]!!)
                        val num = findById(realWorkoutTypesList, tempMap[position.toInt()]!!)
                        realWorkoutTypesList?.remove(num)
                    }
                    // update the map as items are deleted
                    tempMap = (context as MainActivity).getSpinnerListMappings(realWorkoutTypesList)
                    // updates/recycles view
                    adapter.notifyDataSetChanged()
                }
                true
            }

            // on a short click we edit the item clicked
            binding.workoutTypeList.onItemClickListener = AdapterView.OnItemClickListener {
                _,_,_,position ->

                // make sure we don't remove the default workout "No workout" placeholder
                if (position.toInt() != 0) {
                    var tempWorkoutType: WorkoutType? = null
                    lifecycleScope.launch(Dispatchers.IO) {
                        val workoutTypeDAO = (context as MainActivity).db.getWorkoutTypeDao()

                        tempWorkoutType = workoutTypeDAO.get((context as MainActivity).getWorkoutIDByPosition(position.toInt()))

                        lifecycleScope.launch(Dispatchers.Main) {
                            // show the add/update workout dialog
                            val dialog = AddUpdateWorkoutTypeDialog(fm, tempWorkoutType, self)
                            dialog.show(fm, "WORKOUT_TYPE_UPDATE_DIALOG")
                        }
                    }
                }

            }

            // HERE I SET THE ALERTDIALOG
            alertDialog = builder.setView(binding.root)
                .setNegativeButton("Dismiss") { _, _ ->
                    // send cancel data
                    save = false
                    this.dismiss()
                }.create()

            Log.d("status", "alertDialog set")

            return builder.create()
        } ?: throw IllegalStateException("Activity cannot be null")
    }

    fun getAlertDialog(): AlertDialog {
        // returns the AlertDialog used to make the alert
        return alertDialog
    }

    // refresh workout types
    fun refresh(wtl: MutableList<WorkoutType?>?) {
        arrayOfWorkoutTypes.clear()
        if (wtl != null) {
            for (wt in wtl) {
                arrayOfWorkoutTypes.add("Workout ${wt!!.identifier} - ${wt.name}")
            }
        }
        if (wtl != null) {
            realWorkoutTypesList = wtl.toMutableList()
        }
        // refresh/recycle view
        adapter.notifyDataSetChanged()
    }

    override fun onDismiss(dialog: DialogInterface) {
        super.onDismiss(dialog)
        val activity: Activity? = activity
        if (activity is DialogInterface.OnDismissListener) {
            // if save button clicked update list and remove deleted IDs
            if (save) {
                (activity as MainActivity).onWorkoutListUpdate(idsToRemove)
            }
        }
    }
}

Here is the snippet where I call the dialog (in my MainActivity):

                    val workoutTypeDAO = db.getWorkoutTypeDao()

                    // spawn list dialog
                    val dialog = ListWorkoutTypeDialog(workoutTypeDAO.getAll()?.toMutableList(), self, supportFragmentManager)
                    // refresh the list
                    dialog.refresh(workoutTypeDAO.getAll()?.toMutableList())
                    // show the dialog
                    dialog.show(supportFragmentManager, "WORKOUT_LIST_VIEW")

                    Log.d("status", "changing button")
                    // set the button to cancel
                    dialog.getAlertDialog().getButton(AlertDialog.BUTTON_NEGATIVE).text = "Cancel" //ERROR happens here, getAlertDialog returns NULL


I get the following error that alertDialog is not initialized even though it is run BEFORE setting the button:

2024-04-01 15:26:11.006 22922-22962 status                  com.example.workoutbuddy             D  changing button (this should be called last)
2024-04-01 15:26:11.025 22922-22962 AndroidRuntime          com.example.workoutbuddy             E  FATAL EXCEPTION: DefaultDispatcher-worker-1
                                                                                                    Process: com.example.workoutbuddy, PID: 22922
                                                                                                    kotlin.UninitializedPropertyAccessException: lateinit property alertDialog has not been initialized
                                                                                                        at com.example.workoutbuddy.dialogs.ListWorkoutTypeDialog.getAlertDialog(ListWorkoutTypeDialog.kt:133)
                                                                                                        at com.example.workoutbuddy.MainActivity$onOptionsItemSelected$1.invokeSuspend(MainActivity.kt:116)
                                                                                                        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
                                                                                                        at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
                                                                                                        at kotlinx.coroutines.internal.LimitedDispatcher$Worker.run(LimitedDispatcher.kt:115)
                                                                                                        at kotlinx.coroutines.scheduling.TaskImpl.run(Tasks.kt:100)
                                                                                                        at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:584)
                                                                                                        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:793)
                                                                                                        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:697)
                                                                                                        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:684)
                                                                                                        Suppressed: kotlinx.coroutines.internal.DiagnosticCoroutineContextException: [StandaloneCoroutine{Cancelling}@83d2b55, Dispatchers.IO]
2024-04-01 15:26:11.031 22922-22922 status                  com.example.workoutbuddy             D  alertDialog set (this should be called first)

I'm not sure if this is even the right approach to solving this issue as I'm quite new to Android development but I thought this should be relatively easy. Thank you for your help.


Solution

  • First, you're calling create twice which is probably a bug:

    // HERE I SET THE ALERTDIALOG
    alertDialog = builder.setView(binding.root)
        .setNegativeButton("Dismiss") { _, _ ->
            // send cancel data
            save = false
            this.dismiss()
         }.create()         // <-- alertDialog set to this instance
    
    Log.d("status", "alertDialog set")
    
    return builder.create() // <-- New, different instance returned 
    

    Next, you have a misunderstanding of how show works:

    I get the following error that alertDialog is not initialized even though it is run BEFORE setting the button:

    The dialog is not shown immediately. It is added to the fragment manager transaction queue to be executed later (like the next frame).

    // This actually means "schedule the dialog to be shown soon"
    dialog.show(supportFragmentManager, "WORKOUT_LIST_VIEW")
    
    Log.d("status", "changing button")
    // set the button to cancel
    
    // Dialog will NOT have been initialized one line of execution later
    dialog.getAlertDialog().getButton(AlertDialog.BUTTON_NEGATIVE).text = "Cancel" //ERROR happens here, getAlertDialog returns NULL
    

    Finally, to actually do what you want: what I've done in the past is update the buttons in onStart() when the dialog has been fully initialized:

    class ListWorkoutTypeDialog {
        override onStart() {
            alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE).text = "Cancel"
        }
    }