-
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
985: render() again only if state has been changed by the action (or its parent's output handling) #992
Conversation
464ebff
to
05285fb
Compare
e65278a
to
25e65d7
Compare
workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowNode.kt
Outdated
Show resolved
Hide resolved
workflow-core/src/commonMain/kotlin/com/squareup/workflow1/WorkflowAction.kt
Outdated
Show resolved
Hide resolved
26e95af
to
1d2b00c
Compare
cafac7e
to
84ac726
Compare
...dungeon/timemachine/src/test/java/com/squareup/sample/timemachine/TimeMachineWorkflowTest.kt
Outdated
Show resolved
Hide resolved
...nfig/config-android/src/main/java/com/squareup/workflow1/config/AndroidRuntimeConfigTools.kt
Outdated
Show resolved
Hide resolved
public fun forceRerender() { | ||
forcedRerender = true |
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.
Surprised this exists, and surprised that it's public. Hard to imagine feature devs putting this to anything but bad uses.
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 originally did not have this. I've now added this as in internal testing we have a number of scenarios where we drive 'other' (non-Workflow) UI via side effects that occur in actions, but then that require a re-render() to pick up the changes in that other UI. So either we add some kind of increasing counter to the state for those cases or we add a hook to force re-render() without state change like this. I wanted to just test with this first. Sorry I'm realizing now that this PR shoudl still be in draft. I'm not sure why it wasn't. Probably I was thinking at some point it was more ready than it is.
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 this until we decide this is the strategy we want to handle non-Workflow integrations.
workflow-core/src/commonMain/kotlin/com/squareup/workflow1/WorkflowAction.kt
Show resolved
Hide resolved
84ac726
to
2db3e12
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.
Nothing but nits. So excited!
@@ -188,6 +188,62 @@ jobs : | |||
with : | |||
report_paths : '**/build/test-results/test/TEST-*.xml' | |||
|
|||
jvm-stateChange-runtime-test : |
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.
Woot! @RBusarow should own reviewing this for correctness, though.
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 did do some cleanup after this - #1011
"conflate" -> ConflateStaleRenderings(renderOnStateChangeOnly = false) | ||
"conflate-stateChange" -> ConflateStaleRenderings(renderOnStateChangeOnly = true) | ||
"baseline-stateChange" -> RenderPerAction(renderOnStateChangeOnly = true) | ||
else -> RuntimeConfig.DEFAULT_CONFIG |
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 like we should be a bit more strict so that we catch typos for people.
"conflate" -> ConflateStaleRenderings(renderOnStateChangeOnly = false) | |
"conflate-stateChange" -> ConflateStaleRenderings(renderOnStateChangeOnly = true) | |
"baseline-stateChange" -> RenderPerAction(renderOnStateChangeOnly = true) | |
else -> RuntimeConfig.DEFAULT_CONFIG | |
"conflate" -> ConflateStaleRenderings(renderOnStateChangeOnly = false) | |
"conflate-stateChange" -> ConflateStaleRenderings(renderOnStateChangeOnly = true) | |
"baseline-stateChange" -> RenderPerAction(renderOnStateChangeOnly = true) | |
"", "baseline" -> RuntimeConfig.DEFAULT_CONFIG | |
else -> throw IllegalArgumentException("Unrecognized config \"${BuildConfig.WORKFLOW_RUNTIME}\"") |
If you take the enum set suggestion below, we could retire --runtime-config
and replace it with three separate boolean flags.
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 like the Set idea!
I'm going to leave it as strings for the options though as I don't want to mess with the buildConfig stuff I set up before (the command line options are easy - but the buildconfig for the instrumentation tests is harder IIRC)
* which could be null. | ||
* @param stateChanged: whether or not the action changed the state. | ||
*/ | ||
public data class ActionApplied<out OutputT>( |
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 actually rely on this being a data class
? I remember that being a landmine when used in public API, though I forget exactly why.
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 you can implement positional params manually, it's pretty trivial.
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.
Like to support destructuring you mean?
Hmm, now I'm curious - what the was the data class issue in the public API?
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 do use a .copy (which I know I could implement). However, since I've done all the testing and verification with it as a data class I'm loathe to change that.
I have a vague recollection that we had issues before with a data class based on how it does equality? We should be able to rely on that here though.
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.
For posterity. I am going to do this change after all. Canonical resource describing backwards compatibility concerns: https://jakewharton.com/public-api-challenges-in-kotlin/
/** | ||
* We only check for this when [RuntimeConfig.renderOnStateChangeOnly] is true. | ||
*/ | ||
suspend fun maybeCheckNoStateChange(actionResult: ActionProcessingResult): Boolean { |
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.
How about shortCircuitForUnchangedState
? And have the kdoc explain what should happen when true is returned.
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. Done.
/**
* If [runtimeConfig] contains [RuntimeConfigOptions.RENDER_ONLY_WHEN_STATE_CHANGES] then
* send any output, but return true which means restart the runtime loop and process another
* action.
*/
suspend fun shortCircuitForUnchangedState(actionResult: ActionProcessingResult): Boolean {
if (runtimeConfig.contains(RENDER_ONLY_WHEN_STATE_CHANGES) &&
actionResult is ActionApplied<*> && !actionResult.stateChanged
) {
// Possibly send output and process more actions. No state change so no re-render.
sendOutput(actionResult, onOutput)
return true
}
return 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.
"and process another action" seems to imply that we will render, just that we're going to try to process more actions first. Should this say "and process the next action (if any)"?
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.
Just missed me merging this :). But I will follow up with this change.
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.
Technically 'processing an action' can involve suspending until there is one but that's more than a little bit confusing.
* If state has not changed from an action cascade, do not re-render. This has been mostly | ||
* proven out. However, be careful if you have any non-Workflow code you integrate with that | ||
* depends on the Workflow tree re-rendering to pick up changes from its equivalent 'view model.' | ||
* You should change some kind of Workflow state when updating that external code if you want | ||
* Workflow to pick up the change and render again. |
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 state has not changed from an action cascade, do not re-render. This has been mostly | |
* proven out. However, be careful if you have any non-Workflow code you integrate with that | |
* depends on the Workflow tree re-rendering to pick up changes from its equivalent 'view model.' | |
* You should change some kind of Workflow state when updating that external code if you want | |
* Workflow to pick up the change and render again. | |
* If state has not changed from an action cascade (as determined via `equals()`), | |
* do not re-render. For example, when this is true and `noAction()` is enqueued, | |
* the current `render()` pass will short circuit and no rendering will be posted | |
* through the `StateFlow` returned from `renderWorkflowIn()`. | |
* | |
* This has been mostly proven out. However, be careful if you have any non-Workflow | |
* code you integrate with that depends on the Workflow tree re-rendering to pick up | |
* changes from its equivalent 'view model.' You should change some kind of Workflow | |
* state when updating that external code if you want Workflow to pick up the change | |
* and render again. |
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.
Used in the updated location in the enum.
action: WorkflowAction<PropsT, StateT, OutputT>, | ||
childResult: ActionApplied<*>? = null | ||
): ActionProcessingResult { | ||
val (newState, actionApplied) = action.applyTo(lastProps, 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.
val (newState, actionApplied) = action.applyTo(lastProps, state) | |
val (newState: StateT, actionApplied: ActionApplied<StateT>) = action.applyTo(lastProps, state) |
Not positive about the ActionApplied
type, but you get the idea.
Pair(RenderPerAction, ConflateStaleRenderings), | ||
Pair(ConflateStaleRenderings, RenderPerAction), | ||
Pair(ConflateStaleRenderings, ConflateStaleRenderings), | ||
Pair(RenderPerAction(), RenderPerAction()), |
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.
Holy combinatorial explosion! Would this get any simpler of the config was an enum set and RuntimeConfig
was a three value enum? Baseline config would be an empty set.
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.
Narrator: It didn't get any simpler.
We still likely want to test all combinations between the 2 workflowRuntimes we are launching. It's maybe overkill, but its not that slow so its worthwhile.
assertFalse(result.stateChanged) | ||
} | ||
|
||
@Test fun tick_children_handles_no_child_output() = runTest { |
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 / out of scope: we don't have a tick()
method any more, should we update test and var names 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.
Good point. #1012
2db3e12
to
eefa11e
Compare
…r on State Change Only
eefa11e
to
19d02fc
Compare
aiming to satisfy #985 .
After applying an action, we add another layer of information to what we pass to the runtime. This is the
ActionApplied
data class which includes both the output and whether or not the state was changed.If output is produced, we pass it up the parent immediately (as before), but keep the
stateChanged
value sticky if it has changed for any child.This is how it is done in
WorkflowNode
:In the runtime loop then, we look at the
ActionProcessingResult
from theselect
statement and if it is anActionApplied
we can choose whether or not to render based on whetherstateChanged == true
.We also set up a
RuntimeConfig
extension -renderingOnStateChangeOnly
to turn this logic on in the runtime. It also adds tests specifically for it and new shards to run all of our tests against this runtime.