Nana Vong

    Nana Vong

    1 year ago
    if I do like this, Surface will always recompose .but if I use thread or dispatch another thread Surface will not recompose .Why ?
    Surface {
        darkMode.value = !darkMode.value
    }
    this way not recompose:
    Surface {
        thread {
            darkMode.value = !darkMode.value
        }
    }
    Adam Powell

    Adam Powell

    1 year ago
    You shouldn't do either one of these in real code 🙂 here's why it behaves this way:
    By reading
    darkMode.value
    in the right hand side of the expression in a composable function body in snippet 1, you've told compose that the correct answer for that code depends on
    darkMode.value
    as an input. You then immediately write
    darkMode.value
    .
    Compose always wants to recompose something if its inputs changed.
    Semantically speaking, you've told compose, "this statement is false" and asked it to ponder the philosophical implications of that declaration. 🙂
    Neither code snippet is idempotent; both introduce side effects of composition that produce different results depending on how many times the function recomposes.
    The second snippet launches a new thread as an uncontrolled side effect and performs the read and write there; compose has no indication that the read or write have something to do with the recompose scope that launched the thread.
    Nana Vong

    Nana Vong

    1 year ago
    Surface {
        lifecycleScope.launch {
            delay(1000)
            darkMode.value = !darkMode.value
        }
    }
    if add
    delay
    function it will not recompose ,or will always recompose 😂 , always recompose I understand, but after suspend and not recompose I need take some time to understand☹️
    Adam Powell

    Adam Powell

    1 year ago
    If
    lifecycleScope
    is coming from a
    LifecycleOwner
    then you've encountered the reason I dislike immediate coroutine dispatchers 🙂
    At some point
    lifecycleScope
    was changed to use
    Dispatchers.Main.immediate
    which will run that code to first suspension undispatched, which means compose will track the read.
    But once again we're talking about the mechanism, best practices would tell you to never
    coroutineScope.launch {}
    from a composable function body either, you should use the declarative
    LaunchedEffect
    instead.
    (I think we have a lint warning that will scold you for this code now)
    Dominaezzz

    Dominaezzz

    1 year ago
    Is the lint warning in android studio or the compiler plugin?
    Adam Powell

    Adam Powell

    1 year ago
    Android lint, so neither, but studio will enable it by default in the ide
    gildor

    gildor

    1 year ago
    the reason I dislike immediate coroutine dispatchers
    Same here, I was surprised when immediate dispatcher was used by default on lifecycleScope, it caused some bugs on our side after update and in general immediate dispatcher behaviour is much more tricky and doesn’t worth this optimization imo
    Nana Vong

    Nana Vong

    1 year ago
    Do you know where to look at this part of the source code? 🥺
    Adam Powell

    Adam Powell

    1 year ago
    @gildor I don't agree with the use of an immediate dispatcher there either, but in its defense the reason it was done was for timing of avoiding a lifecycle state change before the dispatched continuation runs, not optimization. I'm glad that
    CoroutineStart.UNDISPATCHED
    graduated to stable recently since that almost always addresses whatever lifecycle+timing issue people tend to reach for an immediate dispatcher to solve, but without the unexpected reentrance pitfalls.
    @Nana Vong which part of what source code?
    Nana Vong

    Nana Vong

    1 year ago
    @Adam Powell In
    thread
    and
    delay
    case , whether Composer has check call stack frame to choose recompose ?
    Adam Powell

    Adam Powell

    1 year ago
    the recomposition process takes a mutable snapshot using the snapshot API and installs read and write callbacks for that snapshot. It enters that snapshot to perform recomposition, which causes those callbacks to be invoked on snapshot state access. https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/[…]/androidx/compose/runtime/Recomposer.kt;l=788?q=Recomposer.kt
    The snapshot system knows the current snapshot by way of a form of ThreadLocal; it doesn't access the call stack frame, it accesses that special ThreadLocal to determine the current snapshot.
    The current call stack is only relevant in that the caller of recomposition installs and removes the recomposition snapshot (and its associated access callbacks) as the thread's current snapshot before and after calling recomposition.
    so if the snapshot state read isn't performed during that call to recompose itself, on that calling thread, it isn't recorded as an input value of the composition.
    And if it's not recorded as an input value of composition, changing it later will have no effect on that composition.
    Nana Vong

    Nana Vong

    1 year ago
    Thank u very much, I think my understanding of Compose is getting better🤝