Material3日期选择器触发API加载LazyColumn时主线程卡顿问题
问题描述
我尝试将API获取的数据展示在LazyColumn中,每次从Material3日期选择器选择日期时都会触发API调用,但应用运行时每次API调用都会在Logcat中出现#1、#2的警告栈信息。尽管已在LaunchedEffect中执行相关逻辑,API调用有时仍耗时长达15秒,以下是相关代码示例供分析:
相关代码示例
TodayMatchesByLeague Composable
@Composable fun TodayMatchesByLeague(matchesList: List<TodayResponseItem?>) LazyColumn { items(matchesList.size) { TodayMatchItem(matchItem) } } }
FetchTodayGamesData Composable
@Composable fun FetchTodayGamesData( mainViewModel: MainViewModel = hiltViewModel() ) { val date = mainViewModel.dialogueDate.observeAsState().value!! if (mainViewModel.isClicked.value!!) { LaunchedEffect(key1 = null) { mainViewModel.getTodayMatches( date, 2023 ) mainViewModel.saveCalendarClicked(false) } } when (val state = mainViewModel.todayMatchesState.collectAsState().value ) { is TodayMatchesState.Empty -> {} is TodayMatchesState.Loading -> {} is TodayMatchesState.Error -> {} is TodayMatchesState.Success -> { TodayMatchesByLeague( matchesList = state.data.body()!!.response!! ) } } }
日期选择器逻辑
val selectedDate = rememberSaveable { mutableStateOf<LocalDate?>(LocalDate.now()) } val localDate = selectedDate.value viewModel.saveDialogueDate(localDate.toString()) val datePickerState = rememberDatePickerState() var sheetState by remember{ mutableStateOf(false) } val selectedDate1 = Instant.ofEpochMilli(datePickerState.selectedDateMillis ?: 0) .atZone(ZoneId.systemDefault()) .toLocalDate() if (datePickerState.selectedDateMillis != null) { selectedDate.value = selectedDate1 viewModel.saveCalendarClicked(true) } if (sheetState) BottomSheetDatePicker( state = datePickerState, onDismissRequest = { sheetState = false } ) IconButton( onClick = { sheetState = true } ) { Icon(imageVector = Icons.Default.CalendarMonth, contentDescription = "日历") }
ViewModel代码
@HiltViewModel class MainViewModel @Inject constructor(private val matchesRepository: MatchesRepository): ViewModel() { private var _todayMatchesState = MutableStateFlow<TodayMatchesState>(TodayMatchesState.Empty) val todayMatchesState: StateFlow<TodayMatchesState> = _todayMatchesState // 日历弹窗日期 private var _dialogueDate = MutableLiveData("") val dialogueDate: LiveData<String> = _dialogueDate private var _isClicked = MutableLiveData(true) val isClicked: LiveData<Boolean> = _isClicked fun saveCalendarClicked(isClicked: Boolean) { _isClicked.value = isClicked } fun saveDialogueDate(date: String) { _dialogueDate.value = date } fun getTodayMatches(date: String, season: Int) { _todayMatchesState.value = TodayMatchesState.Loading viewModelScope.launch(Dispatchers.IO) { try { val todayMatchesResponse = matchesRepository.getTodayMatches(date, season) _todayMatchesState.value = TodayMatchesState.Success(todayMatchesResponse) } catch (exception: HttpException) { _todayMatchesState.value = TodayMatchesState.Error("出现错误") } catch (exception: IOException) { _todayMatchesState.value = TodayMatchesState.Error("无网络连接") } } } }
Logcat警告信息
#1 APP_SCOUT_WARNING
事件:APP_SCOUT_WARNING 线程:main 调用栈: at java.lang.Thread.currentThread(Native Method) at java.lang.ThreadLocal.get(ThreadLocal.java:162) at android.os.Looper.myLooper(Looper.java:323) at kotlinx.coroutines.android.HandlerContext.isDispatchNeeded(HandlerDispatcher.kt:137) at kotlinx.coroutines.DispatchedTaskKt.dispatch(DispatchedTask.kt:160) at kotlinx.coroutines.CancellableContinuationImpl.dispatchResume(CancellableContinuationImpl.kt:474) at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl(CancellableContinuationImpl.kt:508) at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl$default(CancellableContinuationImpl.kt:497) at kotlinx.coroutines.CancellableContinuationImpl.resumeWith(CancellableContinuationImpl.kt:368) at kotlinx.coroutines.flow.StateFlowSlot.makePending(StateFlow.kt:284) at kotlinx.coroutines.flow.StateFlowImpl.updateState(StateFlow.kt:349) at kotlinx.coroutines.flow.StateFlowImpl.setValue(StateFlow.kt:316) at coil.compose.ConstraintsSizeResolver.measure-3p2s80s(AsyncImage.kt:209) at androidx.compose.ui.node.BackwardsCompatNode.measure-3p2s80s(BackwardsCompatNode.kt:312) at androidx.compose.ui.node.LayoutModifierNodeCoordinator.measure-BRTryo0(LayoutModifierNodeCoordinator.kt:186) at androidx.compose.foundation.layout.SizeNode.measure-3p2s80s(Size.kt:838) at androidx.compose.ui.node.LayoutModifierNodeCoordinator.measure-BRTryo0(LayoutModifierNodeCoordinator.kt:186) at androidx.compose.foundation.layout.PaddingNode.measure-3p2s80s(Padding.kt:397) at androidx.compose.ui.node.LayoutModifierNodeCoordinator.measure-BRTryo0(LayoutModifierNodeCoordinator.kt:186) at androidx.compose.ui.node.LayoutNodeLayoutDelegate$performMeasureBlock$1.invoke(LayoutNodeLayoutDelegate.kt:255) at androidx.compose.ui.node.LayoutNodeLayoutDelegate$performMeasureBlock$1.invoke(LayoutNodeLayoutDelegate.kt:254) at androidx.compose.runtime.snapshots.Snapshot$Companion.observe(Snapshot.kt:488) at androidx.compose.runtime.snapshots.SnapshotStateObserver$ObservedScopeMap.observe(SnapshotStateObserver.kt:501) at androidx.compose.runtime.snapshots.SnapshotStateObserver.observeReads(SnapshotStateObserver.kt:257) at androidx.compose.ui.node.OwnerSnapshotObserver.observeReads$ui_release(OwnerSnapshotObserver.kt:133) at androidx.compose.ui.node.OwnerSnapshotObserver.observeMeasureSnapshotReads$ui_release(OwnerSnapshotObserver.kt:113) at androidx.compose.ui.node.LayoutNodeLayoutDelegate.performMeasure-BRTryo0(LayoutNodeLayoutDelegate.kt:1622) at androidx.compose.ui.node.LayoutNodeLayoutDelegate.access$performMeasure-BRTryo0(LayoutNodeLayoutDelegate.kt:39) at androidx.compose.ui.node.LayoutNodeLayoutDelegate$MeasurePassDelegate.remeasure-BRTryo0(LayoutNodeLayoutDelegate.kt:623) at androidx.compose.ui.node.LayoutNodeLayoutDelegate$MeasurePassDelegate.measure-BRTryo0(LayoutNodeLayoutDelegate.kt:599) at androidx.compose.foundation.layout.RowColumnMeasurementHelper.measureWithoutPlacing-_EkL_-Y(RowColumnMeasurementHelper.kt:120) at androidx.compose.foundation.layout.RowColumnMeasurePolicy.measure-3p2s80s(RowColumnImpl.kt:69) at androidx.compose.ui.node.InnerNodeCoordinator.measure-BRTryo0(InnerNodeCoordinator.kt:134) at androidx.compose.foundation.layout.PaddingNode.measure-3p2s80s(Padding.kt:397) at androidx.compose.ui.node.LayoutModifierNodeCoordinator.measure-BRTryo0(LayoutModifierNodeCoordinator.kt:186) at androidx.compose.ui.node.LayoutNodeLayoutDelegate$performMeasureBlock$1.invoke(LayoutNodeLayoutDelegate.kt:255) at androidx.compose.ui.node.LayoutNodeLayoutDelegate$performMeasureBlock$1.invoke(LayoutNodeLayoutDelegate.kt:254) at androidx.compose.runtime.snapshots.Snapshot$Companion.observe(Snapshot.kt:488) at androidx.compose.runtime.snapshots.SnapshotStateObserver$ObservedScopeMap.observe(SnapshotStateObserver.kt:501)
#2 APP_SCOUT_HANG
事件:APP_SCOUT_HANG 线程:main 调用栈: at java.lang.ref.Reference$SinkHolder.-$$Nest$sfgetfinalize_count(Unknown Source:2) at java.lang.ref.Reference.reachabilityFence(Reference.java:342) at libcore.util.NativeAllocationRegistry.registerNativeAllocation(NativeAllocationRegistry.java:281) at android.graphics.ColorFilter.getNativeInstance(ColorFilter.java:68) at android.graphics.Paint.getNativeInstance(Paint.java:738) at android.graphics.BaseRecordingCanvas.drawBitmap(BaseRecordingCanvas.java:95) at androidx.compose.ui.graphics.AndroidCanvas.drawImageRect-HPBpro0(AndroidCanvas.android.kt:275) at androidx.compose.ui.graphics.drawscope.CanvasDrawScope.drawImage-AZ2fEMs(CanvasDrawScope.kt:257) at androidx.compose.ui.node.LayoutNodeDrawScope.drawImage-AZ2fEMs(Unknown Source:24) at androidx.compose.ui.graphics.drawscope.DrawScope.drawImage-AZ2fEMs$default(DrawScope.kt:566) at androidx.compose.ui.graphics.vector.DrawCache.drawInto(DrawCache.kt:102) at androidx.compose.ui.graphics.vector.VectorComponent.draw(Vector.kt:181) at androidx.compose.ui.graphics.vector.VectorPainter.onDraw(VectorPainter.kt:248) at androidx.compose.ui.graphics.painter.Painter.draw-x_KDEd0(Painter.kt:212) at androidx.compose.ui.draw.PainterNode.draw(PainterModifier.kt:342) at androidx.compose.ui.node.LayoutNodeDrawScope.drawDirect-x_KDEd0$ui_release(LayoutNodeDrawScope.kt:105) at androidx.compose.ui.node.LayoutNodeDrawScope.draw-x_KDEd0$ui_release(LayoutNodeDrawScope.kt:86) at androidx.compose.ui.node.NodeCoordinator.drawContainedDrawModifiers(NodeCoordinator.kt:376) at androidx.compose.ui.node.NodeCoordinator.draw(NodeCoordinator.kt:365) at androidx.compose.ui.node.LayoutModifierNodeCoordinator.performDraw(LayoutModifierNodeCoordinator.kt:265) at androidx.compose.ui.node.NodeCoordinator.drawContainedDrawModifiers(NodeCoordinator.kt:373) at androidx.compose.ui.node.NodeCoordinator.draw(NodeCoordinator.kt:365) at androidx.compose.ui.node.LayoutNode.draw$ui_release(LayoutNode.kt:962) at androidx.compose.ui.node.InnerNodeCoordinator.performDraw(InnerNodeCoordinator.kt:183) at androidx.compose.ui.node.LayoutNodeDrawScope.drawContent(LayoutNodeDrawScope.kt:66) at androidx.compose.material.ripple.AndroidRippleIndicationInstance.drawIndication(Ripple.android.kt:270) at androidx.compose.foundation.IndicationModifier.draw(Indication.kt:346) at androidx.compose.ui.node.BackwardsCompatNode.draw(BackwardsCompatNode.kt:350) at androidx.compose.ui.node.LayoutNodeDrawScope.drawDirect-x_KDEd0$ui_release(LayoutNodeDrawScope.kt:105) at androidx.compose.ui.node.LayoutNodeDrawScope.performDraw(LayoutNodeDrawScope.kt:76) at androidx.compose.ui.node.LayoutNodeDrawScope.drawContent(LayoutNodeDrawScope.kt:55) at androidx.compose.foundation.BackgroundNode.draw(Background.kt:159) at androidx.compose.ui.node.LayoutNodeDrawScope.drawDirect-x_KDEd0$ui_release(LayoutNodeDrawScope.kt:105) at androidx.compose.ui.node.LayoutNodeDrawScope.draw-x_KDEd0$ui_release(LayoutNodeDrawScope.kt:86) at androidx.compose.ui.node.NodeCoordinator.drawContainedDrawModifiers(NodeCoordinator.kt:376) at androidx.compose.ui.node.NodeCoordinator.access$drawContainedDrawModifiers(NodeCoordinator.kt:56) at androidx.compose.ui.node.NodeCoordinator$drawBlock$1$1.invoke(NodeCoordinator.kt:395) at androidx.compose.ui.node.NodeCoordinator$drawBlock$1$1.invoke(NodeCoordinator.kt:394) at androidx.compose.runtime.snapshots.Snapshot$Companion.observe(Snapshot.kt:488) at androidx.compose.runtime.snapshots.SnapshotStateObserver$ObservedScopeMap.observe(SnapshotStateObserver.kt:501) at androidx.compose.runtime.snapshots.SnapshotStateObserver.observeReads(SnapshotStateObserver.kt:257) at androidx.compose.ui.node.OwnerSnapshotObserver.observeReads$ui_release(OwnerSnapshotObserver.kt:133) at androidx.compose.ui.node.NodeCoordinator$drawBlock$1.invoke(NodeCoordinator.kt:394)
问题分析与解决方案
1. API调用延迟问题
- 核心原因:当前触发逻辑依赖
isClicked状态,且LaunchedEffect使用null作为key,导致重组时可能重复触发;日期选择器的状态更新逻辑存在冗余回调。 - 修复方案:
- 将
LaunchedEffect的key改为date,仅在日期变化时触发API,移除isClicked依赖:@Composable fun FetchTodayGamesData( mainViewModel: MainViewModel = hiltViewModel() ) { val date by mainViewModel.dialogueDate.observeAsState("") LaunchedEffect(date) { if (date.isNotEmpty()) { mainViewModel.getTodayMatches(date, 2023) } } // 状态处理逻辑保持不变 } - 移除日期选择器中
viewModel.saveCalendarClicked(true)的调用,直接通过日期变化触发API。
- 将
2. Logcat警告问题
- #1警告分析:Coil的
AsyncImage在UI测量阶段更新StateFlow,导致主线程协程调度冲突。- 修复:给
AsyncImage设置固定尺寸,避免动态测量时修改状态;升级Coil到最新稳定版本。
- 修复:给
- #2挂起警告分析:主线程绘制矢量图时Native内存分配阻塞,多因大量图标同时绘制或资源过大。
- 修复:使用
rememberVectorPainter复用矢量图标绘制实例;将常用图标提取为全局常量;检查并释放未回收的Native资源。
- 修复:使用
3. 其他优化点
- 移除强制非空断言,添加空值判断避免崩溃:
is TodayMatchesState.Success -> { val response = state.data.body()?.response ?: emptyList() TodayMatchesByLeague(matchesList = response) } - 添加API防抖,避免快速切换日期时重复调用:
private val debounceJob = AtomicReference<Job?>() fun getTodayMatches(date: String, season: Int) { debounceJob.getAndSet(null)?.cancel() debounceJob.set(viewModelScope.launch(Dispatchers.IO) { delay(300) // 300ms防抖延迟 _todayMatchesState.value = TodayMatchesState.Loading try { val todayMatchesResponse = matchesRepository.getTodayMatches(date, season) _todayMatchesState.value = TodayMatchesState.Success(todayMatchesResponse) } catch (e: HttpException) { _todayMatchesState.value = TodayMatchesState.Error("出现错误") } catch (e: IOException) { _todayMatchesState.value = TodayMatchesState.Error("无网络连接") } }) }
内容的提问来源于stack exchange,提问作者Mahmoud Nabil
相关产品推荐
相关产品推荐

