https://kotlinlang.org logo
#compose
Title
# compose
t

Travis Griggs

02/27/2024, 7:40 PM
I think derivedState is just so dang cool sometimes. The ability to wrap a computation that touches a lot of different mutableStates indirectly, and just have it be aware of all of them without having to wire up a ton of deep dependencies is tres cool. I have a case now, that I'm curious if there's a good pattern to cope with. I have this function soonesetRun() that computes disposition of something happening given a certain time from a number of inputs. If any of the inputs changes, then the derivedState dutifully invalidates. But there's also a point where the value computed becomes invalid because... time moved on. I don't want to spin a loop to force an update. Can I somehow make the derivedState invalidate itself when access to its cached value determines it's expired? To be more concrete:
Copy code
val activeRun by remember { derivedStateOf { plans.soonestRun(region.valve) as? ValveRunActive } }
But the ValveActiveRun object can compute "isExpired" quickly. So I'd like to somehow wire that in so that when someone accesses activeRun, it's checking whether the cachedValue.isExpired and if so, recomputing soonestRun() even though none of the touched mutable states inside the computation have changed.
a

Alex Vanyo

02/27/2024, 7:47 PM
Can you turn “time” into a meaningful value that is backed by snapshot-state? Then “time” isn’t that special - it just becomes another input that can change
It sounds like your
soonestRun()
calculation probably calls into some sort of
now()
method to get the current time. Can you turn that source for time into a piece of snapshot state that is an input to the method?
t

Travis Griggs

02/27/2024, 8:16 PM
soonestRun does invoke now(). But what it returns is an interval that is usually in the granularity of hours, but sometimes minutes. The interval is expired nice it’s “passed”. It’s a relatively involved computation, and actually have a number of these active in different cards. We do use a timeRatchetAsState thing to derive “elapsed” and “remaining” counters throughout our UI. So I could feed that into the computation, but then it would be rerunning every second. So I was hoping to avoid that. Then again, maybe I need to understand the whole snapshot thing you refer to. It’s just a black box to me at the moment.
c

Chuck Jazdzewski [G]

02/27/2024, 8:58 PM
One way to do this is to have a
mutableStateOf
that represents a serial number of the values of
soonestRun(region.value)
that is read in the
derivedStateOf
but is not directly part of the result. Changing this serial number will cause the
derivedStateOf
to be recalculated the next time is is read. That is, whenver
isExprired
might have changed, increment the serial number. This will cause
isExprired
to be checked again. This corresponds roughtly to
isExpried
which would then be
val isExpried get() = currentSerialNumber != lastSerialNumber
if it is needed.
t

Travis Griggs

02/28/2024, 6:06 PM
Been wading around in the source code for DerivedState. Wish I understand this better. Have 10x questions probably now. 🙂 I was hopeful that the SnapshotMutationPolicy would be useful for this problem. But IIRC, it seems that the mutation policy just allows you to tune what kind of change is considered a "change" to the currentValue of the derivedState. It's too bad this code is all privatey. I think what I really wish I could do is tune/augment the computation of the inner class ResultRecord.isValid. Or do something in currentValue I guess. Thanks for the ideas @Chuck Jazdzewski [G]. I'll see if I can't figure out how to apply that in this case
c

Chuck Jazdzewski [G]

02/28/2024, 6:56 PM
If would be interested in knowing what change you propose. Even if we don't take it, it will help me understand your case and maybe come up with a change that would be helpful or an extension to the mutation policy that would be helpful.
We are also looking into ways to reduce the overhead of using
derivedStateOf
so I also want to make sure we don't break your case.
t

Travis Griggs

02/28/2024, 7:48 PM
I ended up doing the following: which seems to work. Sad that my exuberance over what derivedState/State/etc does automagicallly could do in one line had to be turned into 16 lines, but I still think it's pretty cool:
Copy code
var expiryTrigger =
    remember { mutableIntStateOf(1) } //  "flip flop" state that we can use to invalidate the memoized soonestRun computation when it's no longer valid
val soonestRun: ValveRunDisposition? by remember {
    derivedStateOf {
       expiryTrigger.value // access this simply to create a dependency on expiryTrigger
       plans.soonestRun(region.valve)
    }
}
val activeRun: ValveRunActive? by remember {
    derivedStateOf {
       if (soonestRun?.isValid(atNow = currentTime.value) == false) {
          expiryTrigger.value *= -1
       }
       soonestRun as? ValveRunActive
    }
}
My initial attempt, I used the expiryTrigger as a key to the
remember(...)
for
soonestRun
, but that didn't cause it to invalidate. Had to do the access inside. I'm not sure I understand why that is
a

Alex Vanyo

02/28/2024, 9:20 PM
Is
currentTime.value
backed by snapshot-state?
mutableStateOf
or similar?
The backwards write of the
expiryTrigger
is a bit suspicious - I’m wondering if you could mold the logic into something like
Copy code
val currentInterval by remember {
    derivedStateOf {
        computeInterval(currentTime.value)
    }
}
val activeRun by remember {
    derivedStateOf {
        plans.soonestRun(currentInterval, region.valve)
    }
}
t

Travis Griggs

02/28/2024, 10:17 PM
currentTime is a produceState. It's a 1s clock tick
👍 1
105 Views