-
Notifications
You must be signed in to change notification settings - Fork 112
Do not show Dialogs before Activity content view is attached.
#1003
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
In anticipation of fix for #1001
cbc58b5 to
5fd5d47
Compare
If the very first rendering would instantiate `Dialog` windows, their content views were getting attached before the `Activity`'s content view. This would cause us to restore saved view state in the wrong order. If a `ComposeView` is in a `Dialog` in this situation, we would crash with an `IllegalStateException` from `SavedStateRegistry.consumeRestoredStateForKey`. The fix is for `LayeredDialogSessions` to memoize update calls received while detached, and replay them at attach time. The details are a little more complicated than that, though: * We always build the `BodyAndOverlaysScreen.content` (typically the stuff in the `Activity` window) eagerly * When nesting `BodyAndOverlaysScreen`, we have to take care that only the outermost `LayeredDialogSessions` instance does the capture and replay thing. Otherwise `Dialog` windows get shown in the wrong order. Fortunately we have good test coverage around this stuff these days, and now with this PR it's even better. Fixes #1003
5fd5d47 to
c96d191
Compare
Dialogs before Activity content view is attached.
If the very first rendering would instantiate `Dialog` windows, their content views were getting attached before the `Activity`'s content view. This would cause us to restore saved view state in the wrong order. If a `ComposeView` is in a `Dialog` in this situation, we would crash with an `IllegalStateException` from `SavedStateRegistry.consumeRestoredStateForKey`. The fix is for `LayeredDialogSessions` to memoize update calls received while detached, and replay them at attach time. The details are a little more complicated than that, though: * We always build the `BodyAndOverlaysScreen.content` (typically the stuff in the `Activity` window) eagerly * When nesting `BodyAndOverlaysScreen`, we have to take care that only the outermost `LayeredDialogSessions` instance does the capture and replay thing. Otherwise `Dialog` windows get shown in the wrong order. Fortunately we have good test coverage around this stuff these days, and now with this PR it's even better. Fixes #1003
c96d191 to
6a8f29a
Compare
If the very first rendering would instantiate `Dialog` windows, their content views were getting attached before the `Activity`'s content view. This would cause us to restore saved view state in the wrong order. If a `ComposeView` is in a `Dialog` in this situation, we would crash with an `IllegalStateException` from `SavedStateRegistry.consumeRestoredStateForKey`. The fix is for `LayeredDialogSessions` to memoize update calls received while detached, and replay them at attach time. The details are a little more complicated than that, though: * We always build the `BodyAndOverlaysScreen.content` (typically the stuff in the `Activity` window) eagerly * When nesting `BodyAndOverlaysScreen`, we have to take care that only the outermost `LayeredDialogSessions` instance does the capture and replay thing. Otherwise `Dialog` windows get shown in the wrong order. Fortunately we have good test coverage around this stuff these days, and now with this PR it's even better. Fixes #1003
6a8f29a to
17b2451
Compare
|
|
||
| scenario.onActivity { activity -> | ||
| val root = WorkflowLayout(activity) | ||
| activity.setContentView(root) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops, these tests were never actually attaching anything to the activity window. Now they do.
If the very first rendering would instantiate `Dialog` windows, their content views were getting attached before the `Activity`'s content view. This would cause us to restore saved view state in the wrong order. If a `ComposeView` is in a `Dialog` in this situation, we would crash with an `IllegalStateException` from `SavedStateRegistry.consumeRestoredStateForKey`. The fix is for `LayeredDialogSessions` to memoize update calls received while detached, and replay them at attach time. The details are a little more complicated than that, though: * We always build the `BodyAndOverlaysScreen.content` (typically the stuff in the `Activity` window) eagerly * When nesting `BodyAndOverlaysScreen`, we have to take care that only the outermost `LayeredDialogSessions` instance does the capture and replay thing. Otherwise `Dialog` windows get shown in the wrong order. Fortunately we have good test coverage around this stuff these days, and now with this PR it's even better. Fixes #1003
17b2451 to
b68a0ef
Compare
|
I'm thinking that @helios175 has been working on something in house that improves our ability to nest AndroidX back handlers, which would obviate this concern entirely. We should probably open source that anyway, but I don't know that doing so is required to merge this PR. (Tracking this as #1007.) Update: in its final form this PR doesn't break the existing behavior where parent views can rely on their children being created / updated synchronously, so #1007 is less urgent. It still seems like it would be a good idea to opensource it though. |
If the very first rendering would instantiate `Dialog` windows, their content views were getting attached before the `Activity`'s content view. This would cause us to restore saved view state in the wrong order. If a `ComposeView` is in a `Dialog` in this situation, we would crash with an `IllegalStateException` from `SavedStateRegistry.consumeRestoredStateForKey`. The fix is for `LayeredDialogSessions` to memoize update calls received while detached, and replay them at attach time. The details are a little more complicated than that, though: * We always build the `BodyAndOverlaysScreen.content` (typically the stuff in the `Activity` window) eagerly * When nesting `BodyAndOverlaysScreen`, we have to take care that only the outermost `LayeredDialogSessions` instance does the capture and replay thing. Otherwise `Dialog` windows get shown in the wrong order. Fortunately we have good test coverage around this stuff these days, and now with this PR it's even better. Fixes #1003
b68a0ef to
9d9ec5e
Compare
If the very first rendering would instantiate `Dialog` windows, their content views were getting attached before the `Activity`'s content view. This would cause us to restore saved view state in the wrong order. If a `ComposeView` is in a `Dialog` in this situation, we would crash with an `IllegalStateException` from `SavedStateRegistry.consumeRestoredStateForKey`. The fix is for `LayeredDialogSessions` to memoize update calls received while detached, and replay them at attach time. The details are a little more complicated than that, though: * We always build the `BodyAndOverlaysScreen.content` (typically the stuff in the `Activity` window) eagerly * When nesting `BodyAndOverlaysScreen`, we have to take care that only the outermost `LayeredDialogSessions` instance does the capture and replay thing. Otherwise `Dialog` windows get shown in the wrong order. Fortunately we have good test coverage around this stuff these days, and now with this PR it's even better. Fixes #1003
40781e4 to
dafe01b
Compare
|
|
||
| override fun dispatchTouchEvent(event: MotionEvent): Boolean { | ||
| return !dialogs.allowEvents || super.dispatchTouchEvent(event) | ||
| return !dialogs.allowContentEvents || super.dispatchTouchEvent(event) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unrelated method rename to try to make it clearer that this is strictly about event handling in the activity window, not the dialogs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why allowContentEvents and not allowBodyEvents? I think Body would help me understand what you mean.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wrestled with that. I went with content because it matches ScreenOverlay.content, which in turn comes from the Wrapper interface. But really they're both still ambiguous, aren't they -- dialog content? dialog body?
How about allowBaseEvents? That would match the updateBase param to LayeredDialogSessions.update.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See insane stream of consciousness below, allowBodyEvents and updateBody() FTW.
workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/DialogCollator.kt
Outdated
Show resolved
Hide resolved
workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/DialogSession.kt
Outdated
Show resolved
Hide resolved
...w-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/LayeredDialogSessions.kt
Outdated
Show resolved
Hide resolved
...w-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/LayeredDialogSessions.kt
Outdated
Show resolved
Hide resolved
dafe01b to
ea4d731
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice! A few naming questions as usual 😄
|
|
||
| @Test fun backButtonWorks() { | ||
| onTopCoverEverything().perform(click()) | ||
| onView(withText("Cover Body")).inRoot(isDialog()).perform(click()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: could use constants for these view texts.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know why, but I always find that makes tests harder for me to read.
| val outerSheet = if (!renderState.showOuterSheet) { | ||
| null | ||
| } else { | ||
| val close = context.eventHandler { state = state.copy(showOuterSheet = false) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
| val close = context.eventHandler { state = state.copy(showOuterSheet = false) } | |
| val closeOuter = context.eventHandler { state = state.copy(showOuterSheet = false) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually I guess that won't compile below heh. Well, take it or leave it.
...s/nested-overlays/src/main/java/com/squareup/sample/nestedoverlays/NestedOverlaysWorkflow.kt
Show resolved
Hide resolved
|
|
||
| override fun dispatchTouchEvent(event: MotionEvent): Boolean { | ||
| return !dialogs.allowEvents || super.dispatchTouchEvent(event) | ||
| return !dialogs.allowContentEvents || super.dispatchTouchEvent(event) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why allowContentEvents and not allowBodyEvents? I think Body would help me understand what you mean.
| initialEnvironment: ViewEnvironment | ||
| ) { | ||
| /** | ||
| * One time call to set up our brand new [OverlayDialogHolder] instance. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to propagate this naming change down to the holder? A little bit incongruous here to say this is not showing, we are 'setting up' the holder, and then the first call is to holder.show.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That gets into another level of API inconsistency. holder.show is public, OverlayDialogHolder.show, and it parallels ScreenViewHolder.show. This is definitely an awkward seam, but it's all in non-public code.
Basically, Dialog.show() conflicts with our show(someRendering, environment) convention. But it's only us on the Workflow team who take the cognitive hit, b/c feature code never gets anywhere near Dialog.show() or Dialog.dismiss().
| private var deferredShow: List<DialogSession>? = null | ||
|
|
||
| /** | ||
| * Clients should ignore UI events targeting their [content][ScreenOverlay.content] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so this is where the content naming comes from? Hmm. How do I map body to content? are they equivalent? 'Body' is the container for 'content'?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was originally
class BodyAndOverlaysScreen<B: Screen, O: Overlay>(
val body: B,
val overlays: List<O> = emptyList()
)but I recently added the common Wrapper type, and standardized content (makes sense everywhere else :/):
class BodyAndOverlaysScreen<C: Screen, O: Overlay>(
override val content: C,
val overlays: List<O> = emptyList()
): Wrapper<Screen, C>While we still have time to fuss with these names, would this be more or less confusing?
class BodyAndOverlaysScreen<B: Screen, O: Overlay>(
val body: B,
val overlays: List<O> = emptyList()
): Wrapper<Screen, B> {
override val content: B = body
}I guess I like it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, derp: and that kdoc is TOTALLY WRONG! Link should be to BodyAndOverlays.content!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tell you what, let's decouple this naming conversation from the ordering fix. Maybe even move it upstream.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or for…IT'S ALREADY BodyAndOverlaysScreen.body.
I'll change things here accordingly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/**
* While this is true, clients should ignore UI events targeting the
* [body][BodyAndOverlaysScreen.body] view beneath the
* [Dialog][android.app.Dialog] windows they manage. It reflects
* that a [ModalOverlay] is (or soon will be) showing over the body.
*/
public var allowBodyEvents: Boolean = trueThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm going to rename updateBase to updateBody as well.
If the very first rendering would instantiate `Dialog` windows, their content views were getting attached before the `Activity`'s content view. This would cause us to restore saved view state in the wrong order. If a `ComposeView` is in a `Dialog` in this situation, we would crash with an `IllegalStateException` from `SavedStateRegistry.consumeRestoredStateForKey`. The fix is for `LayeredDialogSessions` and `DialogCollator` to conspire to decouple created `Dialog` instances from showing them the first time. Normally we go ahead and do both at once. But if `LayeredDialogSessions.update` is called before the corresponding `BodyAndOverlaysContainer` view is attached to the `Activity` window, we defer showing until it is. Fixes #1001
ea4d731 to
bf2b9cf
Compare
|
@steve-the-edwards PTAL, adopted most of your naming fixes. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still a novice at Workflow internals but I'm not seeing any obvious issues, changes make sense to me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Love it. LGTM
If the very first rendering would instantiate
Dialogwindows, their content views were getting attached before theActivity's content view. This would cause us to restore saved view state in the wrong order. If aComposeViewis in aDialogin this situation, we would crash with anIllegalStateExceptionfromSavedStateRegistry.consumeRestoredStateForKey.The fix is for
LayeredDialogSessionsandDialogCollatorto conspire to decouple createdDialoginstances from showing them the first time. Normally we go ahead and do both at once. But ifLayeredDialogSessions.updateis called before the correspondingBodyAndOverlaysContainerview is attached to theActivitywindow, we defer showing until it is.Fixes #1001