I've slipped the LazyColumn a LazyList State like this:
val listState = LazyListState() @Composable fun foo() { LazyColumn( state = listState
Now I want to call listState.scrollToItem(index) in a Player.Listener:
player.addListener(object : Player.Listener { override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { fileIndex(player.currentMediaItemIndex).let { highlightCurrentlyPlaying(it, true) GlobalScope.launch { listState.scrollToItem(it) // <- LOOK HERE } } } }
This unfortunately crashes my app:
FATAL EXCEPTION: main
Process: cyberdynesoftware.musink, PID: 26555
java.lang.IllegalArgumentException: performMeasureAndLayout called during measure layout
at androidx.compose.ui.internal.InlineClassHelperKt.throwIllegalArgumentException(InlineClassHelper.kt:36)
at androidx.compose.ui.node.MeasureAndLayoutDelegate.measureAndLayout(MeasureAndLayoutDelegate.kt:825)
at androidx.compose.ui.platform.AndroidComposeView.measureAndLayout(AndroidComposeView.android.kt:1735)
at androidx.compose.ui.node.Owner.measureAndLayout$default(Owner.kt:258)
at androidx.compose.ui.platform.AndroidComposeView.dispatchDraw(AndroidComposeView.android.kt:2063)
at android.view.View.draw(View.java:22795)
at android.view.View.updateDisplayListIfDirty(View.java:21623)
at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4572)
at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4545)
at android.view.View.updateDisplayListIfDirty(View.java:21567)
at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4572)
at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4545)
at android.view.View.updateDisplayListIfDirty(View.java:21567)
at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4572)
at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4545)
at android.view.View.updateDisplayListIfDirty(View.java:21567)
at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4572)
at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4545)
at android.view.View.updateDisplayListIfDirty(View.java:21567)
at android.view.ThreadedRenderer.updateViewTreeDisplayList(ThreadedRenderer.java:534)
at android.view.ThreadedRenderer.updateRootDisplayList(ThreadedRenderer.java:540)
at android.view.ThreadedRenderer.draw(ThreadedRenderer.java:616)
at android.view.ViewRootImpl.draw(ViewRootImpl.java:4632)
at android.view.ViewRootImpl.performDraw(ViewRootImpl.java:4348)
at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:3479)
at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:2233)
at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:9016)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1105)
at android.view.Choreographer.doCallbacks(Choreographer.java:896)
at android.view.Choreographer.doFrame(Choreographer.java:815)
at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:1090)
at android.os.Handler.handleCallback(Handler.java:938)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loopOnce(Looper.java:201)
at android.os.Looper.loop(Looper.java:288)
at android.app.ActivityThread.main(ActivityThread.java:7881)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:568)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1045)
Funny thing, nowhere in there I can find any of my code. But when I comment the line commented with LOOK HERE the exception goes away.
I'm out of my depth here. How do I set this up properly? I think there are two questions here:
How to properly slip the LazyColumn a LazyListState to be called from outside the Composable function? My global variable listState compiles, but I have my doubts that that's proper.
How to create a fire and forget CoroutineScope that allows me to call a suspend function from outside a suspend function? I don't think that memory leaks are something to worry about in this case. GlobalScope comes with pesky warnings without really telling me what to do...
GlobalScope is indeed the culprit here. It uses the Default dispatcher under the hood, but everything UI related must run on the Main dispatcher.
The reason for that is that the entire UI is single-threaded (albeit massively asynchronous) to guarantee that no race conditions can occur: One thread changing some state while another thread reads that state at the same time. This thread is usually the main thread, the only thread that the Main dispatcher provides.
In your case that means that listState.scrollToItem(it) must be moved to the Main dispatcher. You could do that by simply passing it as a parameter to launch:
GlobalScope.launch(Dispatchers.Main) {
listState.scrollToItem(50) // <- LOOK HERE
}
But as you already figured out, you shouldn't even use GlobalScope at all. It would be slightly better to use CoroutineScope(Dispatchers.Main) instead, but this also creates a dangling coroutine that you have no control over.
The proper solution is to either move this code to somewhere where you naturally have a coroutine scope available, like in a ViewModel. There you can use the viewModelScope that already uses the Main dispatcher, so you wouldn't even need to specify that afterwards.
In all other cases your class or your function where you placed your code should accept a parameter of type CoroutineScope. Then you don't have to create one ad-hoc when you actually want to launch a new coroutine, but you do need to pass one to create the class respectively call the function in the first place. How this is best solved depends on the overall architecture of your app. For classes a common solution would be to use a dependency injection framework like Hilt or Koin to inject the CoroutineScope if you cannot simply pass a fitting CoroutineScope down (like from the ViewModel), for functions you usually declare the scope as a receiver:
fun CoroutineScope.myFunctionThatLaunchesCoroutines() {
launch(Dispatchers.Main) {
// ...
}
}
myFunctionThatLaunchesCoroutines can now be simply be called from any suspend function without further ado.
In the specific case of performing suspending tasks in a callback you might want to have a look at callbackFlow. That would probably be the cleanest solution when using a layered app architecture: It allows you to use a CoroutineScope from where the Flow is collected (usually the UI, where you have easy access to a matching CoroutineScope).