Description
This problem affects both Swift and Kotlin libraries.
Description
When a modal screen is shown (ModalContainerScreen
in Swift, ModalContainer
/AlertContainerScreen
in Kotlin), the workflow specifies a base screen to show under the modal, and a list of modals to show above the base screen. These screens are bound to special containers that hook into each platform's modal/dialog mechanisms to show standard dialogs when the modal list is non-empty. They both correctly support empty modal lists, going from empty to non-empty modal lists, and going back from non-empty to empty modal lists.
They also both support going from an arbitrary rendering to a modal rendering (with zero or more modals). However, neither of them support going from a modal rendering with a non-empty modal list to any other rendering. The workflow infrastructure tears down the modal container without telling it that it's going away, and the containers have no chance to clean up after themselves.
On iOS, this manifests as the modal disappearing without animation. On Android, the dialog is never dismissed but nobody has a handle so it stays up forever and gets leaked (until the user tries interacting with it, at which point it will crash because it has a stale rendering).
Workaround
If you have a workflow that is rendering a ModalContainerScreen
/AlertContainerScreen
, ensure that you're always rendering that screen. If you don't have any modals to show, just use an empty list.
Non-Solution 1: Force consistent renderings
This is a non-starter. Even if we can force a particular workflow to either always or never render a modal screen, we can't propagate that invariant to the parent. We definitely can't at compile time, and doing so at runtime is brittle and very easy to accidentally trigger. A significant part of the workflow architecture is that workflows can render whatever they want and either their parent or the view system will just deal with it.
Non-Solution 2: Automatically wrap renderings with modal screens
The infrastructure can insert a root workflow above whatever root "user" workflow is passed in that monitors for modal screens, and once it sees once always wraps all renderings in modal screens.
This is also a non-starter. It's hacky and brittle, and would require the platform-specific runtimes to know how to create instances of all possible modal screens, which isn't possible at least on Android (would need to have constructor configured for any HasModals
implementations that could possibly be rendered).
Proposed Solution: Provide cleanup hook in view bindings
I think this solution would work for iOS, but I'm not sure it would for Android. On iOS, the hook would simply need to animate any existing modals away.
On Android, it depends how the hook is implemented. If we just hook into the view lifecycle, we can dismiss all dialogs whenever the container is detached, but that would include rotation. The system already handles preserving dialogs across rotation, so this might cause non-standard behavior since we're manually dismissing and then recreating dialogs. Alternatively, if there is a higher-level hook in the binding itself that is only triggered when a particular ViewRegistry
-view is going away, it should work as long as containers actually handle rotation correctly to begin with. I don't think the extra complexity exists on iOS because the lifecycle is simpler – the ViewController
lifecycle matches the view binding lifecycle 1-to-1.
On Android, we can just hook into the container's view lifecycle and dismiss on detach.