1

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.

fweigl
  • 21,278
  • 20
  • 114
  • 205

1 Answers1

1

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
    }

}
ianhanniballake
  • 191,609
  • 30
  • 470
  • 443
  • @ianhanniballake , I understand the new ahead-of-time approach. It suggests either a custom back callback OR system back handling. However, what if I want a custom back callback AND system back handling? For instance, I want to intervene in the system back handling and run "Analytics.track("BACK_PRESSED")" before the system's behavior. How can I achieve this? – irfano Mar 05 '23 at 11:27
  • @irfano - you cannot do that. You can add your analytics to the `onDestroy` method though - using `isFinishing()` (in an Activity) or `isRemoving()` (in a Fragment) to know when you are being popped. – ianhanniballake Mar 05 '23 at 16:56