Home > Mobile >  Processing list in Coroutines scope causes exception
Processing list in Coroutines scope causes exception

Time:02-20

I have a function that show markers (I call it tasks) on Google Maps. To make it cleaner, I chose an approach that iterates through the already displayed tasks and the new tasks. If a task already exists in maps and does not in the new list, I delete its marker. If the task is in both lists but its version has changed, I modify its marker . And if the task is only in the new list, i add its marker. Each time the user scrolls through the maps, I search for tasks based on the scrolled positon. The user can scroll multiple times, which should stop the previous call of the function.

the problem is that sometimes i get this exception

Fatal Exception: java.util.ConcurrentModificationException
       at java.util.ArrayList$Itr.next(ArrayList.java:860)
       at com.weryou.android.ui.missions.search.map.SearchMissionMapFragment$displayMissions$1.invokeSuspend(SearchMissionMapFragment.kt:435)
       at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
       at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
       at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
       at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
       at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
       at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)

And this exception

Fatal Exception: java.lang.IndexOutOfBoundsException: Index: 132, Size: 132
       at java.util.ArrayList.get(ArrayList.java:437)
       at kotlin.collections.CollectionsKt__MutableCollectionsKt.filterInPlace$CollectionsKt__MutableCollectionsKt(CollectionsKt__MutableCollectionsKt.java:284)
       at kotlin.collections.CollectionsKt__MutableCollectionsKt.removeAll(CollectionsKt__MutableCollectionsKt.java:269)
       at com.weryou.android.ui.missions.search.map.SearchMissionMapFragment$displayMissions$1.invokeSuspend(SearchMissionMapFragment.kt:220)
       at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
       at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
       at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
       at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
       at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
       at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)

This is my function

private val alreadyDisplayedMissions = ArrayList<Pair<MissionHeaderEntity, Marker>>()

private fun displayMissions(newMissions : List<MissionHeaderEntity>)
{
    lifecycleScope.launch(Dispatchers.Default) {
        
        val missionsToRemove : List<Pair<MissionHeaderEntity, Marker>> = alreadyDisplayedMissions.filter { oldMission ->
            
            val newVersionOfOldMission : MissionHeaderEntity? = newMissions.find { it.id == oldMission.first.id }
            
            newVersionOfOldMission == null || newVersionOfOldMission != oldMission.first
            
        }
        
        alreadyDisplayedMissions.removeAll { mission -> missionsToRemove.any { mission.first.id == it.first.id } }
        
        // Loop in the new missions to display in map all new missions with a determined location,
        // plus the tuto mission of the user if exists.
        val missionsToShow : List<Pair<MissionHeaderEntity, MarkerOptions>> = newMissions.filter { newMission ->
            (newMission.firstMission == true && newMission.owned == true)
            || alreadyDisplayedMissions.none { newMission.id == it.first.id }
            || (newMission.place?.address?.location?.latitude != null && newMission.place?.address?.location?.longitude != null)
        }.map { newMission ->
            
            // The icon resource of the marker to display
            val markerIcon : Drawable = when(newMission.state)
            {
                MissionState.BOOKED -> markerMissionBooked
                MissionState.AVAILABLE -> markerMissionAvailable
                MissionState.PRE_RELEASED -> markerMissionPreReleased
                else -> markerMissionOwned
            }
            
            // The icon of the marker to display .
            val markerBitmap : Bitmap = mapUtils.createMissionMarkerIcon(markerIcon, newMission.costing?.price.toCurrency(requireContext()))
            
            val markerLatitude : Double = newMission.place?.address?.location?.latitude!!
            val markerLongitude : Double = newMission.place?.address?.location?.longitude!!
        
            
            // The location of the marker to display.
            val markerLatLng = LatLng(markerLatitude, markerLongitude)
            
            val markerOptions : MarkerOptions = MarkerOptions()
                .position(markerLatLng)
                .icon(BitmapDescriptorFactory.fromBitmap(markerBitmap))
            
            Pair(newMission, markerOptions)
        }
        
        lifecycleScope.launch(Dispatchers.Main) {
            
            missionsToRemove.forEach { it.second.remove() }
            
            missionsToShow.forEach {
                
                val marker : Marker? = googleMap?.addMarker(it.second)
                marker?.tag = it.first
                
                alreadyDisplayedMissions.add(Pair(it.first, marker!!))
            }
        }
    }
}

What's wrong in my function and how to solve those exceptions?

CodePudding user response:

I assume the java.util.ConcurrentModificationException exception occurs for alreadyDisplayedMissions list. You can try to use a Job instance, returned by launch function. You can cancel the current job when the next one is started. In this case you should check whether the current job is active by using isActive property or ensureActive function:

private var job: Job? = null

private fun displayMissions(newMissions : List<MissionHeaderEntity>) {
    job?.cancel()
    lifecycleScope.launch(Dispatchers.Default) {
        // use ensureActive() everywhere before using alreadyDisplayedMissions list
        ensureActive()
        ... 
    }
}

ensureActive() throws CancellationException if the job is no longer active, so the execution will not be continued after this function.

  • Related