Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Proper usage of OnBackPressedDispatcher

Tags:

android

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.

like image 884
fweigl Avatar asked Sep 07 '25 08:09

fweigl


2 Answers

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
    }

}
like image 112
ianhanniballake Avatar answered Sep 09 '25 04:09

ianhanniballake


As we can't use onBackPressedDispatcher for triggering default back pressed behavior after execution our action, there is another solution to get both for newer Android (onBackInvokedDispatcher) versions on older ones (onBackPressed still can be used for older versions):

in Manifest:

<application
    android:enableOnBackInvokedCallback="true"

In Activity:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
        onBackInvokedDispatcher.registerOnBackInvokedCallback(1000) {
            onBackAction()
            onBackPressedDispatcher.onBackPressed()
        }
    }
}

@Deprecated("Deprecated in Java")
override fun onBackPressed() {
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
        onBackAction()
    }
    @Suppress("DEPRECATION")
    super.onBackPressed()
}

private fun onBackAction() {
    // your action
    // p.s. if you use it for compose screen logic, 
    // the just implement some logic to check
    // current screen based on current navController's route
}
like image 36
user924 Avatar answered Sep 09 '25 05:09

user924