How can i invalidate a complete tree of composables. So i want to force that all childs gets rendere...
t
How can i invalidate a complete tree of composables. So i want to force that all childs gets rendered again. (I have a very special use case where this is necessary) My problem is that compose optimizes to much 😄
I already tried to increase a counter in a state. But this does not invalidates all childs recursively.
f
The first answer I can think of is to use
staticCompositionLocalOf
but there might be some better solution. But I would really like to see the very special use case if you don't mind sharing. Documentation of `staticCompositionLocalOf`:
Create a CompositionLocal key that can be provided using CompositionLocalProvider. Changing the value provided will cause the entire tree below CompositionLocalProvider to be recomposed, disabling skipping of composable calls.
A static CompositionLocal should be only be used when the value provided is highly unlikely to change.
t
Thanks @Filip Wiesner for this fast answer i will have a look. My use case is some kind of hot code reload feature for Compose for Desktop. When it is working i will post the result and also the code.
f
Oh right! You are working on that "LiveCanvas component". I saw your post in #compose-desktop
t
I am now able to compile and execute code inside of a compose application. But the compose part is not updated in some cases. So i try to get it a little bit more reliable now.
f
Good luck with that 🤞
t
Unfortunately this does not solve my problem. Or maybe i did some thing wrong. Documentation is not clear to me. I now implemented it this way:
Copy code
val providableLiveObj = staticCompositionLocalOf {
    liveObj
}
Box() {
    block(providableLiveObj.current, version)
}
block is:
block: @Composable I.(Int) -> Unit
f
I think you have to use
Provider
first:
Copy code
CompositionLocalProvider(providableLiveObj provides ...) {
    block(providableLiveObj.current, version)
}
And than you have to change the value to something else. And be sure that the new values is not equal to the last one
t
yes now it works 😄 wohhoooo
🙌 1
I will do some code cleanups and prepare a demonstration. Will share the code soon.
Give me maybe one day
f
And make sure to change providableLiveObj to ProvidableLiveObj 😛 Edit: LocalProvidableLiveObj 😄
t
for me it makes no sense that i have to provide a factory to staticCompositionLocalOf And than later provide this value
Why is this needed. Currently i provide the same value for both
Copy code
val providableLiveObj = staticCompositionLocalOf {
        liveObj
    }
    CompositionLocalProvider(providableLiveObj provides liveObj) {
        Box() {
            block(providableLiveObj.current, version)
        }
    }
It works but it looks ugly to me 😄
f
I was just referring to the naming convention of CompotiotionLocals to start with LocalX 😄
t
Yes ok i see.
a
What about
currentRecomposeScope.invalidate()
?
t
No for my use case that is not enough. Just tried it.
There are still some other problems i see. It looks like some times some lambdas get not updated. But i think the Compose part works fine when i use this staticCompositionLocal
Hmm it looks like even staticCompositionLocal is not enough for my use case. Only when i remove every composables for one frame and than add them again every thing is really reloaded.
f
Give me maybe one day
I knew you'll regret this :D
t
The lambda inside of this button does not get changed:
Copy code
Button(onClick = { text.value = "Button pressed 1" }) {
    Text("Press button1")
}
So when i change the text of Text it gets updated. But when i change the text inside the onClick lambda the old lamda is executed again.
Of course i think without seeing my live update code you can not really help me. But the class that implements the composable gets reloaded during runtime. Not only this class all classes which are in this kotlin file.
Ok but for now it works with a short flickering 😄 With the side effect that all remembered variables get initialized again.
Ok i found a solution without staticCompositionLocalOf
Copy code
key(liveObj) {
    liveObj?.let { block(it) }
}
Also does not flicker any more. But of course the remembered variables are gone.
s
cc @Adam Powell @Leland Richardson [G] @jim ^
If I understand the use case, you're swapping the code but not sending new data to a composable and wish to trigger recomposition?
t
Yes. I think using key is fine for my use case. But if you have any better idea would be great to hear.
s
Yea - key will work well, though as you mentioned you'll drop the previous values of remember.
If anyone knows a way to have your recomposition and keep your locals, it'll be those three 🙂
t
Using the first approached "staticCompositionLocalOf" worked but some lambdas (Maybe all lambdas which are not composables) are not reloaded.
s
Yea there's a lot of optimizations around how lambdas work as values to make them stableish that Leland might have input on
a
you could try to use
Recomposer.Companion.saveStateAndDisposeForHotReload()/loadStateAndComposeForHotReload()
- the latter accepts the token returned by the former but today it's just a placeholder
they're marked internal but they're there for android studio to be able to poke at via the debugger
t
I see this function but for me it is not clear how i have to use them. So i would assume i call saveStateAndDisposeForHotReload() when i do have a reloaded composable. But when and where to call the loadStateAndComposeForHotReload()? Should i call this in the composable context? Maybe this way:
Copy code
val token = remember(liveObj) {
    saveStateAndDisposeForHotReload()
}
loadStateAndComposeForHotReload(token)
libeObj.composable()
l
@Timo Drick perhaps i need to understand the use case better. the lambda didn’t change because the previous value was equivalent since its entire capture scope was equal. is your code relying on the lambda getting set again? it might help if i understand better the broad goal here
t
I change the code above a littebit. Maybe it is easier to understand now. The liveObj is a instance of a class that is hot reloaded. And this class do have a function which provides the composable to render
l
why does that result in you needing to invalidate the whole hierarchy?
t
I am not sure. One guess was because of optimizations of compose. When i do have e.g.:
Text("Test1")
than change the code to:
Text("Test2")
it does not change the displayed text without using at least the staticCompositionLocalOf approach
When i do have a Button with an onClick listener and change the content of the click listener this new content is not executed after hot relaod.
When i am using a function instead of a lambda it kind of works.
l
hmm
t
I am preparing a gitlab repo where you can hava a look soon. It is less code than expected to get all this running.
l
i think we need to take a step back. i would rather we interpret the behavior above as a “bug” that needs to be understood and fixed, versus an optimization that needs to be worked around by invalidating the entire hierarchy 🙂
t
Code is commited here: https://gitlab.com/compose1/livecomposable in the file live_compile.kt i do have the different method of invalidation:
Copy code
/*
    // Without any enforced recursive invalidation it does not work at all :-(
    liveObj?.let { block(it) }
    */

    // This method of hot reload swapping is working but lambda functions are not swapped reliable
    /*
    val LocalLiveObj = staticCompositionLocalOf {
        liveObj
    }
    CompositionLocalProvider(LocalLiveObj provides liveObj) {
        LocalLiveObj.current?.let { block(it) }
    }
    */

    // This method of hot reload swapping is working well as far as i tested but the remembered variables get lost.
    key(liveObj) {
        liveObj?.let { block(it) }
    }

    /*
    //TODO figure out how this can be used and how to call internal functions
    val token = remember(liveObj) {
        saveStateAndDisposeForHotReload()
    }
    loadStateAndComposeForHotReload(token)
    composeableBlock(libeObj)
     */
Just start the main in main.kt and change code during runtime in live_compose.kt.
The code all together to support hot reload is under 200 loc.
@Adam Powell it looks like i have to call Recomposer.Companion.saveStateAndDisposeForHotReload() inside of a LaunchedEffect coroutineScope. At least than i do not get any errors. But than the complete composition is empty.
l
ok. i understand what you’re doing better now. this is a pretty neat idea actually. I think it might require more digging for me to understand exactly what’s going on. but regardless, there are assumptions that compose will make that won’t quite be compatible with this. At it’s core, let’s say we have two functions that are the same “function”, just compiled at two different times, with slightly different code. (ie, a “Hello” literal changed to a “Hello World” literal). Compose won’t generate code that will handle swapping these two functions safely
the safest way to switch between the two instances of these functions would be to surround it with
key
like you’re doing in the linked file. but as mentioned above, this will lose all state beneath that key call
t
Ok thanke you very much for looking into this. I think using key is fine. Because at the end the compilation is not faster than compiling with IntelliJ so in practice the hotreload would be used for just a small part of the app to work on fine tuning the UI temporary.