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
Replaces *.initializeView with *.viewStarter #602
Conversation
b6360db
to
cb38765
Compare
a6cfb05
to
42d0231
Compare
* created (that is, immediately after the call to [ViewFactory.buildView]). | ||
* [showRendering], [getRendering] and [environment] are all available when this is called. | ||
* Defaults to a call to [View.showFirstRendering]. | ||
* @param viewStarter DOCUMENT ME RAY |
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.
TODO?
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.
Oopsy, that was supposed to shame me into writing the doc before asking for review.
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.
*
* @param viewStarter An optional wrapper for the function invoked when [View.start]
* is called, allowing for last second initialization of a newly built [View].
* See [ViewStarter] for details.
/**
* A wrapper for the function invoked when [View.start] is called, allowing for
* last second initialization of a newly built [View]. Provided via [ViewRegistry.buildView]
* or [DecorativeViewFactory.viewStarter].
*
* While [View.getRendering] may be called from [startView], it is not safe to
* assume that the type of the rendering retrieved matches the type the view was
* originally built to display. [ViewFactories][ViewFactory] can be wrapped, and
* renderings can be mapped to other types.
*/
@WorkflowUiExperimentalApi
public fun interface ViewStarter {
/** Called from [View.start]. [doStart] must be invoked. */
public fun startView(
view: View,
doStart: () -> Unit
)
}
public typealias ViewShowRendering<RenderingT> = | ||
(@UnsafeVariance RenderingT, ViewEnvironment) -> Unit |
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.
What is the variance that is not matching here?
by nature of only being an input parameter on the function is it wanting you to make RenderingT
explicitly contravariant to ViewShowRendering
?
Haven't tested, but my guess is
public typealias ViewShowRendering<RenderingT> = | |
(@UnsafeVariance RenderingT, ViewEnvironment) -> Unit | |
public typealias ViewShowRendering<in RenderingT> = | |
(RenderingT, ViewEnvironment) -> Unit |
Should work without the Unsafe annotation?
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's been a while, but I think it's because that change is theoretically required but you're not allowed to do it with a typealias
.
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.
ah gotcha. I kind of remember you saying something like that!
get() = getTag(R.id.workflow_ui_view_state) as? WorkflowUiViewState<*> | ||
|
||
@WorkflowUiExperimentalApi | ||
private var View.workflowTag: WorkflowUiViewState<*> |
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.
private var View.workflowTag: WorkflowUiViewState<*> | |
private var View.workflowUiViewState: WorkflowUiViewState<*> |
Tag (while it is the implementation) kept tripping me up. Not convinced this must be done but just a thought.
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.
Seems helpful to me, will do.
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.
And maybe rename WorkflowUiViewState
to just WorkflowViewState
while we're at it.
* Establishes [showRendering] as the implementation of [View.showRendering] | ||
* for the receiver, possibly replacing the existing one. Likewise sets / updates | ||
* the values returned by [View.getRendering] and [View.environment]. |
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.
* Establishes [showRendering] as the implementation of [View.showRendering] | |
* for the receiver, possibly replacing the existing one. Likewise sets / updates | |
* the values returned by [View.getRendering] and [View.environment]. | |
* Establishes [showRendering] as the implementation of [View.showRendering] | |
* for the receiver, possibly replacing the existing one. Likewise sets / updates | |
* the values returned by [View.getRendering] and [View.environment] by setting | |
* the [WorkflowUiViewState] tag on the View receiver. |
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.
Remember that WorkflowUiViewState
is a private type.
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.
gotcha.
) { | ||
workflowTag = when (workflowTagOrNull) { | ||
is New<*> -> New(initialRendering, initialViewEnvironment, showRendering, starter) | ||
else -> New(initialRendering, initialViewEnvironment, showRendering) |
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 if the View WorkflowUiViewState
was Started
we just recycle it back to New
?
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 guess that's what you mean by 'possibly replacing the existing one', but it was not clear to me whether you can replace it only before it is Started or you can replace it after as well?
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.
This is mostly about, if there is already a New
tag we have to propagate the starter
.
That actually happens a lot. When a view factory gets wrapped, this method gets called again to rebind the view to the wrapper rendering type, so repeated calls are the norm.
FooScreen : Screen
- We render
NamedScreen(FooScreen())
- The view is built by
FooScreenFactory
, which callsbindShowRendering<FooScreen>()
NamedScreenFactory
invokesFooScreenFactory.buildView
, and callsbindShowRendering<NamedScreen<*>>()
on the view thatFooScreenFactory
built.
I actually had a case for is Started<*>
that would throw when this was called, but I got afraid of making it difficult to reuse views. I figured just letting it fall through to the else would be more like the current behavior.
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'll add doc to this effect.
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.
Ack. As long as its safe and understood that we could re-use them. Wasn't sure if we needed cleanup hooks before being re-used but I guess this is functionally cleaning up the WorkflowUiViewState
which is one good reason to keep it all in that type/tag.
} | ||
|
||
/** | ||
* It is usually more convenient to use [WorkflowViewStub] than to call this method directly. |
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 is usually more convenient to use [WorkflowViewStub] than to call this method directly. | |
* This is not necessary when using [WorkflowViewStub] as it will handle this check for you. |
private val View.workflowTagAsStarted: Started<*> | ||
get() = workflowTag as? Started<*> ?: error( | ||
"Expected $this to have been started, but View.start() has not been called" | ||
) |
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.
Goodness me this is the first time I have deeply pined for Swift's protocol extensions.
I get why we need all of this as extensions on the View but it is very difficult to follow the logic with all these (albeit handy) extension shortcuts.
I guess it's not possible (given the coupling) to extend View
to a WorkflowManagedView
to have all these extensions in a more understandable form?
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.
Nope, this approach is entirely about avoiding the need for custom View classes.
We could create a facade interface that just wrapped a plain old View. I don't think it's worth the effort. Feature developers basically never bump into this stuff, thanks to WorkflowViewStub
and such. That will be even more true when the UI overhaul is done, I think.
But just to scratch the nerdsnipe itch, here's a sketch of what that could be.
fun <RenderingT> ViewRegistry.buildView(...): WorkflowView<RenderingT>
interface WorkflowView<RenderingT> {
fun asView(): View
fun start()
val rendering: RenderingT
val environment: ViewEnvironment
fun update(
rendering: RenderingT,
environment: ViewEnvironment
)
// ...
}
fun <RenderingT> View.asWorkflowView(): WorkflowView<T>? {
// Looks for the tag and confirms the type is right?
}
Still thinking this would be a distraction, but I'm wavering. Dammit Steve, are you proud that you've done this 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.
And WorkflowViewStub
could implement WorkflowView<*>
! All the stock containers could!
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.
Sigh, tracked: #606
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.
Goodness me this is the first time I have deeply pined for Swift's protocol extensions.
Man, I've been wishing for those since I left Objective-C for Java.
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.
Haha. Amazing. Very proud. I get your point though that the audience is small. This does make it a lot easier to grok though IMHO.
* Sets the rendering associated with this view, and displays it by invoking | ||
* the [ViewShowRendering] function previously set by [bindShowRendering]. |
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.
* Sets the rendering associated with this view, and displays it by invoking | |
* the [ViewShowRendering] function previously set by [bindShowRendering]. | |
* Shows [rendering] in this View by invoking | |
* the [ViewShowRendering] function previously set by [bindShowRendering]. |
I get its setting a tag but that is the implementation detail I think and not necessary to the outcome of the function.
) | ||
|
||
@WorkflowUiExperimentalApi | ||
private fun Any.matches(other: Any) = compatible(this, other) |
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 get that maybe this makes it more fluent in showRendering
above but adding another layer of indirection is a lot to carry in your mind when grokking.
Not a big deal but maybe can we just use compatible(tag.showing, rendering)
above? Is there another use for this?
42d0231
to
0320832
Compare
Update addresses @steve-the-edwards review.
|
* | ||
* Intended for use by implementations of [ViewFactory.buildView]. | ||
* @throws IllegalStateException when called after [View.start] |
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.
Not true!
Fixes #597. It used to be the case that every `DecorativeViewFactory` called `showRendering()` twice (#397). We fixed that (we thought) by introducing the `initializeView` lambda to `ViewRegistry.buildView` and `DecorativeViewFactory` (#408). Unfortunately, that fix botched recursion. Individual `DecorativeViewFactory` instances work fine, but if you wrap them you still get one `showRendering` call from each. Worse, upstream `initializeView` lambdas are clobbered by immediately downstream ones. e.g., when a `WorkflowViewStub` shows a `DecorativeViewFactory`, the `WorkflowLifecycleRunner.installOn` call in the former is clobbered. The fix is to completely decouple building a view from from this kind of initialization. `ViewRegistry.buildView` and its wrappers no longer try to call `showRendering` at all. Instead the caller of `buildView` (mostly `WorkflowViewStub`) is reponsible for immediately calling `View.start` on the new `View`. `View.start` makes the initial `showRendering` call that formerly was the job of `ViewFactory.buildView` -- the factory builds the view, and the container turns the key. Since `View.start` is called only after all wrapped `ViewFactory.buildView` functions have executed, we're certain it will only happen once. Of course we still need the ability to customize view initialization via wrapping, especially to invoke `WorkflowLifecycleOwner.installOn`. To accomodate that, the function that `View.start` executes can be wrapped via the new `viewStarter` argument to `ViewRegistry.buildView` and `DecorativeViewFactory`, which replaces `initializeView`. This required a pretty thorough overhaul of `ViewShowRendering.kt` The `ViewShowRenderingTag` that it hangs off of a view tag is renamed `WorkflowViewState`, and extracted to a separate file. `WorkflowViewState` is a sealed class with two implementations (`New` and `Started`) to help us enforce the order of the `ViewRegistry.buildView`, `View.bindShowRendering`, `View.start` and `View.showRendering` calls.
0320832
to
b93a522
Compare
21 passed, 24 had a stupid socket error. I'm merging this. |
Our homegrown `View.takeWhileAttached` somehow causes a strange issue on API 30 devices where `View.onAttached` can be called twice, at least since the recent change to introduce `View.start` landed in #602. We've seen crashes on a variety of Samsung devices out in the wild, and can reproduce the issue on API 30 AVDs via Android Studio's _Apply Changes and Restart Activity_. The fix is to be mainstream and use `Lifecycle`, which add as a new required parameter to `WorkflowLayout.start`. The old overloads are now deprecated, and will be deleted soon.
Our homegrown `View.takeWhileAttached` somehow causes a strange issue on API 30 devices where `View.onAttached` can be called twice, at least since the recent change to introduce `View.start` landed in #602. We've seen crashes on a variety of Samsung devices out in the wild, and can reproduce the issue on API 30 AVDs via Android Studio's _Apply Changes and Restart Activity_. The fix is to be mainstream and use `Lifecycle`, which is added as a new required parameter to `WorkflowLayout.start`. The old overloads are now deprecated, and will be deleted soon.
Our sketchy homegrown `View.takeWhileAttached` somehow causes a strange issue on API 30 devices where `View.onAttached` can be called twice, at least since the recent change to introduce `View.start` landed in #602. We've seen crashes on a variety of Samsung devices out in the wild, and can reproduce the issue on API 30 AVDs via Android Studio's _Apply Changes and Restart Activity_. The fix is to be mainstream and use `Lifecycle`, which is added as a new required parameter to `WorkflowLayout.start`. The old overloads are now deprecated, and will be deleted soon. We also promote `WorkflowLayout.show` to be publicly visible, to give apps the option of taking control over how they collect their renderings.
Our sketchy homegrown `View.takeWhileAttached` somehow causes a strange issue on API 30 devices where `View.onAttached` can be called twice, at least since the recent change to introduce `View.start` landed in #602. We've seen crashes on a variety of Samsung devices out in the wild, and can reproduce the issue on API 30 AVDs via Android Studio's _Apply Changes and Restart Activity_. The fix is to be mainstream and use `Lifecycle`, which is added as a new required parameter to `WorkflowLayout.start`. The old overloads are now deprecated, and will be deleted soon. We also promote `WorkflowLayout.show` to be publicly visible, to give apps the option of taking control over how they collect their renderings.
Our sketchy homegrown `View.takeWhileAttached` somehow causes a strange issue on API 30 devices where `View.onAttached` can be called twice, at least since the recent change to introduce `View.start` landed in #602. We've seen crashes on a variety of Samsung devices out in the wild, and can reproduce the issue on API 30 AVDs via Android Studio's _Apply Changes and Restart Activity_. The fix is to be mainstream and use `Lifecycle`, which is added as a new required parameter to `WorkflowLayout.start`. The old overloads are now deprecated, and will be deleted soon. We also promote `WorkflowLayout.show` to be publicly visible, to give apps the option of taking control over how they collect their renderings.
Our sketchy homegrown `View.takeWhileAttached` somehow causes a strange issue on API 30 devices where `View.onAttached` can be called twice, at least since the recent change to introduce `View.start` landed in #602. We've seen crashes on a variety of Samsung devices out in the wild, and can reproduce the issue on API 30 AVDs via Android Studio's _Apply Changes and Restart Activity_. The fix is to be mainstream and use `Lifecycle`, which is added as a new required parameter to `WorkflowLayout.start`. The old overloads are now deprecated, and will be deleted soon. We also promote `WorkflowLayout.show` to be publicly visible, to give apps the option of taking control over how they collect their renderings.
Our sketchy homegrown `View.takeWhileAttached` somehow causes a strange issue on API 30 devices where `View.onAttached` can be called twice, at least since the recent change to introduce `View.start` landed in #602. We've seen crashes on a variety of Samsung devices out in the wild, and can reproduce the issue on API 30 AVDs via Android Studio's _Apply Changes and Restart Activity_. The fix is to be mainstream and use `Lifecycle`, which is added as a new required parameter to `WorkflowLayout.start`. The old overloads are now deprecated, and will be deleted soon. We also promote `WorkflowLayout.show` to be publicly visible, to give apps the option of taking control over how they collect their renderings.
Our sketchy homegrown `View.takeWhileAttached` somehow causes a strange issue on API 30 devices where `View.onAttached` can be called twice, at least since the recent change to introduce `View.start` landed in #602. We've seen crashes on a variety of Samsung devices out in the wild, and can reproduce the issue on API 30 AVDs via Android Studio's _Apply Changes and Restart Activity_. The fix is to be mainstream and use `Lifecycle`, which is added as a new required parameter to `WorkflowLayout.start`. The old overloads are now deprecated, and will be deleted soon. We also promote `WorkflowLayout.show` to be publicly visible, to give apps the option of taking control over how they collect their renderings.
Fixes #597.
It used to be the case that every
DecorativeViewFactory
calledshowRendering()
twice (#397). We fixed that (we thought) by introducing theinitializeView
lambda toViewRegistry.buildView
andDecorativeViewFactory
(#408).Unfortunately, that fix botched recursion. Individual
DecorativeViewFactory
instances work fine, but if you wrap them you still get one
showRendering
call from each. Worse, upstream
initializeView
lambdas are clobbered byimmediately downstream ones. e.g., when a
WorkflowViewStub
shows aDecorativeViewFactory
, theWorkflowLifecycleRunner.installOn
call in theformer is clobbered.
The fix is to completely decouple building a view from from this kind of
initialization.
ViewRegistry.buildView
and its wrappers no longer try tocall
showRendering
at all.Instead the caller of
buildView
(mostlyWorkflowViewStub
) is reponsiblefor immediately calling
View.start
on the newView
.View.start
makesthe initial
showRendering
call that formerly was the job ofViewFactory.buildView
-- the factory builds the view, and the containerturns the key. Since
View.start
is called only after all wrappedViewFactory.buildView
functions have executed, we're certain it will onlyhappen once.
Of course we still need the ability to customize view initialization via
wrapping, especially to invoke
WorkflowLifecycleOwner.installOn
. Toaccomodate that, the function that
View.start
executes can be wrapped viathe new
viewStarter
argument toViewRegistry.buildView
andDecorativeViewFactory
, which replacesinitializeView
.This required a pretty thorough overhaul of
ViewShowRendering.kt
The
ViewShowRenderingTag
that it hangs off of a view tag is renamedWorkflowViewState
, and extracted to a separate file.WorkflowViewState
is a sealed class with two implementations (New
andStarted
) to help us enforce the order of theViewRegistry.buildView
,View.bindShowRendering
,View.start
andView.showRendering
calls.