2024.1+Coroutine Tips and Tricks
Edit pageLast modified: 21 February 2025tip
Kotlin Coroutines×IntelliJ PlatformThis section focuses on explaining coroutines in the specific context of the IntelliJ Platform. If you are not experienced with Kotlin Coroutines, it is highly recommended to get familiar with Learning Resources first.
This section presents techniques to use coroutines efficiently and avoid common pitfalls.
Switching Between the Background Threads and EDT
Avoid the invokeLater
-style often found in Java:
launch(Dispatchers.EDT) {
val uiData = collectUiData()
// switch to Default:
launch(Dispatchers.Default) {
val result = compute(uiData)
// switch to EDT again:
launch(Dispatchers.EDT) {
applyUiData(result)
}
}
}
The recommended approach:
launch(Dispatchers.EDT) {
val uiData = collectUiData()
// switch to Default:
val result = withContext(Dispatchers.Default) {
compute(uiData)
}
// this will be resumed on EDT automatically:
applyUiData(result)
}
Dispatching to the End of a Queue
In some cases, it is required to exit the current EDT event and continue after all events in the queue are processed. In a non-coroutine context, it could be implemented like in the following snippet:
invokeLater {
step1()
invokeLater {
step2()
invokeLater {
step3()
}
}
}
In a coroutine context, use the following approach:
withContext(Dispatchers.EDT) {
step1()
yield() // suspends here, dispatches the following block again on EDT
step2()
yield()
step3()
}
This approach works with any sequential dispatcher, e.g., created with Dispatchers.Default.limitedParallelism(1)
.
The same applies to runBlockingCancellable
:
runBlockingCancellable {
println(1)
launch {
print(2)
yield()
print(3)
}
print(4)
yield()
print(5)
yield()
print(6)
}
// Output: 142536
Scheduling Tasks With a Fixed Delay
There is no scheduleWithFixedDelay()
in coroutines, because it can be easily implemented with the following snippet:
val job = coroutineScope.launch {
delay(initialDelayMs)
while (true) {
action() // can be suspending as well
delay(delayMs)
}
}
When the job is no longer needed, remember to cancel the launched coroutine:
job.cancel()
or the whole scope:
coroutineScope.cancel()
Limiting Dispatcher Parallelism
Each call of limitedParallelism()
creates a new independent dispatcher instance, effectively not limiting the parallelism:
suspend fun doSomething() {
withContext(Dispatchers.Default.limitedParallelism(3)) {
// ...
}
}
Instead, store the dispatcher instance into a static property and use it as a context:
private val myDispatcher = Dispatchers.Default.limitedParallelism(3)
suspend fun doSomething() {
withContext(myDispatcher) {
// ...
}
}
warning
Do not use
limitedParallelism(1)
for code synchronization. UseMutex
instead.
Changing Modality State
Avoid changing modality state in the middle of a running coroutine:
cs.launch {
// ...
withContext(Dispatchers.EDT + ModalityState.any().asContextElement()) {
// ...
}
}
Add the modality state to the context when launching a coroutine:
cs.launch(ModalityState.current().asContextElement()) {
// ...
withContext(Dispatchers.EDT) {
// ...
}
}
It is possible that the coroutine is launched as a response to a user event from EDT, where ModalityState.current()
is available.
If the coroutine is launched from a background thread, then it should not be invoked on top of an unrelated dialog anyway. The absence of the context modality state is effectively equivalent to specifying ModalityState.nonModal()
.
Thanks for your feedback!