I'm having a problem with AlertDialog: if I want to finish the current activity after I open a new one, I get crashed with the following error:
E/WindowManager: android.view.WindowLeaked: Activity com.myapp.ShowsActivity has leaked window DecorView#2435213[ShowsActivity] that was originally added here
This happens when I want to log out a user. The AuthLogic.logout() method gets called (I exported it to a standalone class to have a more readable code) which triggers a "are you sure?" alert dialog which then redirects to login activity.
Logout button logic:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_shows)
// some code . . .
btnLogout.setOnClickListener {
AuthLogic.logout(this)
finish() // *1
}
}
AuthLogic.logout():
fun logout(context: Context) {
val builder = AlertDialog.Builder(context)
builder.setTitle(R.string.are_you_sure)
builder.setMessage(R.string.confirm_msg_logout)
builder.setPositiveButton(R.string.confirm) { dialogInterface: DialogInterface, i: Int ->
// aditional logout logic
context.startActivity(AuthActivity.newStartIntent(context))
}
builder.setNegativeButton(R.string.cancel) { di: DialogInterface, _: Int -> }
builder.show()
}
I noticed that the error pops up if I finish my activity on *1 comment. If I remove that line, no error gets shown, but this causes a problem because I can go back to the previous activity and I don't want that. I already tried using dialogInterface.dismiss() on setPositiveButton lambda method, but no success. Any ideas?
EDIT! solved!:)
I rewrote AuthLogic.logout():
fun logout(context: Context) {
val builder = AlertDialog.Builder(context)
builder.setTitle(R.string.are_you_sure)
builder.setMessage(R.string.confirm_msg_logout)
builder.setPositiveButton(R.string.confirm) { dialogInterface: DialogInterface, i: Int ->
// logout logic ...
dialogInterface.dismiss()
context.startActivity(AuthActivity.newStartIntent(context))
(context as Activity).finish()
}
builder.setNegativeButton(R.string.cancel) { di: DialogInterface, _: Int -> }
builder.show()
}
and removed the finish() line seen on *1.
Thanks to #Johan Kovalski for the tip.
Try to dimiss your alert dialog before finish the activity.
yourAlertDialog.dimiss();
Dialog Window is created with activity Context. Activity has to cleanup windows that it owns. You should destroy dialog first, then activity. Or use DialogFragment.
Log you mentioned in question tells you that activity cannot be properly destroyed (as you requested with finish()) because dialog still holds reference to it.
Related
I'm a total newbie, and my app has a main activity and a test-taking activity. Normally pressing back button in the test activity takes you back to main activity. This is fine, but I want to add a confirmation dialog asking if they really want to abandon the test first. So far I have the following in the test activity:
override fun onBackPressed() {
var exit = ExitTestDialogFragment()
exit.show(supportFragmentManager,"exit")
}
class ExitTestDialogFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return activity?.let {
val builder = AlertDialog.Builder(it)
builder.setTitle("Leave Test?")
builder.setMessage("Your score will be lost.")
.setPositiveButton("OK",
DialogInterface.OnClickListener { dialog, id ->
// This is where I'd like to return to Main Activity
})
.setNegativeButton("Cancel",
DialogInterface.OnClickListener { dialog, id ->
dialog.dismiss()// User cancelled the dialog
})
// Create the AlertDialog object and return it
builder.setCancelable(false)
builder.create()
} ?: throw IllegalStateException("Activity cannot be null")
}
}
I can't seem to figure out how how do what would normally be the Activity's super.onBackPressed() from the dialog fragment. Like I said, I'm super new to android, so may need a bit of an ELI5 answer.
call finish() or this.finish() inside your DialogInterface.OnClickListener. Method finish() will destroy current activity that call it, in this case its test-taking activity
You should call mainActivity from your dialog positive Button.
.setPositiveButton("OK",
DialogInterface.OnClickListener { dialog, id ->
// here you can get your current activity
//then dismiss your dialog and finish current activity
//call context.finish or activity.finish here. It will
//finish this activity
//and will take you to the previous activity (in your case
//to mainActivity)
})
If you need any further help feel free to mention it in the comments
Add this in the container dialog
dialog?.setOnKeyListener { dialog, keyCode, event ->
if (keyCode == KeyEvent.KEYCODE_BACK && event.action == KeyEvent.ACTION_UP) {
handleBack() // your code
return#setOnKeyListener true
} else
return#setOnKeyListener false
}
There is ParentFragment that shows DialogFragment. I collect a dialog result through SharedFlow. When result received, dialog dismissed. Should I stop collect by additional code? What happens when dialog closed, but fragment still resumed?
// ParentFragment
private fun save() {
val dialog = ContinueDialogFragment(R.string.dialog_is_save_task)
dialog.show(parentFragmentManager, "is_save_dialog")
lifecycleScope.launch {
dialog.resultSharedFlow.collect {
when (it) {
ContinueDialogFragment.RESULT_YES -> {
viewModel.saveTask()
closeFragment()
}
ContinueDialogFragment.RESULT_NO -> {
closeFragment()
}
ContinueDialogFragment.RESULT_CONTINUE -> {
// dont close fragment
}
}
}
}
}
class ContinueDialogFragment(
#StringRes private val titleStringId: Int,
#StringRes private val messageStringId: Int? = null
) : DialogFragment() {
private val _resultSharedFlow = MutableSharedFlow<Int>(1)
val resultSharedFlow = _resultSharedFlow.asSharedFlow()
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return activity?.let { context ->
AlertDialog.Builder(context)
.setTitle(getString(titleStringId))
.setMessage(messageStringId?.let { getString(it) })
.setPositiveButton(getString(R.string.dialog_yes)) { _, _ ->
_resultSharedFlow.tryEmit(RESULT_YES)
}
.setNegativeButton(getString(R.string.dialog_no)) { _, _ ->
_resultSharedFlow.tryEmit(RESULT_NO)
}
.setNeutralButton(getString(R.string.dialog_continue)) { _, _ ->
_resultSharedFlow.tryEmit(RESULT_CONTINUE)
}
.create()
} ?: throw IllegalStateException("Activity cannot be null")
}
companion object {
const val RESULT_YES = 1
const val RESULT_NO = 0
const val RESULT_CONTINUE = 2
}
}
When a Flow completes depends on its original source. A Flow built with flowOf or asFlow() ends once it reaches the last item in its list. A Flow built with the flow builder could be finite or infinite, depending on whether it has an infinite loop in it.
A flow created with MutableSharedFlow is always infinite. It stays open until the coroutine collecting it is cancelled. Therefore, you are leaking the dialog fragment with your current code because you are hanging onto its MutableSharedFlow reference, which is capturing the dialog fragment reference. You need to manually cancel your coroutine or collection.
Or more simply, you could use first() instead of collect { }.
Side note, this is a highly unusual uses of a Flow, which is why you're running into this fragile condition in the first place. A Flow is for a series of emitted objects, not for a single object.
It is also very fragile that you're collecting this flow is a function called save(), but you don't appear to be doing anything in save() to store the instance state such that if the activity/fragment is recreated you'll start collecting from the flow again. So, if the screen rotates, the dialog will reappear, the user could click the positive button, and nothing will be saved. It will silently fail.
DialogFragments are pretty clumsy to work with in my opinion. Anyway, I would take the easiest route and directly put your behaviors in the DialogFragment code instead of trying to react to the result back in your parent fragment. But if you don't want to do that, you need to go through the pain of calling back through to the parent fragment. Alternatively, you could use a shared ViewModel between these two fragments that will handle the dialog results.
I believe you will have a memory leak of DialogFragment: ParentFragment will be referencing the field dialog.resultSharedFlow until the corresponding coroutine finishes execution. The latter may never happen while ParentFragment is open because dialog.resultSharedFlow is an infinite Flow. You can call cancel() to finish the coroutine execution and make dialog eligible for garbage collection:
lifecycleScope.launch {
dialog.resultSharedFlow.collect {
when (it) {
ContinueDialogFragment.RESULT_YES -> {
viewModel.saveTask()
closeFragment()
cancel()
}
ContinueDialogFragment.RESULT_NO -> {
closeFragment()
cancel()
}
ContinueDialogFragment.RESULT_CONTINUE -> {
// dont close fragment
}
}
}
}
I have an AlertDialog that I want to display at least once to the user and then continuously display the dialog to the user even after the user clicks "ok" until a certain condition is met.
Here's the code structure I have so far for the AlertDialog:
do {
val dialogShow: AlertDialog.Builder = AlertDialog.Builder(this#MainActivity)
dialogShow.setCancelable(false)
dialogShow.setMessage("Message")
.setPositiveButton(
"ok",
object : DialogInterface.OnClickListener {
override fun onClick(dialogInterface: DialogInterface, i: Int) {
if (checkCondition()) {
conditionMet = true
} else {
// Keep looping
}
}
})
.setNegativeButton(
"cancel",
object : DialogInterface.OnClickListener {
override fun onClick(dialogInterface: DialogInterface, i: Int) {
conditionMet = true
return
}
})
dialogShow.show()
} while (conditionMet == false)
The problem now that I am facing is the AlertDialog will display once, but then never again. Even if conditionMet = false it still won't continue to display. How do I keep displaying the same AlertDialog in a loop?
By wrapping the show code in a loop, you're showing it continuously. What you probably want to do it re-show the dialog if it is dismissed. So something like this pseudocode:
fun showObtrusiveDialog() {
...
dialog.setPositiveButton {
if(shouldStillBeObtrusive()) showObtrusiveDialog()
...
}.setNegativeButton {
...
}
dialog.show()
}
An alternate way to handle this would be to disable the buttons until you're ready to allow the dialog to be closed by the user. Here's an extension function you could call when your condition changes:
fun AlertDialog.setAllButtonsState(enabled: Boolean) {
arrayOf(DialogInterface.BUTTON_POSITIVE, DialogInterface.BUTTON_NEGATIVE, DialogInterface.BUTTON_NEUTRAL)
.forEach { getButton(it)?.setEnabled(enabled) }
}
So you can call this to disabled them before you show it, and call it again when your condition changes. You'll need to keep the dialog in a property so you can access it from wherever your condition is being changed.
When I click on the back button, I want to close the connection in service and go back to previous activity.
Code in second activity:
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
if (keyCode == KeyEvent.KEYCODE_BACK) {
if(mBound){
val dialog = ConfirmDialog()
dialog.show(supportFragmentManager, "confirm")
//should wait here until "yes" button is pressed and if so, run the code
mService.closeConnection()
finish()
}
return false
}
return super.onKeyDown(keyCode, event)
}
When the dialog opens the code continues and closes the connection and goes back to previous activity. I want to wait until I click the "yes" button and then continue code below. I know I should have the code inside dialog, but I can't bind the service to the dialog, so I can't stop the connection from there.
Dialog:
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val builder = AlertDialog.Builder(requireActivity())
builder.setView(view)
.setTitle("Confirm")
.setMessage("Do you want to exit?")
.setNegativeButton("no", DialogInterface.OnClickListener(){ _: DialogInterface, _: Int ->
//TODO
})
.setPositiveButton("yes", DialogInterface.OnClickListener(){ _: DialogInterface, _: Int ->
//TODO
})
return builder.create()
}
Again, what I want is simple, when clicking the back button, I can confirm or decline, if I confirm, close the connection in service and go back to previous activity. I guess there is an easier way to do this
I believe your ConfirmDialog() function returns an AlertDialog in which case you can access the components of your alerdialog view this way and close the connection on button click:
dialog.(your_yes_button_id).setOnClickListener {
}
the above code should go below the following:
dialog.show(supportFragmentManager, "confirm")
I am using fragments for my turn based game, how can i be sure that a fragment is visible and added to context in this kind of async-function before executing any code in that fragment?
fun respondToRematchInvitation(invitation : Invitation) {
if (winFragment.isVisible) {
val builder = AlertDialog.Builder(this)
builder.setTitle("Accept invitation for rematch?")
builder.setMessage("Are you sure?")
builder.setPositiveButton("Yes") { _, _ ->
turnBasedMultiplayerClient.acceptInvitation(invitation.invitationId)
.addOnSuccessListener {
Log.d(TAG, "Invitation accepted succesfully")
isDoingTurn = false
gameFragment = GameFragment()
supportFragmentManager.beginTransaction().replace(R.id.fragment_container, gameFragment).addToBackStack(null).commit()
onInitiateMatch(it) //error happens here
}.addOnFailureListener {
createFailureListener("Accepting invitation failed")
}
}
val dialog : AlertDialog = builder.create()
dialog.show()
Now in onInitiateMatch(it) i have some code that modifies gameFragment, for example possible receiving opponents game data and other initialization. Simply using if (gameFragment.isVisible) or if (gameFragment.isAdded) is not enough because that way onInitiateMatch(it) function might not be executed if that if-statement returns false.
Should I use threads for this?
It turned out, that executing supportFragmentManager.executePendingTransactions() solved the problem, I thought first it was not the a solution for the problem.