Proper way to wait that fragment is visible - android

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.

Related

When flow collect stop itself?

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

How do I continuously show an AlertDialog in a do while loop until a certain condition is met?

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.

java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first.(Android/Kotlin)

I am receiving the above error on my Alert dialog but not sure why or how to fix it.
I believe it stems from the function below.
Basically when In my app, navigate to the detail fragment then click the send button in the app to share an SMS the app crashes.
please take a look at my code. Any help is appreciated.
if (sendSmsStarted && permissionGranted) {
context?.let {
val smsInfo = SmsInfo(
"",
"${currentDog?.dogBreed} bred for ${currentDog?.bredFor}",
currentDog?.imageUrl
)
val diaologBinding: SendSmsDiaologBinding =
DataBindingUtil.inflate<SendSmsDiaologBinding>(
LayoutInflater.from(it),
R.layout.send_sms_diaolog, null, false
)
androidx.appcompat.app.AlertDialog.Builder(it).setView(databinding.root)
.setPositiveButton("Send SMS") { dialog: DialogInterface, which ->
if (!diaologBinding.smsInfo.toString().isNullOrEmpty()) {
smsInfo.to = diaologBinding.smsInfo.toString()
sendSms(smsInfo)
}
}
.setNegativeButton("Cancel") { dialog: DialogInterface, which -> }
.show()
diaologBinding.smsInfo = smsInfo
}
}
}```
I believe you intended to use the freshly inflated diaologBinding.root and not databinding.root as the dialog view. The latter looks like something you're already using for another purpose.

Some Questions about BiometricPrompt on Android

I'm trying to figure out how to modify (if it's possible) the normal behavior of biometricPrompt, in particular i want to display Gandalf, when the authentication fails.
I'm currently displaying it with a custom alertDialog, but it remains in background, with the biometricPrompt fragment on foreground exactly like this, and it loses all of its dumbness...
The best solution would probably be to display both, alertDialog and biometricPrompt, on foreground, displaying the image only in the upper half of the screen, but at the moment I've no idea of how to do it, or better, I have no idea how to link layouts together to manage size / margins and everything else.
The other thing I was thinking is to remove the biometricPrompt, so the alert dialog will be put on foreground, but any solution I've tried has failed miserably.
Any type of help/ideas will be welcome.
Anyway, here's the code:
class BiometricPromptManager(private val activity: FragmentActivity) {
private val cryptoManager = CryptoManager(activity)
fun authenticateAndDecrypt(failedAction: () -> Unit, successAction: (String) -> Unit) {
// display biometric prompt, if the user is authenticated, the decryption will start
// if biometric related decryption gives positives results, the successAction will start services data decryption
val executor = Executors.newSingleThreadExecutor()
val biometricPrompt = BiometricPrompt(activity, executor, object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
cryptoManager.startDecrypt(failedAction,successAction)
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
activity.runOnUiThread { failedAction() }
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
activity.runOnUiThread { failedAction() }
}
})
val promptInfo = biometricPromptInfo()
biometricPrompt.authenticate(promptInfo)
}
private fun biometricPromptInfo(): BiometricPrompt.PromptInfo {
return BiometricPrompt.PromptInfo.Builder()
.setTitle("Fingerprint Authenticator")
.setNegativeButtonText(activity.getString(android.R.string.cancel))
.build()
}
}
Open biometric auth from activity :
private fun openBiometricAuth(){
if(sharedPreferences.getBoolean("fingerPrintEnabled",false)) {
if (BiometricManager.from(this).canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS) { // check for hardware/permission
biometric.visibility = View.VISIBLE
BiometricPromptManager(this).authenticateAndDecrypt(::failure, ::callDecryption)
}
}
}
What to do when the user is not recognized :
private fun failure(){
val view = layoutInflater.inflate(R.layout.gandalf, null)
val builder = AlertDialog.Builder(this)
builder.setView(view)
builder.setPositiveButton("Dismiss") { dialog: DialogInterface, id: Int -> dialog.cancel() }
val alertDialog = builder.create()
alertDialog.show()
}
The Biometric API itself handles failed attempts at authentication the following way:
For each failed attempt the onAuthenticationFailed() callback is invoked.
The user gets 5 tries, after the 5th attempt fails the onAuthenticationError() callback receives the error code ERROR_LOCKOUT and the user must wait for 30 seconds to try again.
Showing your dialog inside onAuthenticationFailed() may be too eager and may result in poor user experience. And so a good place might be after you get the ERROR_LOCKOUT. The way the AndroidX Biometric Library works is that it dismisses the BiometricPrompt when the error is sent. Therefore you should have little problem showing your own dialog at that point.
In any case -- i.e. beyond these opinions -- the more general approach is to call cancelAuthentication() to dismiss the prompt and then proceed to showing your own dialog that way.
Also please follow blogpost1 and blogpost2 for recommended design pattern for BiometricPrompt.

How to avoid opening a new Alert dialog if there is one opened already in a fragment

I'm working on this fragment where I have two edit texts and when clicked individually opens an alert dialog. I have been facing this issue where, when the edit text is clicked one after another fastly, it opens two alert dialogs one above another. Is there any way I can avoid this.
I've tried using <item name="android:splitMotionEvents">false</item> in the AppTheme and also used this solution but these solutions solve a different scenario.
Any pointers are appreciated
You can disable the other EditText after the first one is clicked so that only one of the two can be clicked. Re-enable it when the dialog dismiss.
val input1 : EditText = findViewById(R.id.input1)
val input2 : EditText = findViewById(R.id.input2)
input1.setOnFocusChangeListener { _, b ->
if(b)
{
input2.isEnabled = false
AlertDialog.Builder(this).setTitle("input1").setMessage("input1").setNeutralButton("ok"){ _, _ ->
input2.isEnabled = true
}.create().show()
}
}
input2.setOnFocusChangeListener { _, b ->
if(b)
{
input1.isEnabled = false
AlertDialog.Builder(this).setTitle("input2").setMessage("input2").setNeutralButton("ok"){ _, _ ->
input1.isEnabled = true
}.create().show()
}
}
You can use the solution here if you do the following:
Instead of calling the extension View.setSafeOnClickListener, set SafeClickListener to a local field:
val safeClickListener = SafeClickListener { view ->
when(view.id) {
R.id.input1 -> // onClick event for input1
R.id.input2 -> // onClick event for input2
}
}
then set the click listener for those views to use the same instance of SafeClickListener:
findViewById<View>(R.id.input1).setOnClickListener(safeClickListener)
findViewById<View>(R.id.input2).setOnClickListener(safeClickListener)
This way the interval used in SafeClickListener will prevent a rapid second click event from happening on either of the Views.
There can be multiple way in which you can handle this scenario. If you need to keep it clean without involving any boolean variable, then use isShowing() method.
you can refer following snippet.
AlertDialog alert = new AlertDialog.Builder(context).create();
if (alert.isShowing()) {
// don't do anything
}
else {
// show dialog
}
You can use isShowing() method
AlertDialog yourAlertDialog = new AlertDialog.Builder(getContext()).create();
if(!yourAlertDialog.isShowing()){
//Here you can show your dialog else not
yourAlertDialog.show()
}
IN KOTLIN
val alertDialog= activity?.let { AlertDialog.Builder(it).create() };
if(!alertDialog?.isShowing!!){
//Here you can show your dialog else not
alertDialog.show()
}
InCase you have two or more dialogs ,
val alertDialog1= activity?.let { AlertDialog.Builder(it).create() };
val alertDialog2= activity?.let { AlertDialog.Builder(it).create() };
val alertDialog3= activity?.let { AlertDialog.Builder(it).create() };
if(alertDialog1?.isShowing!! && alertDialog1?.isShowing!!){
//Some dialog is shown
}else{
//Here you can show your dialog else not
alertDialog3.show()
}

Categories

Resources