Hey, I have a problem with `RememberObserver`: I h...
# compose
f
Hey, I have a problem with `RememberObserver`: I have a class, let me call it
MyState
, which implements the
RememberObserver
interface. I do cleanup stuff in
onForgotten
and
onAbandoned
. I use
remember
to remember the
MyState
instance:
Copy code
val state = remember { MyState() }
And I configure my activity to handle configuration changes automatically in this way in my AndroidManifest.xml:
Copy code
android:configChanges="colorMode|density|fontScale|keyboard|keyboardHidden|layoutDirection|locale|mcc|mnc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|touchscreen|uiMode"
So the instance will be kept across configuration changes. When I changed the activity’s orientation,
state
was kept, just as I expected. What I didn’t expect is that the
MyState
’s
onForgotten
method was called. This behavior left my remembered
MyState
instance in a wrong state. Is it a bug?
This seems only happen when the
state
is used in conditional flows, like this:
Copy code
val state = remember { MyState() }
if (!isLandscape) {
  Normal(state)
} else {
  Landscape(state)
}
99% sure it’s a bug 😂
z
I think you might be running into the situation described in the docs:
Compose may implicitly remember an object if doing so is required to restart the composition later, such as for composable function parameters.
If a single instance is remembered in more than one location in the same composition, its onRemembered and onForgotten will be called for each location in the composition.
A RememberObserver reference that is propagated to multiple parts of a composition might remain present in the composition for longer than expected if it is remembered (explicitly or implicitly) elsewhere, and a RememberObserver that appears more once can have its callback methods called multiple times in no meaningful order and on multiple threads.
f
I can’t figure out which part of the docs described my case.
I found a
key
function usage in the conditional branch is also relevant:
Copy code
val state = remember { MyState() }
if (!isLandscape) {
  key(state) {
    // do something
  }
  Normal(state)
} else {
  Landscape(state)
}
I made a simplest sample to reproduce this: https://github.com/fengdai/rememberobserver-issue-sample
Run the sample app, the UI is like the 1st screenshot. Rotate the screen, then rotate back. The bug will occur as the 2nd screenshot presents.
n
You might be able to workaround this by using a separate line, remember(state) { ActualObserver(state) }
z
Presumably
Normal
and
Landscape
are composable functions? So you're “passing a
RememberObserver
as a composable function parameter”. The runtime will implicitly remember those values to enable skipping, and when you remove one of them from the composition it will get an
onForgotten
call.
🤯 1
f
Yes, they are composable functions. But, why it doesn’t get an
onRemembered
call when it is implicitly remembered again?
z
You don't have to tag people who are already in threads
Idk why not. @Leland Richardson [G] can probably explain what I missed
l
can you track the number of times onRemembered / onForgotten get called? i think what is probably happening is
key(state)
is causing onForgotten to get called but not onRemembered, which would be a bug
we don’t guarantee that onRemembered/onForgotten only get called once, but i believe we do guarantee that they should get called symmetrically, which this would indicate that they’re not i think
if you add the counts and you increment/decrement and the bug still occurs, that would be useful to know
basically this:
Copy code
class MyState : RememberObserver {
    val isAvailable get() = remembered - forgotten > 0

    private var remembered = 0
    private var forgotten = 0

    override fun onRemembered() {
        remembered++
    }

    override fun onAbandoned() {
        remembered--
    }

    override fun onForgotten() {
        remembered--
    }
}
f
I did, and
isAvailable
still will be false after two screen rotations.
l
what are the counts for one screen rotation?
f
First rotation:
Copy code
remembered: 1
forgotten: 0
And second rotation:
Copy code
remembered: 0
forgotten: 0
l
hmm. interesting. i was expecting 2/1. not 1/0.
n
Note that if the snippet was pasted as is,
forgotten
will always be 0. So it’s probably 2/1 resulting in 1
l
eh, good point. i had a bug in there 😛
2/1 would make more sense to me, but still a bug
f
Yes, I copied and pasted it. 😂
l
2/1 with
forgotten--
instead of
remembered--
?
f
I’ve made a simplest sample to reproduce this issue: https://github.com/fengdai/rememberobserver-issue-sample I think you can update the link to point to it.
l
done. thanks