Consume onBackPressed event using OnBackInvokedCallback in Activity? - android

According to the documentation from API 33 method onBackPressed() of Activity is deprecated. How can I use the suggested solution with OnBackInvokedCallback to prevent the parent method call?
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
// how to consume action here to prevent super.onBackPressed() trigger?
}
})
}
This question concerns the case where there is a hierarchy of custom Activity classes, in one of which I want to prevent this method from being called in the parents in certain cases.
The desired behavior with deprecated API is as follows:
override fun onBackPressed() {
when {
needDoSomethingOnlyHere -> {
// do something
}
needDoSomethingElseOnlyHere -> {
// do something else
}
else -> {
super.onBackPressed()
}
}
}

Related

Proper usage of OnBackPressedDispatcher

Sometimes, I want to handle the back button being pressed by the user myself. My (example) code looks something like this:
class TestActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.test)
}
override fun onBackPressed() {
if (checkSomeCondition()) {
// do nothing
} else {
super.onBackPressed()
}
}
private fun checkSomeCondition() = false
}
I get notified when back is pressed, and then I decide if I want to handle it, or let the system handle it by calling super.onBackPressed().
Since onBackPressed() is now deprecated, I replace it by using the OnBackPressedDispatcher:
class TestActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.test)
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (checkSomeCondition()) {
// do nothing
} else {
onBackPressedDispatcher.onBackPressed()
}
}
})
}
private fun checkSomeCondition() = false
}
The problem with this code: by calling onBackPressedDispatcher.onBackPressed(), I call my own callback, creating an infinite loop instead of handing this over to the system like I would with super.onBackPressed().
This can be circumvented when temporarily disabling my callback like:
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (checkSomeCondition()) {
// do nothing
} else {
this.isEnabled = false
onBackPressedDispatcher.onBackPressed()
this.isEnabled = true
}
}
})
but that seems rather awkward and doesn't seem like it was intended that way.
What's the correct usage of the OnBackPressedDispatcher here that lets me either handle the back button press myself or hand it over to the system?
PS: I have seen this question about the deprecation of onBackPressed, but it doesn't answer my much more specific question.
As per the Basics of System Back video, the whole point of the Predictive Back gesture is to know ahead of time what is going to handle back.
That means that you should never have just-in-time logic such as checkSomeCondition as part of your call to handleOnBackPressed.
Instead, as explained in the Custom back navigation documentation, you should be updating the isEnabled state of your OnBackPressedCallback ahead of time whenever the conditions you used to check in checkSomeCondition changed. This ensures that your callback is only invoked when your condition is already true and is disabled when the condition is false, thus allowing the default behavior to occur.
class TestActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.test)
// Instead of always setting enabled to true at the beginning,
// you need to check the state ahead of time to know what the
// initial enabled state should be
val isConditionAlreadySet = checkSomeCondition()
val callback = object : OnBackPressedCallback(
isConditionAlreadySet
) {
override fun handleOnBackPressed() {
// Since you've handled isEnabled correctly, you know
// your condition is set correctly here, so you can
// unconditionally do what you need to do to handle back
}
}
// This is the key part - now you know ahead of time
// when the condition changes, which lets you control
// when you want to handle back or not
setOnSomeConditionChangedListener { isConditionMet ->
callback.isEnabled = isConditionMet
}
onBackPressedDispatcher.addCallback(this, callback)
}
private fun checkSomeCondition() = false
private fun setOnSomeConditionChangedListener(
(isConditionMet: Boolean) -> Unit
) {
// The implementation here will depend on what
// conditions checkSomeCondition() actually depends on
}
}

onBackPressed() deprecated, What is the alternative?

I have upgraded targetSdkVersion and compileSdkVersion to 33.
Now getting warning onBackPressed is deprecated.
It is suggested to use use OnBackInvokedCallback or androidx.activity.OnBackPressedCallback to handle back navigation instead. Anyone can help me to use the updated method.
Example:
Use Case: I use if (isTaskRoot) {} inside onBackPressed(){} method to check activity is last on the activity-stack.
override fun onBackPressed() {
if (isTaskRoot) { // Check this activity is last on the activity-stack.(Check Whether This activity opened from Push-Notification)
startActivity(Intent(mContext, Dashboard::class.java))
finish()
} else {
finishWithResultOK()
}
}
According your API level register:
onBackInvokedDispatcher.registerOnBackInvokedCallback for API level 33+
onBackPressedDispatcher callback for backword compatibility "API level 13+"
This requires to at least use appcompat:1.6.0-alpha03; the current is 1.6.0-alpha04:
implementation 'androidx.appcompat:appcompat:1.6.0-alpha04'
// kotlin
import androidx.activity.addCallback
if (BuildCompat.isAtLeastT()) {
onBackInvokedDispatcher.registerOnBackInvokedCallback(
OnBackInvokedDispatcher.PRIORITY_DEFAULT
) {
// Back is pressed... Finishing the activity
finish()
}
} else {
onBackPressedDispatcher.addCallback(this /* lifecycle owner */, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
// Back is pressed... Finishing the activity
finish()
}
})
}
// ====================================================
/* Or for lambda simplicity: */
// ====================================================
if (BuildCompat.isAtLeastT()) {
onBackInvokedDispatcher.registerOnBackInvokedCallback(
OnBackInvokedDispatcher.PRIORITY_DEFAULT
) {
// Back is pressed... Finishing the activity
finish()
}
} else {
onBackPressedDispatcher.addCallback(this /* lifecycle owner */) {
// Back is pressed... Finishing the activity
finish()
}
}
UPDATE:
Thanks to #ianhanniballake comment; you can just use OnBackPressedDispatcher even in API level 33+
The OnBackPressedDispatcher is already going to be using the Android T specific API internally when using Activity 1.6+,
So, you can just do:
// kotlin
import androidx.activity.addCallback
onBackPressedDispatcher.addCallback(this /* lifecycle owner */, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
// Back is pressed... Finishing the activity
finish()
}
})
// ====================================================
/* Or for lambda simplicity: */
// ====================================================
onBackPressedDispatcher.addCallback(this /* lifecycle owner */) {
// Back is pressed... Finishing the activity
finish()
}
// java
import androidx.activity.OnBackPressedCallback;
getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) {
#Override
public void handleOnBackPressed() {
// Back is pressed... Finishing the activity
finish();
}
});
Note that you shouldn't override the onBackPressed() as that will make the onBackPressedDispatcher callback not to fire; check this answer for clarifying that.
Replace onBackPressed() with below code.
onBackPressedDispatcher.onBackPressed()
With a combination of top answers. Here is a solution :
1. When you need to press the back button, copy this :
Note: it will automatically destroy your activity.
button.setOnClickListener {
onBackPressedDispatcher.onBackPressed()
}
2. When you need to handle the back button pressed, copy this :
onBackPressedDispatcher.addCallback(this, object: OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
// Whatever you want
// when back pressed
println("Back button pressed")
finish()
}
})
Simply replace
override fun onBackPressed() {
super.onBackPressed() //Replace this is deprecated line
}
with
override fun onBackPressed() {
onBackPressedDispatcher.onBackPressed() //with this line
}
Update:
As we know override fun onBackPressed() is also deprecated so also replace this function with code below:
onBackPressedDispatcher.addCallback(this, object: OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
println("Back button pressed")
// Code that you need to execute on back press i.e. finish()
}
})
The above code we need to write inside some method like onCreate() function.
You could use the onBackPressedDispatcher
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
}
})
in here "this" means the lifeCycleOwner
You can use the OnBackInvokedCallback
as described in the documentation and follow this guide here to update your code
In Kotlin, this way is working
1- Remove onBackPressed()
2- below onCreate(savedInstanceState: Bundle?) add these lines:
if (Build.VERSION.SDK_INT >= 33) {
onBackInvokedDispatcher.registerOnBackInvokedCallback(
OnBackInvokedDispatcher.PRIORITY_DEFAULT
) {
exitOnBackPressed()
}
} else {
onBackPressedDispatcher.addCallback(
this,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
Log.i("TAG", "handleOnBackPressed: Exit")
exitOnBackPressed()
}
})
}
3- Define a new function for handling
fun exitOnBackPressed() {
}
Bonus: To close DrawerLayout when onBackPressed use like below (according to this I/O talk),
val callback = onBackPressedDispatcher.addCallback(this, false) {
binding.drawerLayout.closeDrawer(GravityCompat.START)
}
binding.drawerLayout.addDrawerListener(object : DrawerListener {
override fun onDrawerOpened(drawerView: View) {
callback.isEnabled = true
}
override fun onDrawerClosed(drawerView: View) {
callback.isEnabled = false
}
override fun onDrawerSlide(drawerView: View, slideOffset: Float) = Unit
override fun onDrawerStateChanged(newState: Int) = Unit
})
Here is the extension function to implement OnBackPressedCallback in activity.
fun AppCompatActivity.addOnBackPressedDispatcher(onBackPressed: () -> Unit = { finish() }) {
onBackPressedDispatcher.addCallback(
this,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
onBackPressed.invoke()
}
}
)
}
Usage:
addOnBackPressedDispatcher {
//doSomething()
}
use onBackPressedDispatcher.onBackPressed() instead of super.onBackPressed()
override fun onBackPressed() {
onBackPressedDispatcher.onBackPressed()
}
override the onDismiss() function for BottomSheets.
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
//TODO your logic.
}
This is the easy way to call the default Android BackButton since onBackPressed has been deprecated:
this#MyActivity.onBackPressedDispatcher.onBackPressed()
example:
myCloseButton.setOnClickListener { this#MyActivity.onBackPressedDispatcher.onBackPressed() }
Use like below,
override fun onClick(v: View?) {
when (v?.id) {
R.id.iv_back -> onBackPressedMethod()
}
}
and now create that method for handling back event
private fun onBackPressedMethod(){
if (Build.VERSION.SDK_INT >= 33) {
onBackInvokedDispatcher.registerOnBackInvokedCallback(
OnBackInvokedDispatcher.PRIORITY_DEFAULT) {
// back button pressed... finishing the activity
finish()
}
} else {
onBackPressedDispatcher.addCallback(
this,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
// back button pressed... finishing the activity
finish()
}
})
}
}
That's it!

Using androix backButtonPressedCallback

I need to implement a custom onBackButtonPress method for Fragments. example in LogoutFragment, after logout is handled. user cannot go backStack but a message is shown like press again to exist and is exited. I used this solution. But is not working. Then I saw this Android Doc with onBackPressedDispatcher callback method CODE BELOW. I guess this will work. I added dependencies, but how to implement this in a different fragments, with only 1 activity and fragment container. Kotlin version.
class LogoutFragment : DaggerFragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val callback = requireActivity().onBackPressedDispatcher.addCallback(this) {
...
}
...
}
...
}
onBackPressedDispatcher gives an opportunity to handle back press differently in every fragment. Hence you may have to give this piece of code in every fragment.
In the fragment:
var doubleBackToExitPressedOnce = false
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
requireActivity().onBackPressedDispatcher.addCallback(this, callback)
}
val callback = object : OnBackPressedCallback(true ) {
override fun handleOnBackPressed() {
if (doubleBackToExitPressedOnce)
requireActivity().finish()
Toast.makeText(requireContext(), "Press again to go back",
Toast.LENGTH_LONG).show()
doubleBackToExitPressedOnce = true
}
}
If you want to handle back presses of all fragments in one place, you can do so by manipulating the doBack() method in your reference: How to implement onBackPressed() in Fragments?
//pseudocode
fun doBack()
{
//find the fragment
val fragment = supportFragmentManager.findFragmentByTag(
//your fragment tag
)
if(fragment is FragmentA)
{
//do something
}
else if(fragment is FragmentB)
{
//do something else
}
else
{
activity.getSupportFragmentManager().popBackStack(null,
FragmentManager.POP_BACK_STACK_INCLUSIVE);
}
}

Kotlin synthetic view in activity gets null when a custom dialog is shown and dismissed

I am updating a view in activity's onCreate method which is working fine using kotlin extension as stated below.
Activity's onCreate
import kotlinx.android.synthetic.main.activity_otpverification.*
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_otpverification)
tvContactNumber.text = getString(R.string.dual_string_value_placeholder)
}
Then at a click of a button I am showing a custom dialog for performing some action. When the dialog is dismissed I update the same textView in activity with the data sent from dialog, but the view tvContact is throwing null exception.
Activity's onClick
override fun onClick(p0: View?) {
when (p0?.id) {
R.id.ivEdit -> {
object : ChangeNumberDialog(this) {
override fun onSubmitClicked(number: String) {
tvContactNumber.text =number
}
}.show()
}
}
}
onSubmitClicked is an abstract method in the dialog which is triggered when the dialog is dismissed.
Error from logcat :
java.lang.IllegalStateException: tvContactNumber must not be null
at com.beat.em.ui.activities.OTPVerificationActivity$onClick$1.onSubmitClicked
(OTPVerificationActivity.kt:211)
onCreate and onClick methods from the ChangeNumberDialog:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val view = layoutInflater.inflate(R.layout.dialog_change_number, null, false)
setContentView(view)
setCanceledOnTouchOutside(false)
setCancelable(true)
tvSubmit.setOnClickListener(this)
}
override fun onClick(view: View) {
when (view.id) {
R.id.tvSubmit -> {
onSubmitClicked(etNumber.text.toString().trim())
dismiss()
}
}
}
I have just started using kotlin extension and not able to understand the cause. Help appreciated.
The variable you are trying to access is in another scope, try to add explicit scope to the view i.e.
this#YourActivity.tvContactNumber.text = number

Show confirmation on back/up in Fragment with Navigation Architecture Component

I am using the Navigation Architecture Component for Android.
For one of my fragments I wish to intercept "back" and "up" navigation, so that I can show a confirmation dialog before discarding any unsaved changes by the user. (Same behavior as the default Calendar app when you press back/up after editing event details)
My current approach (untested) is as follows:
For "up" navigation, I override onOptionsItemSelected on the fragment:
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
if(item?.itemId == android.R.id.home) {
if(unsavedChangesExist()) {
// TODO: show confirmation dialog
return true
}
}
return super.onOptionsItemSelected(item)
}
For "back" navigation, I created a custom interface and callback system between the fragment and its activity:
interface BackHandler {
fun onBackPressed(): Boolean
}
class MainActivity : AppCompatActivity() {
...
val backHandlers: MutableSet<BackHandler> = mutableSetOf()
override fun onBackPressed() {
for(handler in backHandlers) {
if(handler.onBackPressed()) {
return
}
}
super.onBackPressed()
}
...
}
class MyFragment: Fragment(), BackHandler {
...
override fun onAttach(context: Context) {
super.onAttach(context)
if (context is MainActivity) {
context.backHandlers.add(this)
}
}
override fun onDetach() {
(activity as? MainActivity)?.backHandlers?.remove(this)
super.onDetach()
}
override fun onBackPressed(): Boolean {
if(unsavedChangedExist()) {
// TODO: show confirmation dialog
return true
}
}
...
}
This is all pretty gross and boilerplatey for such a simple thing. Is there a better way?
As of androidx.appcompat:appcompat:1.1.0-beta01, in order to intercept the back button with navigation component you need to add a callback to the OnBackPressedDispatcher. This callback has to extend OnBackPressedCallback and override handleOnBackPressed. OnBackPressedDispatcher follows a chain of responsibility pattern to handle the callbacks. In other words, if you set your callback as enabled, only your callback will be executed. Otherwise, OnBackPressedDispatcher will ignore it and proceed to the next callback, and so on until it finds an enabled one (this might be useful when you have more than one callback, for instance). More info on this here.
So, in order to show your dialog, you would have to do something similar to this in your Fragment:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val callback = requireActivity().onBackPressedDispatcher.addCallback(this) {
// Show your dialog and handle navigation
}
// you can enable/disable the callback here by setting
// callback.isEnabled = true/false. Or just enable it in the lambda.
}
That addCallback method takes in a LifecycleOwner, and it will make sure that the callback is added when LifecycleOwner reaches the STARTED stage. Not only that, but this also makes it so that the callback is removed when its associated LifecycleOwner is destroyed.
As for the up button, it seems like (at least for now) there aren't many possibilities. The only option I could find up until now that uses the navigation component is to add a listener for the navigation itself, which would handle both buttons at the same time:
navController.addOnDestinationChangedListener { navController, destination ->
if (destination.id == R.id.destination) {
// do your thing
}
}
Regardless, this has the caveat of allowing the activity or fragment where you add the listener to know about destinations it might not be supposed to.
With the navigation architecture components, you can do something like this:
Tell your activity to dispatch all up clicks on the home button(back arrow) to anyone listening for it. This goes in your activity.
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) {
onBackPressedDispatcher.onBackPressed()
return true
}
return super.onOptionsItemSelected(item)
}
Then in your fragments, consume the events like so
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requireActivity().onBackPressedDispatcher.addCallback(this) {
if (*condition for showing dialog here*) {
// Show dialog
} else {
// pop fragment by calling function below. Analogous to when the user presses the system UP button when the associated navigation host has focus.
findNavController().navigateUp()
}
}
}
for up navigation simply override onOptionsItemSelected()
override fun onOptionsItemSelected(item: MenuItem): Boolean =
when (item.itemId) {
android.R.id.home -> {
showDialog() // show your dialog here
true
}
else -> super.onOptionsItemSelected(item)
}
You can used following function in onAttach in your fragment to override the onBackPressed() with help of the navigation components.
requireActivity().onBackPressedDispatcher.addCallback(
this,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (YOUR_CONDITION) {
// Do something here
} else {
if (!findNavController().navigateUp()) {
if (isEnabled) {
isEnabled = false
requireActivity().onBackPressedDispatcher.onBackPressed()
}
}
}
}
}
)
If you're using it with AppBarConfiguration, with the latest release there is now an AppBarConfiguration.OnNavigateUpListener. Refer to the below link for more information
https://developer.android.com/reference/androidx/navigation/ui/AppBarConfiguration.OnNavigateUpListener
if u override onBackPressed() in your activity must ensure that it should call super.onBackOnBackPressed() otherwise these dispatcher wont trigger

Categories

Resources