Hello, what could cause recomposition to not trigg...
# compose
l
Hello, what could cause recomposition to not trigger in a
ComposeView
despite the
State
objects being mutated from the class holding them?
b
👋 Composable not visible nor tied to any lifecycle ?
👋🏼 1
incorrect use of the Stable annotation
l
At first, the
ComposeView
was in
Visibility.GONE
, but even after changing it, it's visible, but the UI keeps the render of the initial composition, and further compositions don't happen when the
State
s are mutated. I'm a bit surprised because I already use
ComposeView
successfully in other parts of the project, in a very similar way. And I'm not using the
@Stable
annotation.
b
https://developer.android.com/reference/kotlin/androidx/compose/ui/platform/ComposeView#ComposeView(android.content.[…]util.AttributeSet,kotlin.Int)
This 
android.view.View
 requires that the window it is attached to contains a 
ViewTreeLifecycleOwner
.
Are you in this case ?
l
I think it'd crash if it wasn't the case. I can tell you the
Context
is an
AppCompatActivity
.
I'm starting to think that I found a bug in Compose, and I don't understand what triggers it exactly.
b
It would be nice if you could isolate what's happening in a snippet so that we can have a look at it
a
What version are you using? If you are using 1.0 then you may want to try 1.1.0-alpha02 as there is a bug in 1.0 where in some cases state change doesn't cause recomposition.
l
1.0.1. I'll try the alpha, thanks for the suggestion 🙂
BTW, I'm trying to reproduce it in a separate project, but it's been half an hour with no success already, in addition to all the time spent trying to figure it out in the main project where I actually have the issue.
Still happening in 1.1.0-alpha02 😞
I'll try to introduce a
produceState
that loops updates a state that will display something, to see if that at least gets updated.
So, I added the following snippet, along with a
Text
to display its value:
Copy code
val iterationsState = produceState(0) {
        value = 100
        repeatWhileActive {
        value = 1000
            delay(500)
            value = value + 10
        }
    }
And I noticed that when the app cold starts, it briefly shows
1000
before updating to
0
. Subsequent Activity starts lead to
0
being displayed straightaway. It looks like something is resetting composition and freezes all future compositions.
Hello @Adam Powell, are you aware of a case where in some `ComposeView`s integrated in a hierarchy, recomposition might never work? I spent hours trying to figure it out, showing the value of
someValueFlow
, and only in one of the 3
ComposeView
I see the value being updated correctly.
Copy code
private val fileScope = MainScope()
val someValueFlow: StateFlow<Int> = MutableStateFlow(0).also {
    fileScope.launch {
        while (true) {
            delay(500)
            it.value++
        }
    }
}
I use
collectAsState()
in all cases, and in a new project it works, but in company's project, only in one of the 3 views, and I have no clue why. The one working was already there, the 2 others are new ones I added, just like in the brand new project. It seems like there's some shared mutable state under the hood of the Compose runtime that puts itself in an inconsistent state. I hope you're not mad at me pinging you 🙏🏼
a
I am not mad but I would prefer if @-mentioning team members did not become a habit in here for conversations we aren't already involved in. 🙂 The team is large and those of us who do monitor this channel do so regardless of notifications; if something that you think might be a bug isn't getting attention here then filing a bug on the issue tracker will be more effective
@-mentions are kind of shouting at us to get priority over any of the other threads that we might be looking through as well
l
I understand. I did so because I feared the number of messages in the thread would make it seem like it's resolved, while I am super puzzled and am clueless about where to look, having failed to make a reproducer in half a day.
a
are there any other windows in play? are these UIs experiencing issues in the larger app contained within dialog windows or something other than the main activity window?
👀 1
some useful places to put breakpoints to validate assumptions would be in the GlobalSnapshotManager in the global write observer to make sure
Snapshot.sendApplyNotifications
is getting called as expected, and in the Recomposer's snapshot apply observer that schedules recompositions
👀 2
the window-scoped recomposers will pause recompositions if the ViewTreeLifecycleOwner becomes stopped
(at the root of the window, installed by ComponentActivity by default)
l
As you suggested, I put breakpoints on these 2 lines, and they are hit several times, until they're no longer (I think it's each time a recomposition triggers). The composable is recomposed 3 times in a row after process birth, and 2 times after each Activity recreation after that. I think that inconsistency between first composition in process life and further compositions is already the symptom of an underlying issue. I'm also not sure it does exactly 2 recompositions instead of one for other recreations.
Actually, the culprit is not there, even though there is some potential issues with not keeping a strong reference to the scope, it cannot happen there because the implementation details here make sure there's always a strong reference kept to the
Continuation
somewhere.
Oh no… I think I know why that happens! Look closely at lines 44 to 48 in the snippet I screenshotted above. There's no strong reference to the coroutine being launched, and despite its scope having a
Job
inside (unlike
GlobalScope
or a custom implementation of the interface) that would keep a strong reference to the child coroutines, this scope itself has no strong reference pointing to it, so almost the entire object graph is orphan for the GC (garbage collector). I still don't understand how it can work for a while or forever in some cases though.
BTW, here's the commit that introduced that regression (found via "blame" on cs.android.com), click "expand all" to see the file: https://android-review.googlesource.com/c/platform/frameworks/support/+/1613087
The solution would be to put the scope in a member property. However, it looks like it's not the only problem. I could copy paste the code in the host
Activity
, in a way that would not let the coroutine be garbage collected, but it didn't fix the end issue of the Composable not updating.
Copy code
class TheActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        // ... Other code
        lifecycleScope.launch {
            val channel = Channel<Unit>(Channel.CONFLATED)
            launch(AndroidUiDispatcher.Main) {
                channel.consumeEach {
                    Snapshot.sendApplyNotifications()
                }
            }
            Snapshot.registerGlobalWriteObserver {
                channel.trySend(Unit)
            }
        }
    }
    // ... Other code
}
I'm still working through it, but here's something I found out already: The
setContent
function in
ComposeView
is not idempotent. In my project, if I call it after some delay, everything works as expected. However, if I call it at the earliest time possible, so as I'm creating the View hierarchy (I do this programmatically with Splitties Views DSL BTW), recomposition never happens. That means I have a workaround, but I'm not sure yet if I'll succeed in making a reproducer that is not my company's project.
Found how to reproduce! In my
Activity
, I'm calling
setContentView
twice with the same view hierarchy instance, expecting that function to be idempotent, and it is, in the Android platform. However, I see that the
ComponentActivity
from AndroidX calls the
initViewTreeOwners()
function everytime (no conditions), and the name of that function having "init" in it likely means that it wasn't designed to be idempotent, breaking the contract set by the Android platform. Whenever the view hierarchy that includes the composables is passed a second time to
setContentView
, in another dispatch loop (calling twice in a row doesn't break anything), the composables stop updating, and there's no going back.
Here's a full one-file reproducer that can be put in any Compose 1.0+ project.
Filed an issue for that latter finding: https://issuetracker.google.com/issues/197773820
👍 1
219 Views