Zhuinden / simple-stack

[ACTIVE] Simple Stack, a backstack library / navigation framework for simpler navigation and state management (for fragments, views, or whatevers).
Apache License 2.0
1.36k stars 76 forks source link

BackHandler in Compose (and any backPressDispatcher callback) only receives backs in super.onBackPressed, but should be received before backstack.goBack #248

Closed Zhuinden closed 2 years ago

Zhuinden commented 2 years ago

Googlers made back handling much trickier than it should be.

Using backstack.goBack() before super.onBackPressed() means that the dispatcher won't receive it before the backstack going back.

    /**
     * Called when the activity has detected the user's press of the back
     * key. The {@link #getOnBackPressedDispatcher() OnBackPressedDispatcher} will be given a
     * chance to handle the back button before the default behavior of
     * {@link android.app.Activity#onBackPressed()} is invoked.
     *
     * @see #getOnBackPressedDispatcher()
     */
    @Override
    @MainThread
    public void onBackPressed() {
        mOnBackPressedDispatcher.onBackPressed();
    }

Basically, Jetpack team should have probably made onBackPressed final with this new current approach, as the order cannot be correct if you do anything before calling super.onBackPressed.

The solution is to register an always-enabled callback, but the Activity actually tries to close the action bar first instead of finishing. That call is inaccessible outside of ComponentActivity, which does not expose ComponentActivity.super.onBackPressed() in any way.

        onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { // always intercept.
            override fun handleOnBackPressed() {
                val topKey = backstack.top<FragmentKey>()
                val fragment = supportFragmentManager.findFragmentByTag(topKey.fragmentTag)
                if (fragment is BackHandler) {
                    val handled = fragment.handleBack()
                    if (handled) {
                        return
                    }
                }
                if (!Navigator.onBackPressed(this@MainActivity)) {
                    finish() // we cannot access Activity.onBackPressed() anymore. RIP
                }
            }
        })

I don't see a way to access Activity.onBackPressed() because super.onBackPressed() would just send to the dispatcher again, causing an infinite loop.


I wonder if it is possible to call Activity.onBackPressed() using reflection, this requires investigation. However, this basically means that the way backstack.goBack() is called before super.onBackPressed, it intercepts back from fragments that would normally register themselves as a dispatcher.

It works in the multi-stack sample because I manually manage isActiveForBack with onStart, instead of relying on the back press dispatcher.

https://github.com/Zhuinden/simple-stack/blob/dbd1b1fd12ac20e10c01b6d8be03ade60ccc3868/samples/multistack-samples/simple-stack-example-multistack-nested-fragment/src/main/java/com/zhuinden/simplestackbottomnavfragmentexample/core/navigation/FragmentStackHost.kt#L31

Zhuinden commented 2 years ago

I have been told that you can do callback.remove(); super.onBackPressed(); backPressedDispatcher.add(callback) 😂 😢

Zhuinden commented 2 years ago

Btw, same as https://github.com/Zhuinden/simple-stack-compose-integration/issues/12