How to use webview in jetpack compose for desktop-App
Asked Answered
E

2

14

I am writing a desktop application using desktop compose. But unable to find any suggestion on how to use web-view like in android we are supposed to use.

For desktop-app, we can not use android web-view any help and suggestion will be highly appreciated.

Effeminate answered 13/6, 2021 at 13:57 Comment(1)
github.com/JetBrains/compose-jb/issues/668 check for thisGusman
B
17

I have implemented a JavaFX WebView in Compose Desktop (additional features of a WebView here - http://tutorials.jenkov.com/javafx/webview.html). My actual usecase was for mapping purposes as there is not really any decent mapping Java libraries. However this example just loads a web url, but you can easily load a custom html page from resources that loads custom content.

Setup as follows (note java fx plugin can only be used in actual desktop module not shared as it clashes with android library plugin).

desktop module build.gradle file :

import org.jetbrains.compose.compose
import org.jetbrains.compose.desktop.application.dsl.TargetFormat

plugins {
    kotlin("multiplatform")
    id("org.openjfx.javafxplugin") version "0.0.10"
    id("org.jetbrains.compose") version "1.0.1"
}

group = "com.example.webview"
version = "1.0.0"

kotlin {
    jvm {
        compilations.all {
            kotlinOptions {
                jvmTarget = "11"
            }
            withJava()
        }
    }

    sourceSets {
        val jvmMain by getting {
            dependencies {
                implementation(project(":shared"))
                implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0-native-mt")
                { version { strictly("1.6.0-native-mt") } }
                implementation(compose.desktop.currentOs)
                //Optional other deps
                implementation(compose.uiTooling)
                implementation(compose.runtime)
                implementation(compose.foundation)
                implementation(compose.material)
                implementation(compose.animation)
                implementation(compose.animationGraphics)
            }
        }
        val jvmTest by getting {
            dependencies {
                // testing deps
            }
        }
    }
}

compose.desktop {
    application {
        mainClass = "Mainkt"
        nativeDistributions {
            targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
            packageName = "your.package.name"
            packageVersion = "1.0.0"
        }
    }
}

javafx {
    version = "16"
    modules = listOf("javafx.controls", "javafx.swing", "javafx.web", "javafx.graphics")
}

Main kotlin file :

fun main() = application(exitProcessOnExit = true) {
    // Required to make sure the JavaFx event loop doesn't finish (can happen when java fx panels in app are shown/hidden)
    val finishListener = object : PlatformImpl.FinishListener {
        override fun idle(implicitExit: Boolean) {}
        override fun exitCalled() {}
    }
    PlatformImpl.addListener(finishListener)

    Window(
        title = "WebView Test",
        resizable = false,
        state = WindowState(
            placement = Floating,
            size = DpSize(minWidth.dp, minHeight.dp)
        ),
        onCloseRequest = {
            PlatformImpl.removeListener(finishListener)
            exitApplication()
        },
        content = {
            val jfxPanel = remember { JFXPanel() }
            var jsObject = remember<JSObject?> { null }

            Box(modifier = Modifier.fillMaxSize().background(Color.White)) {

                ComposeJFXPanel(
                    composeWindow = window,
                    jfxPanel = jfxPanel,
                    onCreate = {
                        Platform.runLater {
                            val root = WebView()
                            val engine = root.engine
                            val scene = Scene(root)
                            engine.loadWorker.stateProperty().addListener { _, _, newState ->
                                if (newState === Worker.State.SUCCEEDED) {
                                    jsObject = root.engine.executeScript("window") as JSObject
                                    // execute other javascript / setup js callbacks fields etc..
                                }
                            }
                            engine.loadWorker.exceptionProperty().addListener { _, _, newError ->
                                println("page load error : $newError")
                            }
                            jfxPanel.scene = scene
                            engine.load("http://google.com") // can be a html document from resources ..
                            engine.setOnError { error -> println("onError : $error") }
                        }
                    }, onDestroy = {
                        Platform.runLater {
                            jsObject?.let { jsObj ->
                                // clean up code for more complex implementations i.e. removing javascript callbacks etc..
                            }
                        }
                    })
            }
        })
}

@Composable
fun ComposeJFXPanel(
    composeWindow: ComposeWindow,
    jfxPanel: JFXPanel,
    onCreate: () -> Unit,
    onDestroy: () -> Unit = {}
) {
    val jPanel = remember { JPanel() }
    val density = LocalDensity.current.density

    Layout(
        content = {},
        modifier = Modifier.onGloballyPositioned { childCoordinates ->
            val coordinates = childCoordinates.parentCoordinates!!
            val location = coordinates.localToWindow(Offset.Zero).round()
            val size = coordinates.size
            jPanel.setBounds(
                (location.x / density).toInt(),
                (location.y / density).toInt(),
                (size.width / density).toInt(),
                (size.height / density).toInt()
            )
            jPanel.validate()
            jPanel.repaint()
        },
        measurePolicy = { _, _ -> layout(0, 0) {} })

    DisposableEffect(jPanel) {
        composeWindow.add(jPanel)
        jPanel.layout = BorderLayout(0, 0)
        jPanel.add(jfxPanel)
        onCreate()
        onDispose {
            onDestroy()
            composeWindow.remove(jPanel)
        }
    }
}

Result :

enter image description here

Thanks goes to code in this original post which gives the bulk implementation of JFXPanel - https://github.com/JetBrains/compose-jb/issues/519#issuecomment-804030550 and subsequent comment by myself around the Java FX event loop fix.

Broadus answered 20/1, 2022 at 0:20 Comment(1)
I tried this out and the performance is not great, plus some images are not loading. Do you know a way around that?Armure
L
1

Try looking at this source BrowserView.kt It is based on Java Chromium Embedded Framework (JCEF)

Luganda answered 10/6, 2023 at 14:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.