SafeLaunch.kt
42 lines · 1.9 KB
Changed in: 0.5.0
| 1 | package dev.stave.ui |
| 2 | |
| 3 | import androidx.lifecycle.ViewModel |
| 4 | import androidx.lifecycle.viewModelScope |
| 5 | import kotlinx.coroutines.CoroutineExceptionHandler |
| 6 | import kotlinx.coroutines.CoroutineScope |
| 7 | import kotlinx.coroutines.Job |
| 8 | import kotlinx.coroutines.launch |
| 9 | |
| 10 | /** |
| 11 | * Coroutine exception handler shared by every ViewModel.launch site. |
| 12 | * |
| 13 | * Without this, uncaught exceptions from viewModelScope.launch bubble up to the JVM |
| 14 | * default handler, which Compose Desktop / Swing surfaces as an opaque "Error" |
| 15 | * dialog showing only the exception message — e.g. `index: 57, size: 57` after a |
| 16 | * right-click → Play Next, with no stack trace and no way to recover gracefully. |
| 17 | * |
| 18 | * Logging here means the same exception becomes a stderr stack trace |
| 19 | * (recoverable from `./gradlew :desktop:run` output or a shell-launched packaged |
| 20 | * install) and the user keeps playing music. The app remains responsive. |
| 21 | * |
| 22 | * Note: This swallows the exception by *handling* it — viewModelScope's |
| 23 | * SupervisorJob doesn't cancel siblings, so other launches continue normally. |
| 24 | */ |
| 25 | private val viewModelExceptionHandler = CoroutineExceptionHandler { _, throwable -> |
| 26 | System.err.println( |
| 27 | "[Earhorn] ViewModel coroutine threw (${throwable.javaClass.simpleName}):" |
| 28 | ) |
| 29 | throwable.printStackTrace(System.err) |
| 30 | } |
| 31 | |
| 32 | /** |
| 33 | * Like viewModelScope.launch, but installs [viewModelExceptionHandler] so an |
| 34 | * unexpected throw becomes a logged stack trace rather than an error dialog. |
| 35 | * |
| 36 | * Use for any coroutine triggered by a user action (right-click handlers, |
| 37 | * button clicks, menu items, drag callbacks, etc). Long-running collection |
| 38 | * jobs (state flows, lifecycle observers) can also use this; the handler |
| 39 | * only fires on actually-uncaught exceptions. |
| 40 | */ |
| 41 | fun ViewModel.safeLaunch(block: suspend CoroutineScope.() -> Unit): Job = |
| 42 | viewModelScope.launch(viewModelExceptionHandler, block = block) |


