-
Notifications
You must be signed in to change notification settings - Fork 81
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
Change rememberRetain not to retain the value of removed node #1794
Change rememberRetain not to retain the value of removed node #1794
Conversation
Thanks for the contribution! Unfortunately we can't verify the commit author(s): roy.tk <r***@k***.com>. One possible solution is to add that email to your GitHub account. Alternatively you can change your commits to another email and force push the change. After getting your commits associated with your GitHub account, sign the Salesforce Inc. Contributor License Agreement and this Pull Request will be revalidated. |
50dc04e
to
affec25
Compare
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
Please be patient and don't tag maintainers, we will get to it when we get to 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.
I agree with the goal of having rememberRetained
match in behavior with rememberSaveable
and remember
: if one of those leaves composition in an if
, the state is wiped, and I would expect the same for rememberRetained
.
One thing not clear right now to me still after running some tests: if there is the if (...) { rememberRetained() }
inside the RetainedStateProvider
, I think by the same logic, state should also be lost.
However, in my tests, the state is still preserved in that case.
Maybe the LocalCanRetainChecker
inside RetainedStateProvider
shouldn't be set to the static CanRetainChecker.Always
?
circuit-retained/src/commonMain/kotlin/com/slack/circuit/retained/RememberRetained.kt
Outdated
Show resolved
Hide resolved
circuit-retained/src/androidMain/kotlin/com/slack/circuit/retained/AndroidContinuity.kt
Show resolved
Hide resolved
...tained/src/androidInstrumentedTest/kotlin/com/slack/circuit/retained/android/RetainedTest.kt
Show resolved
Hide resolved
@alexvanyo I agree with this
However, I couldn't reproduce the test case you mentioned. (I tested with this)
|
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 RetainedStateHolder
API looks very similar to #1168 as its now mimicking the SaveableStateHolder
API exactly, so curious to hear from Zac as to how that exploration went previously.
circuit-retained/src/commonMain/kotlin/com/slack/circuit/retained/RetainedStateHolder.kt
Show resolved
Hide resolved
circuit-retained/src/commonMain/kotlin/com/slack/circuit/retained/RetainedStateHolder.kt
Outdated
Show resolved
Hide resolved
Still want it! It essentially stalled because I hadn't picked it back up again. If that's something that naturally can fall out of this work then that'd be dope 👌 |
I’ve cherry-picked the To use the existing circuit’s From my understanding, the issue addressed in #1168 is resolved by using this It looks like all the issues have been resolved. If there’s anything else you’d like me to work on, please let me know! |
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.
Thanks a bunch! @alexvanyo is going to take one more look today as well then I think we can move ahead with this. Thanks a bunch for your patience in this. Josh is on vacation and we've had a bit of a chaotic month to start the new year
...InstrumentedTest/kotlin/com/slack/circuit/retained/android/RetainedStateRestorationTester.kt
Show resolved
Hide resolved
import com.slack.circuit.retained.RetainedValueProvider | ||
import com.slack.circuit.retained.rememberRetained | ||
|
||
/** Copy of [RetainedStateHolder] to return content 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: do you think we can/should reconcile these into one 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.
I think it would be good to consolidate them, but there are a few considerations:
-
Just like
SaveableStateHolder
in Add withCompositionLocalProvider to avoid backward writes #1451 , we need to investigate further whether converting aRetainedStateHolder
without a return value into one with a return value would cause any issues. -
The
RetainedStateHolder
without a return value internally usesReusableContent(key)
, while the one with a return value useskey(key)
. I’m not entirely sure if usingReusableContent(key)
in the case of a return value would cause any issues.
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.
Thank you for the work here and investigating the different options! The resulting API and usage matches my expectations more, and is more consistent with the SaveableStateProvider
setup.
// Now provide a new registry to the content for it to store any retained state in, | ||
// along with a retain checker which is always true (as upstream registries will | ||
// maintain the lifetime), and the other provided values |
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: this comment is now a bit out of date, it could probably point to how RetainedStateProvider
works
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.
Um.. yes I changed a little bit.
samples/star/src/commonMain/kotlin/com/slack/circuit/star/home/HomeScreen.kt
Show resolved
Hide resolved
Sorry one last request - would you mind doing a writeup of this change into the CHANGELOG.md file? It can be a longer/more detailed section to explain it since it's more significant of a change :). Let's make sure we highlight both the behavior changes and the new APIs. |
LocalCanRetainChecker provides CanRetainChecker.Always, | ||
content = content, | ||
) | ||
DisposableEffect(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.
Something very subtle I ran into experimenting with this change: I think this needs to have both entryCanRetainChecker
and childRegistry
as keys, or alterantively using rememberUpdatedState
with them to avoid operating on stale references to old childRegistry
s or entryCanRetainChecker
s.
This is exacerbated by #1919 - if the CanRetainChecker
instance changes, then this can get into a bad state where the child registry gets a new instance, and this disposable effect operates on the stale old 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.
Sorry for bringing up a difficult discussion again at the end of a long task, but to properly address the issue Alex raised, I think I need to understand how the Main Contributors think about the following concerns.
Should CanRetainChecker
only affect RetainedStateRegistry
?
CanRetainChecker
originally seems to have existed for RetainedStateRegistry
. However, since it is provided via CompositionLocal, the following code can occur:
val registry = remember { RetainedStateRegistry() }
CompositionLocalProvider(LocalRetainedStateRegistry provides registry) {
CompositionLocalProvider(LocalCanRetainChecker provides remember { CanRetainChecker { false }}) {
val value = rememberRetained { ... }
}
}
In this case, when registry.saveAll()
is called, the value should / should not be saved?
I believe that LocalCanRetainChecker
should only affect to RetainedStateRegistry level, not for individual rememberRetained. (which means the value should be saved)
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 right now we need to check in rememberRetained still. We could revisit being able to check it solely in RetainedStateRegistry but I think that's outside the scope of this PR maybe?
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.
Yes right. I agree. For this update, I’ll make sure LocalCanRetainChecker
continues to work as expected and still affects rememberRetained
.
However, the changes I made earlier caused an issue where LocalCanRetainChecker
no longer influenced rememberRetained
, so I’ve fixed that.
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.
Sounds good. Don't forget the changelog writeup!
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.
Of course! 🙂
I was actually writing a more detailed explanation of the changes I just uploaded.
But it’s taking a bit of time, so it’d be great if you could review it tomorrow or the day after!
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.
Alex, since the code in RetainedStateHolder
that referenced parent CanRetainChecker
has been removed, we no longer need to handle the scenario of CanRetainChecker changes separately.
To elaborate, in your #1920 you addressed changes in CanRetainChecker
within rememberRetained
. In my revised approach, whether the child registry is saved in RetainedStateHolder
is automatically determined by the CanRetainChecker
applied to rememberRetained
in below
rememberRetained(key = key) { RetainedStateRegistry() }
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 see - so the latest changes now fully rely upon CanRetainChecker
being used in rememberRetained
, instead of it also being checked inside RetainedStateHolder
. Makes sense to me!
As I mentioned above, Additionally, as part of this fix, I simplified the model by removing the unnecessary
|
LocalCanRetainChecker provides CanRetainChecker.Always, | ||
content = content, | ||
) | ||
DisposableEffect(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.
I think I see - so the latest changes now fully rely upon CanRetainChecker
being used in rememberRetained
, instead of it also being checked inside RetainedStateHolder
. Makes 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.
Awesome work! Thanks again for your patience with us as well, really excited to get this finally in
@@ -3,6 +3,40 @@ Changelog | |||
|
|||
**Unreleased** | |||
-------------- | |||
### Behaviour Change: rememberRetained |
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.
Great writeup!
Resolves #1783, I propose modifying the behavior of
rememberRetained
to improve consistency.Changes to rememberRetained Behavior
The following explanation is based on the code sample below.
Current Behavior
Before
saveAll
is called on the registryrememberRetained
added when the condition istrue
will be removed when the condition changes tofalse
.true
and the samerememberRetained
is called, the previous value is not retained.Test code for this behavior
After
saveAll
is called on the registryrememberRetained
added when the condition istrue
will be removed when the condition changes tofalse
.saveAll
is called on the registry.true
and the samerememberRetained
is called, the previous value is retained.Test code for this behavior
Challenges with the Current Behavior
saveAll
call on theRetainedStateRegistry
is unknown from the perspective of lower-level content, making retention depend on whethersaveAll
was called.remember
/rememberSaveable
, the node doesn’t retain the previous value when it’s removed and added back, making it easy to expectrememberRetained
to behave similarly.Due to these challenges, I propose modifying
rememberRetained
to behave likeremember
/rememberSaveable
, so thatrememberRetained
does not retain values when it is hidden and reshown in the compose node based on a condition.Internal Implementation Changes
Overall, I drew inspiration from the implementation of
SaveableStateHolder
andrememberSaveable
for these modifications.The following changes were made to achieve this goal:
1. Modify
RetainableSaveableHolder
to always unregister values from theRetainedStateRegistry
whenonForgotten
is called.In the previous implementation, if
rememberRetained
was removed from the node andonForgotten
was called, values were not unregistered from the registry ifcanRetain
wastrue
. As a result, all values were retained in the registry regardless of whetherrememberRetained
was present in the node whensaveAll
was called.Now, values are unregistered from the registry when
onForgotten
is called, so only values present in the composition node are retained whensaveAll
is called .2. Change the timing of
saveAll
inRetainedStateRegistry
Previously,
saveAll
was called on theRetainedStateRegistry
withinRetainableSaveableHolder
whenonForgotten
was invoked. However, due to the change in 1,onForgotten
of all childrememberRetained
nodes is called beforeonForgotten
ofrememberRetained { RetainedStateRegistry() }
, which would result in no values being retained.Therefore, I referenced the
SaveableStateHolder
implementation to create a separateDisposableEffect
to runsaveAll
, as shown below:By structuring it this way,
saveAll
can be called on theRetainedStateRegistry
before the child content's dispose stage. Thus, even ifonForgotten
is called on the child content'srememberRetained
and unregisters the value, it will still be retained.Compared to the existing implementation, this change needs to be applied to all cases where the
RetainedStateRegistry
is redefined in a nested way. To facilitate this process, I made theRetainedStateProvider
RetainedStateHolder
function public and modifiedNavigableCircuitContent
andPausableState
to use it. (24d8547ff654b4 )3. Change the timing of
saveAll
inAndroidContinuity
For the same reason as in 2,
continuityRetainedStateRegistry
also needs to callsaveAll
usingDisposableEffect
after declaring the child content.Example
However, in Android,
continuityRegistry
should always callsaveAll
whenonStop
is called. Since callingsaveAll
multiple times won’t cause issues, I modified it to callsaveAll
conveniently upononStop
or disposal.Changes to Test Code
Change 1
In
RetainedTest.kt
, parts wherenestedRegistry
is declared now useRetainedStateProvider
RetainedStateHolder
, assaveAll
must be manually called.affec25
Change 2
In
NestedRetainWithPushAndPop
andNestedRetainWithPushAndPopAndCannotRetain
, the tests assume the same value is retained regardless of theshowNestedContent
value, so I set the same key inRetainedStateProvider
to retain the values.However, since these assumptions may change with this PR, it may need verification to ensure correctness.
affec25
Change 3
To test
ImpressionEffect
, I modified the function attempting to recreate it. Previously, the condition was first set tofalse
to remove the child content and thensaveAll
was performed. In the modified behavior,saveAll
is performed first and then the child content is removed.465f98f
Additional Test Cases for the Reported Issue
I added test cases to cover the issue reported, which show which tests failed with the previous implementation and how they succeed with the modified implementation.
019c8cc, 5b279d4
When I initially reported this issue, I may have been somewhat aggressive due to the unexpected behavior. I apologize if it came across that way. Through working on this modification, I had the chance to explore the internal structure of
rememberRetained
and have come to appreciate how robust and well-designed many parts of thecircuit
library are. I have great respect for the efforts of the main contributors.After considering various approaches, I believe this change to the behavior of
rememberRetained
is the right direction. However, since the implementation ofrememberRetained
is a core part ofcircuit
, there may be differing perspectives on this.While having this PR approved would be ideal, if there are differing views, I hope we can use this as a foundation for further discussion.
Fixes #1783