Skip to content

Simple Stack 2.7.0

Compare
Choose a tag to compare
@Zhuinden Zhuinden released this 31 Mar 17:27
· 24 commits to master since this release
26b35f3

Simple-Stack 2.7.0 (2023-03-31)

  • MAJOR FEATURE ADDITION: Added Backstack.setBackHandlingModel(BackHandlingModel.AHEAD_OF_TIME) to
    support android:enableBackInvokedCallback="true" on Android 14 for predictive back gesture support.

With this, Navigator.Installer.setBackHandlingModel(), BackstackDelegate.setBackHandlingModel(),
and Backstack.setBackHandlingModel() are added.

Also, ServiceBinder.getAheadOfTimeBackCallbackRegistry() is added as a replacement for ScopedServices.HandlesBack.
Please note that using it requires AHEAD_OF_TIME mode, and without it, trying to
use ServiceBinder.getAheadOfTimeBackCallbackRegistry() throws an exception.

Also, Backstack.willHandleAheadOfTimeBack(), Backstack.addAheadOfTimeWillHandleBackChangedListener()
and Backstack.removeAheadOfTimeWillHandleBackChangedListener() are added.

IMPORTANT:

The AHEAD_OF_TIME back handling model must be enabled similarly to how setScopedServices() or other similar configs
must be called before backstack.setup(), Navigator.install(), or BackstackDelegate.onCreate().

When AHEAD_OF_TIME is set, the behavior of goBack() changes. Calling goBack() when willHandleAheadOfTimeBack()
returns false throws an exception.

When AHEAD_OF_TIME is set, ScopedServices.HandlesBack will no longer be called (as it cannot return whether a
service WILL handle back or not), and should be replaced with registrations to the AheadOfTimeBackCallbackRegistry.

When AHEAD_OF_TIME is NOT set (and therefore the default, EVENT_BUBBLING is set),
calling willHandleAheadOfTimeBack or addAheadOfTimeWillHandleBackChangedListener
or removeAheadOfTimeWillHandleBackChangedListener throws an exception.

To migrate to use the ahead-of-time back handling model, then you might have the previous
somewhat onBackPressedDispatcher-compatible (but not predictive-back-gesture compatible) code:

class MainActivity : AppCompatActivity(), SimpleStateChanger.NavigationHandler {
    private lateinit var fragmentStateChanger: DefaultFragmentStateChanger

    @Suppress("DEPRECATION")
    private val backPressedCallback = object: OnBackPressedCallback(true) {
        override fun handleOnBackPressed() {
            if (!Navigator.onBackPressed(this@MainActivity)) {
                this.remove() 
                onBackPressed() // this is the reliable way to handle back for now 
                this@MainActivity.onBackPressedDispatcher.addCallback(this)
            }
        }
    }
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        onBackPressedDispatcher.addCallback(backPressedCallback) // this is the reliable way to handle back for now

        val binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        
        fragmentStateChanger = DefaultFragmentStateChanger(supportFragmentManager, R.id.container)
        
        Navigator.configure()
            .setStateChanger(SimpleStateChanger(this))
            .install(this, binding.container, History.single(HomeKey))
    }

    override fun onNavigationEvent(stateChange: StateChange) {
        fragmentStateChanger.handleStateChange(stateChange)
    }
}

This code changes to the following in order to support predictive back gesture using ahead-of-time model:

class MainActivity : AppCompatActivity(), SimpleStateChanger.NavigationHandler {
    private lateinit var fragmentStateChanger: FragmentStateChanger

    private lateinit var authenticationManager: AuthenticationManager

    private lateinit var backstack: Backstack

    private val backPressedCallback = object : OnBackPressedCallback(false) { // <-- !
        override fun handleOnBackPressed() {
            backstack.goBack()
        }
    }

    private val updateBackPressedCallback = AheadOfTimeWillHandleBackChangedListener { // <-- !
        backPressedCallback.isEnabled = it // <-- !
    }

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

        setContentView(R.layout.main_activity)

        onBackPressedDispatcher.addCallback(backPressedCallback) // <-- !

        fragmentStateChanger = FragmentStateChanger(supportFragmentManager, R.id.container)

        backstack = Navigator.configure()
            .setBackHandlingModel(BackHandlingModel.AHEAD_OF_TIME) // <-- !
            .setStateChanger(SimpleStateChanger(this))
            .install(this, binding.container, History.single(HomeKey))

        backPressedCallback.isEnabled = backstack.willHandleAheadOfTimeBack() // <-- !
        backstack.addAheadOfTimeWillHandleBackChangedListener(updateBackPressedCallback) // <-- !
    }

    override fun onDestroy() {
        backstack.removeAheadOfTimeWillHandleBackChangedListener(updateBackPressedCallback); // <-- !
        super.onDestroy()
    }

    override fun onNavigationEvent(stateChange: StateChange) {
        fragmentStateChanger.handleStateChange(stateChange)
    }
}

Please make sure to remove the AheadOfTimeWillHandleBackChangedListener in onDestroy (Activity) or onDestroyView (
Fragment), because the listener staying registered would be a memory leak.

A "lifecycle-aware" callback might be added to simple-stack-extensions later.

If you can't update to the AHEAD_OF_TIME back handling model, then don't worry, as backwards compatibility has been
preserved with the previous behavior.

When using AHEAD_OF_TIME back handling model, ScopedServices.HandlesBack is no longer called. To replace this, you
might have had something like this:

class FragmentStackHost(
    initialKey: Any
) : Bundleable, ScopedServices.HandlesBack {
    var isActiveForBack: Boolean = false
    
    // ...
    
    override fun onBackEvent(): Boolean {
        if (isActiveForBack) {
            return backstack.goBack()
        } else {
            return false
        }
    }
}

This is replaced like so:

class FragmentStackHost(
    initialKey: Any,
    private val aheadOfTimeBackCallbackRegistry: AheadOfTimeBackCallbackRegistry,
) : Bundleable, ScopedServices.Registered {
    var isActiveForBack: Boolean = false
        set(value) {
            field = value
            backCallback.isEnabled = value && backstackWillHandleBack
        }

    private var backstackWillHandleBack = false
        set(value) {
            field = value
            backCallback.isEnabled = isActiveForBack && value
        }

    private val backCallback = object : AheadOfTimeBackCallback(false) {
        override fun onBackReceived() {
            backstack.goBack()
        }
    }

    private val willHandleBackChangedListener = AheadOfTimeWillHandleBackChangedListener {
        backstackWillHandleBack = it
    }

    init {
        // ...
        backstackWillHandleBack = backstack.willHandleAheadOfTimeBack()
        backstack.addAheadOfTimeWillHandleBackChangedListener(willHandleBackChangedListener)
    }

  override fun onServiceRegistered() {
    aheadOfTimeBackCallbackRegistry.registerAheadOfTimeBackCallback(backCallback)
  }

  override fun onServiceUnregistered() {
    aheadOfTimeBackCallbackRegistry.unregisterAheadOfTimeCallback(backCallback)
  }
}

Where FragmentStackHost gets the AheadOfTimeBackCallbackRegistry
from serviceBinder.getAheadOfTimeBackCallbackRegistry().

So in this snippet, whether back will be handled needs to be propagated up, and manage the enabled state of
the AheadOfTimeBackCallback to intercept back if needed.

While this might seem a bit tricky, this is how Google does it in their own micromanagement of communicating with
the onBackPressedDispatcher as well, so evaluating ahead of time who will want to handle back later is unavoidable.

  • DEPRECATED: BackstackDelegate.onBackPressed() and Navigator.onBackPressed(). Not only are they the same
    as backstack.goBack() and merely managed to confuse people historically, but this deprecation mirros the deprecation
    of onBackPressed in compileSdk 33, to push towards using predictive back.