Why does this code freeze everything and how can I...
# multiplatform
s
Why does this code freeze everything and how can I prevent it? I have this `PhotoSyncService`:
Copy code
private val scope = CoroutineScope(Dispatchers.Default)

class PhotoSyncService {

    private var jobMap: MutableMap<PhotoSource, Job> = mutableMapOf()

    fun sync(photoSource: PhotoSource, updateSyncInfo: (SyncInfo) -> Unit) {

        val existingJob = jobMap[photoSource]

        if (existingJob != null && existingJob.isActive)
            return

        val job = scope.launch {

            try {

                updateSyncInfo(
                    SyncInfo(
                        log = "Starting...",
                        status = SyncStatus.SYNCING
                    )
                )

                delay(2500)

                updateSyncInfo(
                    SyncInfo(
                        log = "Sync done.",
                        status = SyncStatus.SYNCED
                    )
                )

            } catch (ex: CancellationException) {

                updateSyncInfo(
                    SyncInfo(
                        log = "Sync canceled.",
                        status = SyncStatus.ERROR
                    )
                )
            }
        }

        jobMap[photoSource] = job
    }

    fun cancelSync(photoSource: PhotoSource) {

        val job = jobMap[photoSource]

        job?.let {

            job.cancel()
            jobMap.remove(photoSource)
        }
    }
}
And I got a
PhotoStore
class that uses this service like that:
Copy code
private fun forceSync(
    oldState: PhotoState,
    photoSource: PhotoSource
): PhotoState {

    photoSyncService.sync(photoSource) { syncInfo ->

        /*
         * This update comes from an background coroutine.
         * So we need to bring it back to the main thread.
         */
        launch(Dispatchers.Main) {
            this@PhotoStore.dispatch(PhotoAction.SyncInfoUpdate(photoSource, syncInfo))
        }
    }

    return oldState
}
The
forceSync()
is called on the main thread.
PhotoSyncService
is also initialized on the main thread. As soon as my
updateSyncInfo
lambda is called in my
launch
block it freezes the
PhotoSyncService
&
PhotoStore
... The line
jobMap[photoSource] = job
gives me a
InvalidMutabilityException
. I first thought there were other reasons, but after tracking everything down I know that it must be the lambda call- But why? Because of the call to "this"?
βœ… 1
n
In the
sync
method of
PhotoSyncService
, try to add
Dispatchers.Default
when you launch your coroutines :
Copy code
val job = scope.launch(Dispatchers.Default) {
    // ...
}
or you can surround your
delay(2500)
with a
withContext(Dispatchers.Default)
:
Copy code
withContext(Dispatchers.Default) {
    delay(2500)
}
s
Nope, even with
val job = scope.launch(Dispatchers.Default)
the
this.dispatch()
seems to capture everything. According to the docs
launch()
should be in
Dispatchers.Default
since my scope is there and it will be inherited. Is there a known bug that this does not work?
m
this@PhotoStore
will most likely be freezed from the
launch {}
s
How can I prevent that?
Is there a way to structure this code that it works with the memory model?
m
Move the
dispatch
outside the lambda
Something like this:
Copy code
val syncInfo = photoSyncService.sync(photoSource)
this@PhotoStore.dispatch(PhotoAction.SyncInfoUpdate(photoSource, syncInfo))
Think of
SyncInfo
being immutable messages
That are send back to your
PhotoStore
s
I can't. It's a lambda because during a sync multiple syncInfos are being sent updating a state. SyncInfo itself is an data class.
m
Make
sync
suspend
s
How does that help preventing Kotlin/Native to freeze my PhotoStore?
m
the
PhotoStore
will never be referenced from
PhotoSyncService
PhotoSyncService
does stuff in the background in a
suspend
function
When it's ready, it resumes in the main thread
You don't have to pass
PhotoStore
through the
launch {}
hoops
All the
PhotoSyncService
sees is a
Continuation
and it should work
I'm not 100% certain but this is how I'd do it
s
Like this?
Copy code
private fun forceSync(
    oldState: PhotoState,
    photoSource: PhotoSource
): PhotoState {

    launch(Dispatchers.Main) {

photoSyncService.sync(photoSource) { syncInfo ->

            launch(Dispatchers.Main) {

                this@PhotoStore.dispatch(PhotoAction.SyncInfoUpdate(photoSource, syncInfo))
            }
        }
    }

    return oldState
}
m
Nope, as long as you have
this@PhotoStore
called from the lambda, it will be freezed I believe
s
I don't understand how I can update my
PhotoState
in my
PhotoStore
without taking the result in my lambda and calling
dispatch
πŸ˜•
During a sync the lambda will be called multiple times with a progress and I want to show that in my UI.
m
This is where
suspend
is useful
suspend
is like a lambda but it will not capture your
this@PhotoStore
So that ultimately, you'll have something like this:
Copy code
val syncInfo = photoSyncService.sync(photoSource)
this@PhotoStore.dispatch(PhotoAction.SyncInfoUpdate(photoSource, syncInfo))
Of course that means
forceSync
is now suspend too
If you don't want that, you'll have to go through fire & forget
GlobalScope
stuff
s
But then I only get one SyncInfo as a result from the call to
sync()
m
Ah sorry, I missed that you needed several
In that case, the primitive to use is
Flow<PhotoState>
Copy code
fun sync(photoSource: PhotoSource): Flow<PhotoState>
s
And the flow won't capture like the lambda?
m
And then:
Copy code
photoSyncService.sync(photoSource).collect {
   this@PhotoStore.dispatch(PhotoAction.SyncInfoUpdate(photoSource, it))
}
And the flow won't capture like the lambda?
I hope it won't πŸ˜…
But TBH I'm really not sure
If it's all the same dispatcher there's no need to freeze but I don't know how coroutines do that internally
s
Yes, it freezes because
sync()
switches to
Default
dispatcher to offload it from
Main
.
FlowCollector is not thread-safe and concurrent emissions are prohibited.
is the error now. It says I can try to use channelFlow
πŸ‘€ 1
πŸ‘ 1
Yes,
channelFlow
actually works. πŸŽ‰ I don't understand why, but it prevents the capturing of my PhotoStore. Thank you, @mbonnin πŸ™ Currently it's not an ideal solution because the Flow must be captured to go on while a lambda just gives a feedback (like
onClick()
for a
Button
) , but maybe I can change other stuff to make it more fitting.
m
Nice πŸŽ‰
Yep, this stuff is complex
βž• 1
So depending when you want to ship your product, you might go directly with the new memory model
πŸ‘ 1
I think it's supposed to go beta in Q1 2022