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.
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"
}
}