Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use webview in jetpack compose for desktop-App

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.

like image 682
Vinay Shankar Gupta Avatar asked Nov 14 '22 20:11

Vinay Shankar Gupta


1 Answers

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.

like image 145
Mark Avatar answered Dec 04 '22 14:12

Mark