feat(share): Kotlin Multiplatform native share sheet - #727
Draft
kdroidFilter wants to merge 2 commits into
Draft
kdroidFilter wants to merge 2 commits into
kdroidFilter wants to merge 2 commits into
Conversation
Android and iOS in pure Kotlin, desktop through a Rust JNI bridge (Windows DataTransferManager, macOS NSSharingServicePicker, Linux XDG portal), inspired by robius-share. nucleus-application gains NucleusWindow.share, which resolves the parent window (and the Wayland export's lifetime) from the Tao window.
js and wasmJs share webMain, which reaches navigator.share through js() bodies. File items must be URIs the page can fetch (blob:, data:, same-origin https:); local paths fail with UnsupportedItem. Adds examples/share-web-demo.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
New
sharemodule (dev.nucleusframework:nucleus.share): the system share sheet for Kotlin Multiplatform, inspired byrobius-share. It is the repo's first KMP library module. Rust is used only on desktop: Android, iOS and the web are pure Kotlin.nucleus-applicationgainsNucleusWindow.share, which attaches the share UI to the window, the same waywithFileKitDialogSettingsattaches FileKit dialogs.ACTION_SEND/ACTION_SEND_MULTIPLEin a chooser)UIActivityViewControllerNSSharingServicePickerDataTransferManagerdesktop-window interop)xdg-openfallbackjs,wasmJs)navigator.share)webMain)Documentation
Installation
Sharing
scope.launch { try { ShareSheet.share { title = "Share" // chooser / Share UI title where the platform has one subject = "Nucleus" // e-mail subject (Android), description (Windows) text("Cross-platform native share sheet") url("https://github.com/NucleusFramework/Nucleus") file("/path/to/report.pdf", mimeType = "application/pdf") fileUri("content://com.example/report.txt") // Android content://, file:// on native, blob:/data:/https: on the web } } catch (e: ShareException) { println("${e.error}: ${e.message}") } }ShareSheet.share(request)issuspendand safe from any dispatcher: each platform switches to its UI thread itself.robius-share).ShareRequestcan also be built directly:ShareRequest(items = listOf(ShareItem.Text("…")), title = …, subject = …)orshareRequest { … }.ShareSheet.isSupported: on desktop, whether the native bridge loaded.Errors
ShareException.erroris aShareError:EmptyInvalidItemNoHandlerAlreadyOpenInvalidStateError)UnsupportedItemcontent://on desktop, a local path on the web, filesnavigator.canSharerefuses)NoWindowUnsupportedIoPlatformValidation runs in common code before anything reaches the platform.
From a Nucleus window (desktop)
NucleusWindow.share(request, anchor)/NucleusWindow.share(anchor) { … }resolve the parent from the Tao window:NSWindow. The picker points atanchor, or at the top centre of the window without one.shareAnchor(density)converts a node's window bounds to points.x11:<xid>.xdg_foreignexport, unexported once the portal dialog is gone (see below).nucleus.shareis acompileOnlydependency ofnucleus-application, so it is never forced on consumers. An app that callswindow.sharealready has it, since it builds aShareRequest.Naming the parent yourself
ShareParent.Linux.keepAliveis closed exactly once:sharewhen no dialog was shown (xdg-openfallback);The portal requires a Wayland export to stay alive until its dialog closes, while
sharereturns as soon as it is shown. So the lease is handed to the crate as aRunnable(Completion, run onDrop, so every path releases it), and the Linux portal thread runs it when the portal answers.Platform notes
cacheDir/nucleus-share/<id>/<name>and served read-only by the library's ownNucleusShareProvider. Its authority is${applicationId}.nucleus.share, merged from the library manifest, with no AndroidX dependency.content://URIs are forwarded as-is, with a read grant throughClipData.minSdk21. Unlikerobius-share, nothing is written to the MediaStore Downloads collection.titleandsubjectare ignored. On iPad the popover is centred on the presenting view controller, without an arrow. Presenting while a share sheet is up raisesAlreadyOpen.titleandsubjectare ignored.NSApplication, nothing drains the main queue, and the call now fails instead of hanging.StorageItems. Only paths andfile://URIs are accepted.DataRequestedhandlers of a window are removed before each share.OpenURI/OpenFile);FileChooser.SaveFiles);xdg-openis the fallback when no portal answers.xprop).jsandwasmJs, onewebMainsource reaching the browser throughjs()bodies):localhost) and a user gesture: callsharefrom a click handler, without awaiting anything slow first. Called outside a gesture, the browser rejects withNotAllowedError→ShareError.Platform.ShareData:title←subject ?: title(receivers use it as a subject; there is no chooser title on the web),url← the first URL,text← the texts and the other URLs, in order.file(path)fails withUnsupportedItem. SharefileUri("blob:…" / "data:…" / same-origin "https:…", mimeType)instead: each URI isfetched into aFile, named after the URL's last segment orshared-<n>.<ext>, and the data is checked withnavigator.canSharefirst. A failed fetch →ShareError.Io.AbortError) is no error. Browsers withoutnavigator.share(Firefox desktop) reportisSupported = falseand throwUnsupported.Implementation
commonMain: API, validation, text joining, Android MIME-type rule.androidMain,iosMain,webMain(shared byjsandwasmJs): pure Kotlin. Only strings and lambdas cross the JS / Wasm boundary (file URIs and MIME types are newline-joined), so one source compiles for both.jvmMain:NativeShareBridgeplusShareParent/ShareAnchor.share/src/main/native, with its libraries inshare/src/main/resources, added tojvmMain. This is the JVM JNI-module layout, so the CI globs (*/src/main/..., cache key) stay uniform.nucleus.native-module:org.jetbrains.kotlin.multiplatformis applied, it wiresjvmProcessResources(the target must be namedjvm) and feeds it the native-libraries manifest.sourcesJardependency now also matchesjvmSourcesJar.nucleus_share,jni 0.21):windows 0.62.objc2 0.6/objc2-app-kit 0.3/dispatch2 0.3.robius-share's dependency-free D-Bus client (libc only).nativeSharecall returning an error code (ShareErrorordinal + 1) and a message out-param. It needs noFindClassexceptjava.lang.Runnable.runfor the completion, which is declared inreachability-metadata.json.detekttask isNO-SOURCEin a multiplatform module, so the KDoc rules were silently not enforced. The root build now wires the per-source-setdetekt*MainSourceSettasks (not thedetektBaseline*ones) intocheck.androidKotlinMultiplatformLibraryplugin (AGP 9.1.1),coroutines-android,coroutines-test.base, which providesclean. Kotlin/JS applies the lifecycle plugin to the root, which forbids registering our ownclean.kotlin-js-store/yarn.lockis committed (Node tests + the web demo).sharebuild steps (Windows / macOS / Linux) with thecache-hitguard.Verify ... nativeslists.pre-merge.yaml/publish-maven.yamlEXPECTED arrays.THIRD_PARTY_NOTICES.md§5 (robius-share, MIT) pluslicenses/LICENSE-MIT-robius.txt. The notices ship in the desktop JAR'sMETA-INF/.examples/share-web-demo(Kotlin/JS,./gradlew :examples:share-web-demo:jsBrowserDevelopmentRun): text, link, a generatedblob:file and a local path (unsupported).examples/share-demo(./gradlew :examples:share-demo:run). Text, link, file, mixed and invalid payloads throughnucleusWindow.share.-Dshare.demo.auto=<button>presses one at startup for smoke tests.Test plan
cargo checkforx86_64-pc-windows-msvc,aarch64-apple-darwin,x86_64-unknown-linux-gnu:share:check: common tests (builder, validation, text joining, MIME rule, Web Share mapping) on jvm, js (Node) and wasmJs (Node), + JVM lease test, detekt per source set, ktlint,apiCheckjvm,androidand iOS klibs compile (Kotlin 2.4 builds iOS klibs on a Windows host):nucleus-application:check,apiDumpcommittedshare-demo(text, link, file, mixed; clicked and auto-triggered)content://URIshare-web-demobundle loads in Chrome,navigator.sharedetected (headless check)blob:file from a click in Chrome, Edge, Safari; wasmJs in an appNativeShareBridge+Runnablecompletion