
Cross-platform WebView component exposing WebViewState and navigator APIs; backed by native OS webviews via Rust+UniFFI (Wry), with JS-native bridge, cookie API, request interceptor.
ComposeNativeWebView is a Compose Multiplatform WebView whose API design and mobile implementations (Android & iOS) are intentionally derived almost verbatim from KevinnZou/compose-webview-multiplatform.
Package namespace:
dev.nucleusframework.webview.*
⚠️ Breaking change (v1.0.0+) — migrating from pre-Nucleus / Wry builds
Classpath / package — rename imports and package references:
io.github.kdroidfilter.webview.*→dev.nucleusframework.webview.*- Maven coordinates:
io.github.kdroidfilter:…→dev.nucleusframework:composewebviewDesktop is Tao-only — the old Wry desktop backend is removed. Desktop WebView requires the Nucleus Tao backend (
NativeView):
- App entry:
nucleusApplication(backend = NucleusBackend.Tao) { … }- Dependencies: Nucleus application +
decorated-window-tao(Swing/Compose Desktop without Tao will not host the WebView)Why Tao? Native WebViews are opaque platform surfaces. Tao’s
NativeViewembeds them in the same window stack as Compose, so you can draw Compose UI on top of the WebView (toolbars, dialogs, loading overlays, chrome) instead of fighting a separate HWND/GTK child. You also get the rest of Tao’s desktop stack (decorated window, title bar, input routing, multiplatform windowing) in one path.Android, iOS and WasmJs keep the same API shape; only the package and Maven group change.
Reused on purpose
WebViewState, WebViewNavigator, settings, callbacks, mental model)android.webkit.WebView)WKWebView)If you already know compose-webview-multiplatform, you already know how to use this.
What ComposeNativeWebView adds
dev.nucleusframework)android.webkit.WebView
WKWebView
org.w3c.dom.HTMLIFrameElement
NativeView (requires nucleusApplication / Tao backend).
libcompose_webview_linux.so)compose_webview_windows.dll; needs WebView2 Runtime / Edge)libcompose_webview_macos.dylib)The desktop backend embeds a real native view — it does not render the page offscreen into a bitmap and blit it into the Compose scene:
WKWebView NSView is a subview of the Tao window, below the Compose
Metal layer; Compose punches a transparent hole over the WebView rect.Consequences:
max_fps knob in this library — none of
the backends contain frame-rate logic. The page paints at whatever rate the platform
compositor gives it, which is normally the display refresh rate../gradlew :e2e-desktop:run reports two rendering measurements (they publish numbers,
they do not enforce thresholds):
Passed R01 Rendering requestAnimationFrame rate 90 fps
Passed R02 Rendering WebGL renderer Apple GPU
A healthy R01 is the refresh rate of the display the window is on, and R02 should
name a GPU (a software renderer there is the usual reason WebGL content is slow).
R01 is Skipped when the document reports visibilityState = "hidden": every engine
suspends requestAnimationFrame for a window that is fully covered or backgrounded, so
the sample would read 0 fps and say nothing about the backend. A bare WKWebView in a
plain NSWindow behaves exactly the same — keep the window in front while measuring.
Reference measurement (macOS, M4, 90 Hz display, Nucleus Tao 2.5.5, rAF + WebGL page)
— embedded WebView vs. the same page in a bare WKWebView in a plain NSWindow:
| Workload | Embedded (Tao NativeView) |
Bare WKWebView
|
|---|---|---|
| Canvas 2D animation | 90 fps | 90 fps |
| WebGL, GPU-bound shader | 31–34 fps | 32–35 fps |
Blending an overlay on top does not change that. Same page, full-screen window
(2560×1040), with an animated Compose overlay in the content slot — page fps /
Compose fps, plus GPU utilization sampled while both run at the display rate:
| Compose overlay | Light page | GPU-bound page | GPU util (light page) |
|---|---|---|---|
| none | 90 / 90 | 34 / 90 | 23.4 % |
| 64 dp animated bar | 90 / 90 | 34 / 90 | — |
| full-window translucent scrim | 90 / 90 | 34 / 90 | 22.9 % |
| full-window opaque surface | 90 / 90 | 34 / 90 | — |
bare WKWebView, opaque window |
90 | 34 | 27.6 % |
Note that an opaque Compose overlay does not stop the WebView underneath: it keeps rendering at full speed behind it, so hide or dispose it instead of covering it if you want the GPU work back.
When reporting a frame-rate problem, include the R01/R02 values, the display refresh
rate, and whether Compose content overlaps the WebView.
@Composable
fun App() {
val state = rememberWebViewState("https://example.com")
WebView(state, Modifier.fillMaxSize()) {
// Optional Compose overlay on top of the native WebView
// (NativeView content slot on desktop; Box overlay elsewhere).
}
}That’s it.
dependencies {
implementation("dev.nucleusframework:composewebview:<version>")
}Same artifact for Android, iOS, Desktop and WasmJs.
VisualSuiteApp + suiteCatalog() live in e2e-shared commonMain.
Every platform host runs that same suite against a real WebView:
| Host | Command | Backend |
|---|---|---|
| Desktop | ./gradlew :e2e-desktop:run |
Tao + WebKit2GTK / WKWebView / WebView2 |
| Android |
./gradlew :e2e-android:installDebug then launch app |
android.webkit.WebView |
| Wasm | ./gradlew :e2e-wasmJs:wasmJsBrowserDevelopmentRun |
IFrame |
| iOS | open iosApp in Xcode and Run |
WKWebView |
Cases that need a platform-only capability (history on Wasm, isolated native profiles on desktop, pixel screenshots, …) are Skipped with a reason — not Failed — so the catalog stays identical.
Same pure-logic packages on JVM / Android host / iOS simulator / Wasm browser:
COMMON='--tests dev.nucleusframework.webview.jsbridge.* --tests dev.nucleusframework.webview.web.* --tests dev.nucleusframework.webview.request.* --tests dev.nucleusframework.webview.cookie.* --tests dev.nucleusframework.webview.setting.*'
./gradlew :webview-compose:jvmTest $COMMON
./gradlew :webview-compose:testDebugUnitTest $COMMON
./gradlew :webview-compose:iosSimulatorArm64Test $COMMON # macOS
./gradlew :webview-compose:wasmJsBrowserTest $COMMONloadUrl(url, headers)loadHtml(html)loadHtmlFile(fileName, readType)navigateBack(), navigateForward()
reload(), stopLoading()
canGoBack, canGoForward
isLoadingloadingStatelastLoadedUrlpageTitleUnified cookie API:
state.cookieManager.setCookie(...)
state.cookieManager.getCookies(url)
state.cookieManager.removeCookies(url)
state.cookieManager.removeAllCookies()navigator.evaluateJavaScript("document.title = 'Hello'")WebViewJsBridge(jsBridgeName = "myBridge")
window.kmpJsBridge.callNative("echo", {...}, callback)Intercept navigator-initiated navigations only:
override fun onInterceptUrlRequest(
request: WebRequest,
navigator: WebViewNavigator
): WebRequestInterceptResultUseful for:
val state = rememberWebViewState(
url = "https://example.com"
) {
customUserAgentString = "MyApp/1.0"
}Supports:
val navigator = rememberWebViewNavigator()
WebView(state, navigator)Commands:
loadUrlloadHtmlloadHtmlFileevaluateJavaScriptstate.webSettings.customUserAgentString = "MyApp/1.2.3"state.webSettings.logSeverity = KLogSeverity.Debugwebview-compose/ → Compose Multiplatform API + platform actuals + commonTeste2e-shared/ → shared multiplatform visual e2e suite (VisualSuiteApp)e2e-desktop/, e2e-android/, e2e-wasmJs/, iosApp/ → platform hosts for that suitenucleusApplication + decorated-window-tao). Linux (WebKit2GTK), macOS (WKWebView) and Windows (WebView2) are fully wired.ComposeNativeWebView is a Compose Multiplatform WebView whose API design and mobile implementations (Android & iOS) are intentionally derived almost verbatim from KevinnZou/compose-webview-multiplatform.
Package namespace:
dev.nucleusframework.webview.*
⚠️ Breaking change (v1.0.0+) — migrating from pre-Nucleus / Wry builds
Classpath / package — rename imports and package references:
io.github.kdroidfilter.webview.*→dev.nucleusframework.webview.*- Maven coordinates:
io.github.kdroidfilter:…→dev.nucleusframework:composewebviewDesktop is Tao-only — the old Wry desktop backend is removed. Desktop WebView requires the Nucleus Tao backend (
NativeView):
- App entry:
nucleusApplication(backend = NucleusBackend.Tao) { … }- Dependencies: Nucleus application +
decorated-window-tao(Swing/Compose Desktop without Tao will not host the WebView)Why Tao? Native WebViews are opaque platform surfaces. Tao’s
NativeViewembeds them in the same window stack as Compose, so you can draw Compose UI on top of the WebView (toolbars, dialogs, loading overlays, chrome) instead of fighting a separate HWND/GTK child. You also get the rest of Tao’s desktop stack (decorated window, title bar, input routing, multiplatform windowing) in one path.Android, iOS and WasmJs keep the same API shape; only the package and Maven group change.
Reused on purpose
WebViewState, WebViewNavigator, settings, callbacks, mental model)android.webkit.WebView)WKWebView)If you already know compose-webview-multiplatform, you already know how to use this.
What ComposeNativeWebView adds
dev.nucleusframework)android.webkit.WebView
WKWebView
org.w3c.dom.HTMLIFrameElement
NativeView (requires nucleusApplication / Tao backend).
libcompose_webview_linux.so)compose_webview_windows.dll; needs WebView2 Runtime / Edge)libcompose_webview_macos.dylib)The desktop backend embeds a real native view — it does not render the page offscreen into a bitmap and blit it into the Compose scene:
WKWebView NSView is a subview of the Tao window, below the Compose
Metal layer; Compose punches a transparent hole over the WebView rect.Consequences:
max_fps knob in this library — none of
the backends contain frame-rate logic. The page paints at whatever rate the platform
compositor gives it, which is normally the display refresh rate../gradlew :e2e-desktop:run reports two rendering measurements (they publish numbers,
they do not enforce thresholds):
Passed R01 Rendering requestAnimationFrame rate 90 fps
Passed R02 Rendering WebGL renderer Apple GPU
A healthy R01 is the refresh rate of the display the window is on, and R02 should
name a GPU (a software renderer there is the usual reason WebGL content is slow).
R01 is Skipped when the document reports visibilityState = "hidden": every engine
suspends requestAnimationFrame for a window that is fully covered or backgrounded, so
the sample would read 0 fps and say nothing about the backend. A bare WKWebView in a
plain NSWindow behaves exactly the same — keep the window in front while measuring.
Reference measurement (macOS, M4, 90 Hz display, Nucleus Tao 2.5.5, rAF + WebGL page)
— embedded WebView vs. the same page in a bare WKWebView in a plain NSWindow:
| Workload | Embedded (Tao NativeView) |
Bare WKWebView
|
|---|---|---|
| Canvas 2D animation | 90 fps | 90 fps |
| WebGL, GPU-bound shader | 31–34 fps | 32–35 fps |
Blending an overlay on top does not change that. Same page, full-screen window
(2560×1040), with an animated Compose overlay in the content slot — page fps /
Compose fps, plus GPU utilization sampled while both run at the display rate:
| Compose overlay | Light page | GPU-bound page | GPU util (light page) |
|---|---|---|---|
| none | 90 / 90 | 34 / 90 | 23.4 % |
| 64 dp animated bar | 90 / 90 | 34 / 90 | — |
| full-window translucent scrim | 90 / 90 | 34 / 90 | 22.9 % |
| full-window opaque surface | 90 / 90 | 34 / 90 | — |
bare WKWebView, opaque window |
90 | 34 | 27.6 % |
Note that an opaque Compose overlay does not stop the WebView underneath: it keeps rendering at full speed behind it, so hide or dispose it instead of covering it if you want the GPU work back.
When reporting a frame-rate problem, include the R01/R02 values, the display refresh
rate, and whether Compose content overlaps the WebView.
@Composable
fun App() {
val state = rememberWebViewState("https://example.com")
WebView(state, Modifier.fillMaxSize()) {
// Optional Compose overlay on top of the native WebView
// (NativeView content slot on desktop; Box overlay elsewhere).
}
}That’s it.
dependencies {
implementation("dev.nucleusframework:composewebview:<version>")
}Same artifact for Android, iOS, Desktop and WasmJs.
VisualSuiteApp + suiteCatalog() live in e2e-shared commonMain.
Every platform host runs that same suite against a real WebView:
| Host | Command | Backend |
|---|---|---|
| Desktop | ./gradlew :e2e-desktop:run |
Tao + WebKit2GTK / WKWebView / WebView2 |
| Android |
./gradlew :e2e-android:installDebug then launch app |
android.webkit.WebView |
| Wasm | ./gradlew :e2e-wasmJs:wasmJsBrowserDevelopmentRun |
IFrame |
| iOS | open iosApp in Xcode and Run |
WKWebView |
Cases that need a platform-only capability (history on Wasm, isolated native profiles on desktop, pixel screenshots, …) are Skipped with a reason — not Failed — so the catalog stays identical.
Same pure-logic packages on JVM / Android host / iOS simulator / Wasm browser:
COMMON='--tests dev.nucleusframework.webview.jsbridge.* --tests dev.nucleusframework.webview.web.* --tests dev.nucleusframework.webview.request.* --tests dev.nucleusframework.webview.cookie.* --tests dev.nucleusframework.webview.setting.*'
./gradlew :webview-compose:jvmTest $COMMON
./gradlew :webview-compose:testDebugUnitTest $COMMON
./gradlew :webview-compose:iosSimulatorArm64Test $COMMON # macOS
./gradlew :webview-compose:wasmJsBrowserTest $COMMONloadUrl(url, headers)loadHtml(html)loadHtmlFile(fileName, readType)navigateBack(), navigateForward()
reload(), stopLoading()
canGoBack, canGoForward
isLoadingloadingStatelastLoadedUrlpageTitleUnified cookie API:
state.cookieManager.setCookie(...)
state.cookieManager.getCookies(url)
state.cookieManager.removeCookies(url)
state.cookieManager.removeAllCookies()navigator.evaluateJavaScript("document.title = 'Hello'")WebViewJsBridge(jsBridgeName = "myBridge")
window.kmpJsBridge.callNative("echo", {...}, callback)Intercept navigator-initiated navigations only:
override fun onInterceptUrlRequest(
request: WebRequest,
navigator: WebViewNavigator
): WebRequestInterceptResultUseful for:
val state = rememberWebViewState(
url = "https://example.com"
) {
customUserAgentString = "MyApp/1.0"
}Supports:
val navigator = rememberWebViewNavigator()
WebView(state, navigator)Commands:
loadUrlloadHtmlloadHtmlFileevaluateJavaScriptstate.webSettings.customUserAgentString = "MyApp/1.2.3"state.webSettings.logSeverity = KLogSeverity.Debugwebview-compose/ → Compose Multiplatform API + platform actuals + commonTeste2e-shared/ → shared multiplatform visual e2e suite (VisualSuiteApp)e2e-desktop/, e2e-android/, e2e-wasmJs/, iosApp/ → platform hosts for that suitenucleusApplication + decorated-window-tao). Linux (WebKit2GTK), macOS (WKWebView) and Windows (WebView2) are fully wired.