如何在可组合函数外部将LazyColumn滚动到某一项?
我给LazyColumn注入了一个LazyListState,如下所示:
val listState = LazyListState()
@Composable
fun foo() {
LazyColumn(
state = listState
现在我想在Player.Listener中调用 listState.scrollToItem(index):
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
}
}
}
}
这会不幸地使我的应用崩溃:
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)
有趣的是,在那里根本找不到我的代码。但当我把标有LOOK HERE的那一行注释掉后,异常就消失了。
我已经力不从心了。该如何正确地设置?我认为这里有两个问题:
- 如何把LazyListState正确传给LazyColumn,使其在Composable函数之外也能调用?我的全局变量
listState可以编译通过,但我怀疑那是不是正确的做法。 - 如何创建一个fire-and-forget的 CoroutineScope,以便从非suspend函数调用suspend函数?我觉得内存泄漏在这种情况下不是大问题。GlobalScope会弹出烦人的警告,却没有真正告诉我该怎么做……
解决方案
GlobalScope 确实是这里的罪魁祸首。它在内部使用 Default 调度器,但所有与UI相关的工作必须在 Main 调度器上运行。
原因在于整个UI是单线程的(尽管高度异步),以保证不会出现竞态条件:一个线程在改变某些状态的同时,另一个线程在读取同一状态。这个线程通常是主线程,也是 Main 调度器提供的唯一线程。
在你的场景中,这意味着 listState.scrollToItem(it) 必须切换到 Main 调度器。你完全可以通过把它作为参数传递给 launch 来实现:
GlobalScope.launch(Dispatchers.Main) {
listState.scrollToItem(50) // <- LOOK HERE
}
但正如你已经发现的,你甚至不应该使用 GlobalScope。改用 CoroutineScope(Dispatchers.Main) 虽然稍好一点,但这也会产生一个你无法控制的悬垂协程。
正确的做法是把这段代码移动到你自然拥有协程作用域的地方,例如在ViewModel中。在那里你可以使用已经使用 Main 调度器的 viewModelScope,这样你就不需要再在之后特别指定它。
在其他情况下,你放置代码的类或函数应该接收一个类型为 CoroutineScope 的参数。这样,当你真正想启动一个新的协程时,就不必临时再创建一个,但确实需要把这个参数传给类的构造或先调用的函数。最佳做法取决于你应用的整体架构。对于类,常见的解决方案是使用依赖注入框架(如Hilt或 Koin)来注入CoroutineScope,如果你不能简单地把合适的CoroutineScope传下去(例如从ViewModel),那么就需要用它来注入;对于函数,通常会将作用域声明为接收者:
fun CoroutineScope.myFunctionThatLaunchesCoroutines() {
launch(Dispatchers.Main) {
// ...
}
}
myFunctionThatLaunchesCoroutines 现在可以在任何CoroutineScope实例上调用,或者在任何 suspend 函数中的 coroutineScope 块中调用。
在回调中执行挂起任务的具体情形,你可能会看看 callbackFlow。在使用分层应用架构时,这很可能是最干净的解决方案:它允许你在Flow被收集的地方(通常是UI层,那里你更容易获得匹配的CoroutineScope)使用一个CoroutineScope。