Thread
#compose
    Timo Drick

    Timo Drick

    1 year ago
    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.
    Filip Wiesner

    Filip Wiesner

    1 year ago
    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.
    Timo Drick

    Timo Drick

    1 year ago
    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.
    Filip Wiesner

    Filip Wiesner

    1 year ago
    Oh right! You are working on that "LiveCanvas component". I saw your post in #compose-desktop
    Timo Drick

    Timo Drick

    1 year ago
    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.
    Filip Wiesner

    Filip Wiesner

    1 year ago
    Good luck with that 🤞
    Timo Drick

    Timo Drick

    1 year ago
    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:
    val providableLiveObj = staticCompositionLocalOf {
        liveObj
    }
    Box() {
        block(providableLiveObj.current, version)
    }
    block is:
    block: @Composable I.(Int) -> Unit
    Filip Wiesner

    Filip Wiesner

    1 year ago
    I think you have to use
    Provider
    first:
    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
    Timo Drick

    Timo Drick

    1 year ago
    yes now it works 😄 wohhoooo
    I will do some code cleanups and prepare a demonstration. Will share the code soon.
    Give me maybe one day
    Filip Wiesner

    Filip Wiesner

    1 year ago
    And make sure to change providableLiveObj to ProvidableLiveObj 😛 Edit: LocalProvidableLiveObj 😄
    Timo Drick

    Timo Drick

    1 year ago
    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
    val providableLiveObj = staticCompositionLocalOf {
            liveObj
        }
        CompositionLocalProvider(providableLiveObj provides liveObj) {
            Box() {
                block(providableLiveObj.current, version)
            }
        }
    It works but it looks ugly to me 😄
    Filip Wiesner

    Filip Wiesner

    1 year ago
    I was just referring to the naming convention of CompotiotionLocals to start with LocalX 😄
    Timo Drick

    Timo Drick

    1 year ago
    Yes ok i see.
    Albert Chang

    Albert Chang

    1 year ago
    What about
    currentRecomposeScope.invalidate()
    ?
    Timo Drick

    Timo Drick

    1 year ago
    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.
    Filip Wiesner

    Filip Wiesner

    1 year ago
    Give me maybe one day
    I knew you'll regret this 😄
    Timo Drick

    Timo Drick

    1 year ago
    The lambda inside of this button does not get changed:
    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
    key(liveObj) {
        liveObj?.let { block(it) }
    }
    Also does not flicker any more. But of course the remembered variables are gone.
    Sean McQuillan [G]

    Sean McQuillan [G]

    1 year ago
    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?
    Timo Drick

    Timo Drick

    1 year ago
    Yes. I think using key is fine for my use case. But if you have any better idea would be great to hear.
    Sean McQuillan [G]

    Sean McQuillan [G]

    1 year ago
    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 🙂
    Timo Drick

    Timo Drick

    1 year ago
    Using the first approached "staticCompositionLocalOf" worked but some lambdas (Maybe all lambdas which are not composables) are not reloaded.
    Sean McQuillan [G]

    Sean McQuillan [G]

    1 year ago
    Yea there's a lot of optimizations around how lambdas work as values to make them stableish that Leland might have input on
    Adam Powell

    Adam Powell

    1 year ago
    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
    Timo Drick

    Timo Drick

    1 year ago
    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:
    val token = remember(liveObj) {
        saveStateAndDisposeForHotReload()
    }
    loadStateAndComposeForHotReload(token)
    libeObj.composable()
    l

    Leland Richardson [G]

    1 year ago
    @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
    Timo Drick

    Timo Drick

    1 year ago
    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

    Leland Richardson [G]

    1 year ago
    why does that result in you needing to invalidate the whole hierarchy?
    Timo Drick

    Timo Drick

    1 year ago
    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

    Leland Richardson [G]

    1 year ago
    hmm
    Timo Drick

    Timo Drick

    1 year ago
    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

    Leland Richardson [G]

    1 year ago
    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 🙂
    Timo Drick

    Timo Drick

    1 year ago
    Code is commited here: https://gitlab.com/compose1/livecomposable in the file live_compile.kt i do have the different method of invalidation:
    /*
        // 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

    Leland Richardson [G]

    1 year ago
    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
    Timo Drick

    Timo Drick

    1 year ago
    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.