Search code examples
androidandroid-jetpack-composeretrofit2kotlin-coroutines

Message (the application may be doing too much work on its main thread) while viewing API data inside a LazyColumn


I'm trying to set retrieved data from an API into a LazyColumn, which will be called every time a date is picked from a Material3 date picker, but during the app, these warning traces (#1, #2) in Logcat appeared in every API call. Although I make it inside a LaunchedEffect, The app sometimes takes too much time (up to 15 seconds) in every API call.

I've attached some code samples for more explanation.

@Composable
fun TodayMatchesByLeague(matchesList: List<TodayResponseItem?>) {
    LazyColumn {
        items(matchesList.size) {
            TodayMatchItem(matchItem)
        }
    }
}
@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 = "Calendar")
}
//ViewModel
@HiltViewModel
class MainViewModel @Inject constructor(private val matchesRepository: MatchesRepository): ViewModel() {

    private var _todayMatchesState = MutableStateFlow<TodayMatchesState>(TodayMatchesState.Empty)
    val todayMatchesState: StateFlow<TodayMatchesState> = _todayMatchesState

    //Calendar Dialogue Date
    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("Something went wrong")
            }
            catch (exception: IOException) {
                _todayMatchesState.value = TodayMatchesState.Error("No internet connection")
            }
        }
    }
}

#1

Event:APP_SCOUT_WARNING Thread:main backtrace:
    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

Event:APP_SCOUT_HANG Thread:main backtrace:
    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)


Solution

  • Please make sure first that the API is not the bottlenck here. You can send a request to your API using PostMan or the Thunderclient Visual Studio Code Plugin. With these tools, you can check whether the API itself needs that much time to return the response.

    Once you sorted this out as a possible cause, I would suggest that you attach a debugger and set a breakpoint to the code within the LaunchedEffect. I see no obvious error in your code, but some smells:

    1.) LaunchedEffect or viewModelScope is redundant

    You are using both a viewModelScope and a LaunchedEffect combined for calling the getTodayMatches function. If you use a LaunchedEffect, you can directly call a suspend function without the need for a viewModelScope. So you could update your ViewModel function like this:

    suspend fun getTodayMatches(date: String, season: Int) {
        _todayMatchesState.value = TodayMatchesState.Loading
    
        try {
            val todayMatchesResponse = matchesRepository.getTodayMatches(date, season)
            _todayMatchesState.value = TodayMatchesState.Success(todayMatchesResponse)
        }
        catch (exception: HttpException) {
            _todayMatchesState.value = TodayMatchesState.Error("Something went wrong")
        }
        catch (exception: IOException) {
            _todayMatchesState.value = TodayMatchesState.Error("No internet connection")
        }
    }
    

    2.) Callback Handling seems odd

    Once the DatePicker is confirmed, you set the isClicked variable in the ViewModel to true. Then you use a side effect on another place to execute code when the variable becomes true. Instead of this approach, I would suggest that you directly call the ViewModel function when the selected date changes.

    var selectedDate by rememberSaveable { mutableStateOf<LocalDate?>(LocalDate.now()) }
    val datePickerState = rememberDatePickerState()
    var sheetState by remember{ mutableStateOf(false) }
    
    // Whenever selected date changes, call getTodayMatches
    LaunchedEffect(datePickerState.selectedDateMillis) {
        if (datePickerState.selectedDateMillis != null) {
            // update state variable
            selectedDate = Instant.ofEpochMilli(datePickerState.selectedDateMillis ?: 0)
                .atZone(ZoneId.systemDefault())
                .toLocalDate()
            viewModel.getTodayMatches(selectedDate.toString(), 2023)
        }
    }
    
    if (sheetState)
    
        BottomSheetDatePicker(
            // ...
        )
        // ...
    }
    

    3.) LazyColumn seems odd

    It seems like you are mixing two overloads of the items function. You probably wanted to do

    LazyColumn {
        items(matchesList) { matchItem ->
            TodayMatchItem(matchItem)
        }
    }
    

    4.) Single Source of Truth

    It seems like you are having a lot of duplication in your Composable and ViewModel. Either store the date in your ViewModel, or store it in the Composable. But now, you have a selectedDate variable in your Composable, and also at the same time you have a dialogueDate in your ViewModel. And you need a lot of code to keep these two synchronized. In my suggestion above, I store the date in the Composable.