Skip to content

Conversation

@rjrjr
Copy link
Collaborator

@rjrjr rjrjr commented May 31, 2023

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

In anticipation of fix for #1001
@rjrjr rjrjr force-pushed the ray/restore-dialogs-later-like-a-sane-person branch from cbc58b5 to 5fd5d47 Compare June 1, 2023 18:52
rjrjr added a commit that referenced this pull request Jun 1, 2023
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
@rjrjr rjrjr force-pushed the ray/restore-dialogs-later-like-a-sane-person branch from 5fd5d47 to c96d191 Compare June 1, 2023 22:22
@rjrjr rjrjr changed the title wip: do not build dialogs before container is attached Do not show Dialogs before Activity content view is attached. Jun 1, 2023
rjrjr added a commit that referenced this pull request Jun 1, 2023
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
@rjrjr rjrjr force-pushed the ray/restore-dialogs-later-like-a-sane-person branch from c96d191 to 6a8f29a Compare June 1, 2023 22:30
rjrjr added a commit that referenced this pull request Jun 1, 2023
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
@rjrjr rjrjr force-pushed the ray/restore-dialogs-later-like-a-sane-person branch from 6a8f29a to 17b2451 Compare June 1, 2023 22:32

scenario.onActivity { activity ->
val root = WorkflowLayout(activity)
activity.setContentView(root)
Copy link
Collaborator Author

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.

rjrjr added a commit that referenced this pull request Jun 1, 2023
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
@rjrjr rjrjr force-pushed the ray/restore-dialogs-later-like-a-sane-person branch from 17b2451 to b68a0ef Compare June 1, 2023 23:11
@rjrjr rjrjr marked this pull request as ready for review June 1, 2023 23:11
@rjrjr rjrjr requested review from a team and zach-klippenstein as code owners June 1, 2023 23:11
@rjrjr rjrjr marked this pull request as draft June 2, 2023 00:15
@rjrjr
Copy link
Collaborator Author

rjrjr commented Jun 2, 2023

I'm thinking that View.backPressedHandler and BackButtonScreen will still be solid enough even with this change, but I'll punch up some of the related samples to be more confident about that before merging.

@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.

rjrjr added a commit that referenced this pull request Jun 2, 2023
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
@rjrjr rjrjr force-pushed the ray/restore-dialogs-later-like-a-sane-person branch from b68a0ef to 9d9ec5e Compare June 2, 2023 20:51
rjrjr added a commit that referenced this pull request Jun 2, 2023
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
@rjrjr rjrjr force-pushed the ray/restore-dialogs-later-like-a-sane-person branch 2 times, most recently from 40781e4 to dafe01b Compare June 5, 2023 21:12
@rjrjr rjrjr marked this pull request as ready for review June 5, 2023 22:13

override fun dispatchTouchEvent(event: MotionEvent): Boolean {
return !dialogs.allowEvents || super.dispatchTouchEvent(event)
return !dialogs.allowContentEvents || super.dispatchTouchEvent(event)
Copy link
Collaborator Author

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.

Copy link
Contributor

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.

Copy link
Collaborator Author

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.

Copy link
Collaborator Author

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.

@rjrjr rjrjr force-pushed the ray/restore-dialogs-later-like-a-sane-person branch from dafe01b to ea4d731 Compare June 5, 2023 22:58
Copy link
Contributor

@steve-the-edwards steve-the-edwards left a 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())
Copy link
Contributor

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.

Copy link
Collaborator Author

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) }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
val close = context.eventHandler { state = state.copy(showOuterSheet = false) }
val closeOuter = context.eventHandler { state = state.copy(showOuterSheet = false) }

Copy link
Contributor

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.


override fun dispatchTouchEvent(event: MotionEvent): Boolean {
return !dialogs.allowEvents || super.dispatchTouchEvent(event)
return !dialogs.allowContentEvents || super.dispatchTouchEvent(event)
Copy link
Contributor

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.
Copy link
Contributor

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.

Copy link
Collaborator Author

@rjrjr rjrjr Jun 6, 2023

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]
Copy link
Contributor

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'?

Copy link
Collaborator Author

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?

Copy link
Collaborator Author

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!

Copy link
Collaborator Author

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.

Copy link
Collaborator Author

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.

Copy link
Collaborator Author

@rjrjr rjrjr Jun 6, 2023

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 = true

Copy link
Collaborator Author

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
@rjrjr rjrjr force-pushed the ray/restore-dialogs-later-like-a-sane-person branch from ea4d731 to bf2b9cf Compare June 6, 2023 20:39
@rjrjr rjrjr requested a review from steve-the-edwards June 6, 2023 20:39
@rjrjr
Copy link
Collaborator Author

rjrjr commented Jun 6, 2023

@steve-the-edwards PTAL, adopted most of your naming fixes.

Copy link
Collaborator

@ijwhelan ijwhelan left a 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.

Copy link
Contributor

@steve-the-edwards steve-the-edwards left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love it. LGTM

@rjrjr rjrjr enabled auto-merge June 6, 2023 22:08
@rjrjr rjrjr merged commit 81fe606 into main Jun 6, 2023
@rjrjr rjrjr deleted the ray/restore-dialogs-later-like-a-sane-person branch June 6, 2023 22:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ComposeView and IllegalStateException: You can consumeRestoredStateForKey only after super.onCreate of corresponding component

4 participants