Skip to content

🐛 Run JVM file picker off the main thread#593

Open
kdroidFilter wants to merge 1 commit into
vinceglb:mainfrom
kdroidFilter:fix/jvm-file-picker-off-main-thread
Open

🐛 Run JVM file picker off the main thread#593
kdroidFilter wants to merge 1 commit into
vinceglb:mainfrom
kdroidFilter:fix/jvm-file-picker-off-main-thread

Conversation

@kdroidFilter
Copy link
Copy Markdown
Contributor

Problem

On JVM, platformOpenFilePicker calls PlatformFilePicker.current.openFilePicker(...) / openFilesPicker(...) directly on the caller's dispatcher, unlike openDirectoryPicker and openFileSaver, which already wrap their blocking work in withContext(Dispatchers.IO).

rememberFilePickerLauncher launches its coroutine on the composition scope, whose dispatcher is Dispatchers.Main. On Compose Multiplatform desktop, Dispatchers.Main is the UI/event-loop thread, so the picker's blocking native work runs on it before the first suspension point.

On Linux, the picker path (LinuxFilePickerXdgFilePickerPortal) performs synchronous D-Bus work — building the session-bus connection, the isAvailable() probe, and the OpenFile round-trip — all before it suspends on the response. Running this on the UI thread freezes it.

  • AWT-based backends (the default application { Window { } }) tolerate this because the xdg-desktop-portal handshake is driven by a separate AWT/X11 toolkit thread.
  • No-AWT backends (e.g. a Tao/winit window whose Dispatchers.Main is the single GTK event-loop thread) freeze — and can deadlock: the portal needs the app's event loop to complete the dialog parenting/focus handshake, but that loop is blocked waiting on the synchronous OpenFile call.

The directory picker and file saver are unaffected precisely because they already hop to Dispatchers.IO.

Fix

Wrap the platformOpenFilePicker body in withContext(Dispatchers.IO), matching openDirectoryPicker and openFileSaver. This keeps the blocking native/D-Bus work off the UI thread; the result is delivered back through the normal suspension, so callers that observe the picker state on the main thread are unchanged.

-): Flow<FileKitPickerState<List<PlatformFile>>> {
+): Flow<FileKitPickerState<List<PlatformFile>>> = withContext(Dispatchers.IO) {
     // Filter by extension
     ...
-    return files.toPickerStateFlow()
+    files.toPickerStateFlow()
 }

No API change. One file touched: filekit-dialogs/src/jvmMain/.../FileKit.jvm.kt.

platformOpenFilePicker (JVM) ran PlatformFilePicker.openFilePicker /
openFilesPicker directly on the caller's dispatcher, unlike
openDirectoryPicker and openFileSaver which already wrap their blocking
work in withContext(Dispatchers.IO).

When a file picker is launched from a Compose Multiplatform desktop app
whose Dispatchers.Main is the UI/event-loop thread, this blocks that
thread: on Linux the picker's synchronous D-Bus work (xdg-desktop-portal
connection build + OpenFile round-trip) freezes — and can deadlock — the
UI before the first suspension point. AWT-only apps tolerate it because
the portal handshake is driven by a separate toolkit thread, but no-AWT
backends (e.g. Tao/winit on GTK) freeze.

Wrap the picker body in withContext(Dispatchers.IO), matching the
directory picker and file saver.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant