-
Notifications
You must be signed in to change notification settings - Fork 101
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
Compose in the Runtime Take 3 #824
Conversation
6e53c87
to
5fe9107
Compare
5fe9107
to
ce5557a
Compare
workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowNode.kt
Outdated
Show resolved
Hide resolved
setRendering = { | ||
renderingState.value = it as R | ||
} |
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.
We usually discourage this, but since this is framework code that is not public API and will be run a lot, it might be more efficient to just pass in the MutableState
itself so Compose doesn't have to memoize this lambda for you.
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.
Done.
props: PropsT, | ||
setRendering: (RenderingT) -> Unit | ||
): Unit { | ||
key(props) { |
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.
If you're comparing props anyway, I don't see the reason for putting this in a key function (and if it is necessary, it should probably be an implementation detail of UpdatePropsAndState).
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.
removed.
setRendering: (RenderingT) -> Unit | ||
): Unit { | ||
key(props) { | ||
UpdatePropsAndState(workflow, props) |
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: Compose conventions discourage naming unit-returning composable functions as verbs.
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.
Worthwhile to break convention here I think.
) { | ||
if (newProps != lastProps) { | ||
val newState = interceptor.intercept(workflow, this) | ||
.onPropsChanged(lastProps, newProps, state.value) |
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 hope everyone heeded our advice to not perform side effects in this function 😅
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 think we can protect against that anyway like this:
val newState = produceState(initialValue = state.value, newProps) {
if (newProps != lastProps) {
interceptor.intercept(workflow, this@WorkflowNode)
.onPropsChanged(lastProps, newProps, state.value)
}
}
state.value = newState.value
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.
NVM. that does not work because there is nothing actually reproducing the state!
This works though:
key (newProps) {
if (newProps != lastProps) {
state.value = interceptor.intercept(workflow, this@WorkflowNode)
.onPropsChanged(lastProps, newProps, state.value)
}
}
@@ -52,7 +56,7 @@ internal class WorkflowRunner<PropsT, OutputT, RenderingT>( | |||
private val rootNode = WorkflowNode( | |||
id = workflow.id(), | |||
workflow = workflow, | |||
initialProps = currentProps, | |||
initialProps = currentProps.value, |
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: I don't see you using the delegate syntax for MutableState
s very much, it would make a lot of the code in this PR more idiomatic and a teeny bit cleaner.
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.
The delegate syntax continues to confuse me and I don't necessarily like the syntactic sugar obfuscating what is happening.
In this case currentProps
is a class property. How would I use the delegate syntax here as I thought that's only when using remember
? And remember
is only for within a @Composable
?
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 has nothing to do with delegation. Property delegation is a well-documented Kotlin language feature. It's supported for MutableState
because it has getValue
and setValue
extension methods. More info on how it works here.
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 I was getting confused by all the examples including the remember
in the delegate. I didn't realize that even outside of that it was actually MutableState that had the getValue/setValue so it could be used with delegation.
I see now that you can just use:
val currentProps: PropsT by mutableStateOf(props.value)
that makes a lot more sense.
handler: (ChildOutputT) -> WorkflowAction<PropsT, StateT, OutputT> | ||
): ChildRenderingT { | ||
val stagedChild = remember (child, props, key, handler) { | ||
prepareStagedChild( |
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 believe this is safe to call from composition since it modifies non-snapshot state in this class.
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.
given that SideEffect
only occurs after Composition though and we need this stagedChild
to call the Rendering waht are the options here?
I thought remember
would effectively avoid calling this multiple times across recomposition?
Do you mean that the StagingList needs to be State
?
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 will prevent calling it again on subsequent compositions, but it will still be called if the initial composition is discarded and never applied. It is probably enough to make the staged child list a SnapshotStateList
.
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 I am understanding you correctly, we would make a @Composable StagedChild()
that would have a SideEffect
that would create the child node and add it to the staging list.
That way that would only be called if StagedChild
was recomposed successfully, but we could still rely on its side effect by the time the Rendering
is composed.
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 think i'm just going to have to re-write or modify ActiveStagingList
in a Snapshot-State-friendly way
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.
/**
* Prepare the staged child while only modifying [children] in a SideEffect. This will ensure
* that we do not inappropriately modify non-snapshot state.
*/
@Composable
private fun <ChildPropsT, ChildOutputT, ChildRenderingT> StagedChild(
child: Workflow<ChildPropsT, ChildOutputT, ChildRenderingT>,
props: ChildPropsT,
key: String,
handler: (ChildOutputT) -> WorkflowAction<PropsT, StateT, OutputT>
): WorkflowChildNode<*, *, *, *, *> {
val childState = remember (child, key, props, handler) {
children.forEachStaging {
require(!(it.matches(child, key))) {
"Expected keys to be unique for ${child.identifier}: key=\"$key\""
}
}
mutableStateOf(children.firstActiveMatchingOrNull {
it.matches(child, key)
} ?: createChildNode(child, props, key, handler))
}
SideEffect {
children.swapOrStage(
predicate = {it.matches(child, key)},
child = childState.value)
}
return childState.value
}
410fbd1
to
320bdc5
Compare
Still todo on the Snapshotting and figure out how we want to 'freeze' to guard against out of lifecycle calls.
320bdc5
to
d48eaf6
Compare
f132ca8
to
dcec792
Compare
OK so I have updated this with a recent commit that isolates all of teh Compose optimizations as a plugin module |
|
Superseded by #886 |
To address #665