{"id":46693,"date":"2024-10-20T09:33:15","date_gmt":"2024-10-20T00:33:15","guid":{"rendered":"https:\/\/charlezz.com\/?p=46693"},"modified":"2024-10-20T09:39:41","modified_gmt":"2024-10-20T00:39:41","slug":"kmp-%eb%8d%b0%ec%8a%a4%ed%81%ac%ed%83%91jvm%ec%9a%a9-%ec%9b%b9%eb%b7%b0-%ea%b5%ac%ed%98%84%ed%95%98%ea%b8%b0","status":"publish","type":"post","link":"https:\/\/charlezz.com\/?p=46693","title":{"rendered":"[KMP] \ub370\uc2a4\ud06c\ud0d1(jvm)\uc6a9 \uc6f9\ubdf0 \uad6c\ud604\ud558\uae30"},"content":{"rendered":"\n<p>Compose Multiplatform \uc5d0\uc11c\ub294 \ud50c\ub7ab\ud3fc\ubcc4 \uacf5\ud1b5\uc73c\ub85c \uc0ac\uc6a9\uac00\ub2a5\ud55c WebView\ub97c \uc81c\uacf5\ud558\uace0 \uc788\uc9c0 \uc54a\uc73c\ubbc0\ub85c, expect\/actual \ud0a4\uc6cc\ub4dc\ub97c \ud65c\uc6a9\ud558\uc5ec \ud50c\ub7ab\ud3fc\ubcc4\ub85c \uc6f9\ubdf0\ub97c \uad6c\ud604\ud574\uc57c \ud55c\ub2e4.<\/p>\n\n\n\n<p>\ucc98\uc74c\uc5d0\ub294 <a href=\"https:\/\/github.com\/KevinnZou\/compose-webview\">https:\/\/github.com\/KevinnZou\/compose-webview<\/a> \ub77c\uc774\ube0c\ub7ec\ub9ac\ub97c \ud65c\uc6a9\ud558\uc5ec \uad6c\ud604\ud558\uace0\uc790 \ud588\uc73c\ub098, \uac1c\ubc1c\ud658\uacbd \ubc84\uc804\uacfc \uc77c\uce58\ud558\uc9c0 \uc54a\uc544 \ubb38\uc81c\uac00 \uc0dd\uacbc\ub2e4.<\/p>\n\n\n\n<p> \uc774 \ud3ec\uc2a4\ud305\uc5d0\uc11c\ub294 \uc9c1\uc811 JavaFX\ub97c \ud65c\uc6a9\ud558\uc5ec desktop(jvm)\uc744 \uc704\ud55c \uc6f9\ubdf0 \uad6c\ud604\ubc29\ubc95\uc744 \uc18c\uac1c\ud558\uace0\uc790 \ud55c\ub2e4.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">JavaFX\ub780?<\/h2>\n\n\n\n<blockquote class=\"wp-block-quote\">\n<p>&#8220;JavaFX is an open source, next generation client application platform for desktop, mobile and embedded systems built on Java. It is a collaborative effort by many individuals and companies with the goal of producing a modern, efficient, and fully featured toolkit for developing rich client applications.&#8221;<\/p>\n<cite>\uacf5\uc2dd\ubb38\uc11c \ub0b4\uc6a9 \ubc1c\ucdcc<\/cite><\/blockquote>\n\n\n\n<p>JavaFX\ub294 Java\ub97c \uae30\ubc18\uc73c\ub85c \ud558\ub294 \ub370\uc2a4\ud06c\ud1b1, \ubaa8\ubc14\uc77c \ubc0f \uc784\ubca0\ub514\ub4dc \uc2dc\uc2a4\ud15c\uc744 \uc704\ud55c \uc624\ud508 \uc18c\uc2a4 \ucc28\uc138\ub300 \ud074\ub77c\uc774\uc5b8\ud2b8 \uc560\ud50c\ub9ac\ucf00\uc774\uc158 \ud50c\ub7ab\ud3fc\uc774\ub2e4. \ub9ce\uc740 \uac1c\uc778\uacfc \ud68c\uc0ac\uc758 \ub178\ub825\uc73c\ub85c \ub2e4\ucc44\ub85c\uc6b4 \ud074\ub77c\uc774\uc5b8\ud2b8 \uc560\ud50c\ub9ac\ucf00\uc774\uc158\uc744 \uac1c\ubc1c\ud558\uae30 \uc704\ud55c \ud604\ub300\uc801\uc774\uace0 \ud6a8\uc728\uc801\uc774\uba70 \ubaa8\ub4e0 \uae30\ub2a5\uc744 \uac16\ucd98 \ud234\ud0b7\uc744 \uc81c\uc791\ud55c\ub2e4\ub294 \ubaa9\ud45c\ub97c \uac00\uc9c0\uace0 \uc788\ub2e4.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">JavaFX \uc758\uc874\uc131 \ucd94\uac00\ud558\uae30<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>implementation(\"org.openjfx:javafx-base:$jdk:$platform\")\nimplementation(\"org.openjfx:javafx-graphics:$jdk:$platform\")\nimplementation(\"org.openjfx:javafx-web:$jdk:$platform\")\nimplementation(\"org.openjfx:javafx-swing:$jdk:$platform\")\nimplementation(\"org.openjfx:javafx-controls:$jdk:$platform\")\nimplementation(\"org.openjfx:javafx-media:$jdk:$platform\")<\/code><\/pre>\n\n\n\n<p>\uc758\uc874\uc131 \ucd94\uac00\ud558\ub294 \ubc29\ubc95\uc774 \ub2e4\uc18c \uae4c\ub2e4\ub86d\ub2e4.  \uac1c\ubc1c\ud658\uacbd\uc758 jdk \ubc84\uc804\uacfc \ubaa9\ud45c\ub85c \ud558\uace0 \uc788\ub294 \ud50c\ub7ab\ud3fc\ubcc4\ub85c \uc758\uc874\uc131\uc744 \ub2ec\ub9ac \ud558\uae30 \ub54c\ubb38\uc5d0 \ube4c\ub4dc \uc2dc\uc5d0 \uc774\ub97c \uc720\uc758\ud558\uc5ec \uc81c\uacf5\ud574\uc57c\ud55c\ub2e4. \ud50c\ub7ec\uadf8\uc778\uc744 \ud1b5\ud574 \uad00\ub9ac\ud558\ub294 \ubc29\ubc95\ub3c4 \uc788\ub294 \ub4ef \ud558\uc9c0\ub9cc \uc2dc\ub3c4\ud574\ubcf4\uc9c4 \uc54a\uc558\ub2e4.<\/p>\n\n\n\n<p>\ub9cc\uc57d JDK 19\ub97c \uc0ac\uc6a9\ud558\ub294 \uc560\ud50c\ub9ac\uc2e4\ub9ac\ucf58 macOS \ud658\uacbd\uc774\ub77c\uba74 \ub2e4\uc74c\uacfc \uac19\uc774 \ucd94\uac00 \ud558\uba74 \ub41c\ub2e4.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>implementation(\"org.openjfx:javafx-base:19:mac-aarch64\")\nimplementation(\"org.openjfx:javafx-graphics:19:mac-aarch64\")\nimplementation(\"org.openjfx:javafx-web:19:mac-aarch64\")\nimplementation(\"org.openjfx:javafx-swing:19:mac-aarch64\")\nimplementation(\"org.openjfx:javafx-controls:19:mac-aarch64\")\nimplementation(\"org.openjfx:javafx-media:19:mac-aarch64\")<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Compose\ub85c WebView \uad6c\ud604\ud558\uae30<\/h2>\n\n\n\n<p>\uc774\uc81c javafx.scene.web.WebView \ucc38\uc870\uac00 \uac00\ub2a5\ud558\ubbc0\ub85c, https:\/\/github.com\/KevinnZou\/compose-webview \uc758 \ucf54\ub4dc\ub97c \ubc1c\ucdcc\ud558\uc5ec \uc57d\uac04\uc758 \uc218\uc815\uc744 \uac70\uccd0 Compose\ub85c WebView\ub97c \ub2e4\uc74c\uacfc \uac19\uc774 \uad6c\ud604\ud588\ub2e4. <\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>@Composable\nfun JavaFxWebView(\n    state: WebViewState,\n    modifier: Modifier = Modifier,\n    navigator: WebViewNavigator = rememberWebViewNavigator(),\n    onCreated: () -&gt; Unit = {},\n    onDispose: () -&gt; Unit = {},\n    engineChangeListener: ChangeListener&lt;Worker.State&gt;? = null\n) {\n    val jfxPanel = remember { JFXPanel() }\n\n    LaunchedEffect(Unit) {\n        Platform.runLater {\n            val wv = WebView().apply {\n                applySettings(state.settings)\n                engineChangeListener?.run { engine.loadWorker.stateProperty().addListener(this) }\n            }\n            jfxPanel.scene = Scene(wv)\n            state.webView = wv\n            onCreated()\n        }\n    }\n\n    DisposableEffect(Unit) {\n        onDispose {\n            onDispose()\n        }\n    }\n\n    val webView = state.webView\n\n    webView?.let { wv -&gt;\n        LaunchedEffect(navigator, wv) {\n            navigator.handleNavigationEvents(wv)\n        }\n\n        LaunchedEffect(state, wv) {\n            snapshotFlow { state.content }.collect { content -&gt;\n                when (content) {\n                    is WebContent.Url -&gt; {\n                        Platform.runLater {\n                            wv.engine.load(content.url)\n                        }\n                    }\n\n                    is WebContent.Data -&gt; {\n                        Platform.runLater {\n                            if (content.mimeType.isNullOrBlank())\n                                wv.engine.loadContent(content.data)\n                            else\n                                wv.engine.loadContent(content.data, content.mimeType)\n                        }\n                    }\n\n                    is WebContent.NavigatorOnly -&gt; {\n                        \/\/ NO-OP\n                    }\n\n                    else -&gt; {}\n                }\n            }\n        }\n\n        SwingPanel(\n            factory = {\n                jfxPanel\n            },\n            update = {\n\n            },\n            modifier = modifier\n        )\n    }\n}\n\n\npublic sealed class WebContent {\n    public data class Url(\n        val url: String,\n        val additionalHttpHeaders: Map&lt;String, String&gt; = emptyMap(),\n    ) : WebContent()\n\n    public data class Data(\n        val data: String,\n        val baseUrl: String? = null,\n        val encoding: String = \"utf-8\",\n        val mimeType: String? = null,\n        val historyUrl: String? = null\n    ) : WebContent()\n\n    public data object NavigatorOnly : WebContent()\n}\n\ninternal fun WebContent.withUrl(url: String) = when (this) {\n    is WebContent.Url -&gt; copy(url = url)\n    else -&gt; WebContent.Url(url)\n}\n\npublic sealed class LoadingState {\n\n    public object Initializing : LoadingState()\n\n    public data class Loading(val progress: Float) : LoadingState()\n\n    public object Finished : LoadingState()\n}\n\n@Stable\nclass WebViewState constructor(webContent: WebContent) {\n    var lastLoadedUrl: String? by mutableStateOf(null)\n        internal set\n\n    var content: WebContent by mutableStateOf(webContent)\n\n    var loadingState: LoadingState by mutableStateOf(LoadingState.Initializing)\n        internal set\n\n    val isLoading: Boolean\n        get() = loadingState !is LoadingState.Finished\n\n    var pageTitle: String? by mutableStateOf(null)\n        internal set\n\n    val settings: JavaFxWebSettings = JavaFxWebSettings(\n        onSettingsChanged = {\n            applySetting()\n        }\n    )\n\n    private fun applySetting() {\n        webView?.applySettings(settings)\n    }\n\n    fun evaluateJavascript(script: String, callback: ((String?) -&gt; Unit)?) {\n        Platform.runLater {\n            webView?.engine?.executeScript(script).let {\n                callback?.invoke(it?.toString())\n            }\n        }\n    }\n\n    var webView by mutableStateOf&lt;WebView?&gt;(null)\n        internal set\n}\n\n@Stable\npublic class WebViewNavigator(private val coroutineScope: CoroutineScope) {\n    internal sealed interface NavigationEvent {\n        data object Back : NavigationEvent\n        data object Forward : NavigationEvent\n        data object Reload : NavigationEvent\n        data object StopLoading : NavigationEvent\n\n        data class LoadUrl(\n            val url: String,\n            val additionalHttpHeaders: Map&lt;String, String&gt; = emptyMap()\n        ) : NavigationEvent\n\n        data class LoadHtml(\n            val html: String,\n            val baseUrl: String? = null,\n            val mimeType: String? = null,\n            val encoding: String? = \"utf-8\",\n            val historyUrl: String? = null\n        ) : NavigationEvent\n    }\n\n    internal val navigationEvents: MutableSharedFlow&lt;NavigationEvent&gt; = MutableSharedFlow(replay = 1)\n\n    public var canGoBack: Boolean by mutableStateOf(false)\n        internal set\n\n    public var canGoForward: Boolean by mutableStateOf(false)\n        internal set\n\n    public fun loadUrl(url: String, additionalHttpHeaders: Map&lt;String, String&gt; = emptyMap()) {\n        coroutineScope.launch {\n            navigationEvents.emit(\n                NavigationEvent.LoadUrl(\n                    url,\n                    additionalHttpHeaders\n                )\n            )\n        }\n    }\n\n    public fun loadHtml(\n        html: String,\n        baseUrl: String? = null,\n        mimeType: String? = null,\n        encoding: String? = \"utf-8\",\n        historyUrl: String? = null\n    ) {\n        coroutineScope.launch {\n            navigationEvents.emit(\n                NavigationEvent.LoadHtml(\n                    html,\n                    baseUrl,\n                    mimeType,\n                    encoding,\n                    historyUrl\n                )\n            )\n        }\n    }\n\n    public fun navigateBack() {\n        coroutineScope.launch { navigationEvents.emit(NavigationEvent.Back) }\n    }\n\n    public fun navigateForward() {\n        coroutineScope.launch { navigationEvents.emit(NavigationEvent.Forward) }\n    }\n\n    public fun reload() {\n        coroutineScope.launch { navigationEvents.emit(NavigationEvent.Reload) }\n    }\n\n    public fun stopLoading() {\n        coroutineScope.launch { navigationEvents.emit(NavigationEvent.StopLoading) }\n    }\n}\n\n@Composable\npublic fun rememberWebViewNavigator(\n    coroutineScope: CoroutineScope = rememberCoroutineScope()\n): WebViewNavigator = remember(coroutineScope) { WebViewNavigator(coroutineScope) }\n\n@Composable\npublic fun rememberWebViewState(\n    url: String,\n    additionalHttpHeaders: Map&lt;String, String&gt; = emptyMap()\n): WebViewState =\n    remember {\n        WebViewState(\n            WebContent.Url(\n                url = url,\n                additionalHttpHeaders = additionalHttpHeaders\n            )\n        )\n    }.apply {\n        this.content = WebContent.Url(\n            url = url,\n            additionalHttpHeaders = additionalHttpHeaders\n        )\n    }\n\n@Composable\npublic fun rememberWebViewStateWithHTMLData(\n    data: String,\n    baseUrl: String? = null,\n    encoding: String = \"utf-8\",\n    mimeType: String? = null,\n    historyUrl: String? = null\n): WebViewState =\n    remember {\n        WebViewState(WebContent.Data(data, baseUrl, encoding, mimeType, historyUrl))\n    }.apply {\n        this.content = WebContent.Data(\n            data, baseUrl, encoding, mimeType, historyUrl\n        )\n    }\n\n@Composable\npublic fun rememberSaveableWebViewState(): WebViewState =\n    rememberSaveable(saver = WebStateSaver) {\n        WebViewState(WebContent.NavigatorOnly)\n    }\n\nval WebStateSaver: Saver&lt;WebViewState, Any&gt; = run {\n    val pageTitleKey = \"pagetitle\"\n    val lastLoadedUrlKey = \"lastloaded\"\n\n    mapSaver(\n        save = {\n            mapOf(\n                pageTitleKey to it.pageTitle,\n                lastLoadedUrlKey to it.lastLoadedUrl,\n            )\n        },\n        restore = {\n            WebViewState(WebContent.NavigatorOnly).apply {\n                this.pageTitle = it&#91;pageTitleKey] as String?\n                this.lastLoadedUrl = it&#91;lastLoadedUrlKey] as String?\n            }\n        }\n    )\n}\n\nprivate fun WebView.applySettings(javaFxWebSettings: JavaFxWebSettings) {\n    engine.isJavaScriptEnabled = javaFxWebSettings.javaScriptEnabled\n}\n\ninternal suspend fun WebViewNavigator.handleNavigationEvents(\n    webView: WebView\n): Nothing = withContext(Dispatchers.Main) {\n    navigationEvents.collect { event -&gt;\n        with(webView.engine) {\n            when (event) {\n                is WebViewNavigator.NavigationEvent.Back -&gt;\n                    if (history.currentIndex &gt; 0) history.go(-1)\n\n                is WebViewNavigator.NavigationEvent.Forward -&gt;\n                    if (history.currentIndex &lt; history.maxSize - 1) history.go(1)\n\n                is WebViewNavigator.NavigationEvent.Reload -&gt;\n                    reload()\n\n                is WebViewNavigator.NavigationEvent.StopLoading -&gt;\n                    stopLoading()\n\n                is WebViewNavigator.NavigationEvent.LoadHtml -&gt;\n                    loadContent(event.html, event.mimeType)\n\n                is WebViewNavigator.NavigationEvent.LoadUrl -&gt;\n                    loadUrl(event.url, event.additionalHttpHeaders)\n            }\n        }\n    }\n}<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">\uc790\ubc14\uc2a4\ud06c\ub9bd\ud2b8 \ucf5c\ubc31 \uc218\uc2e0\ud558\uae30<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>JavaFxWebView(\n        engineChangeListener = { observableState, oldState, newState -&gt;\n            if (newState == Worker.State.SUCCEEDED) {\n                val window = state.webView?.engine?.executeScript(\"window\") as JSObject\n                window.setMember(\"${\uc790\ubc14\uc2a4\ud06c\ub9bd\ud2b8\uc778\ud130\ud398\uc774\uc2a4\uba85}\", object : JavascriptInterface {\n                    override fun completeLogin(token: String) {\n                        println(\"\ucf5c\ubc31 \uc218\uc2e0, token = $token\")\n                    }\n                })\n            }\n        }\n    )\n\ninterface JavascriptInterface {\n    \/\/ \ucf5c\ubc31\ubc1b\uc744 \uba54\uc11c\ub4dc \uba85\n    fun completeLogin(token:String)\n}<\/code><\/pre>\n","protected":false},"excerpt":{"rendered":"<p>Compose Multiplatform \uc5d0\uc11c\ub294 \ud50c\ub7ab\ud3fc\ubcc4 \uacf5\ud1b5\uc73c\ub85c \uc0ac\uc6a9\uac00\ub2a5\ud55c WebView\ub97c \uc81c\uacf5\ud558\uace0 \uc788\uc9c0 \uc54a\uc73c\ubbc0\ub85c, expect\/actual \ud0a4\uc6cc\ub4dc\ub97c \ud65c\uc6a9\ud558\uc5ec \ud50c\ub7ab\ud3fc\ubcc4\ub85c \uc6f9\ubdf0\ub97c \uad6c\ud604\ud574\uc57c \ud55c\ub2e4. \ucc98\uc74c\uc5d0\ub294 https:\/\/github.com\/KevinnZou\/compose-webview \ub77c\uc774\ube0c\ub7ec\ub9ac\ub97c \ud65c\uc6a9\ud558\uc5ec \uad6c\ud604\ud558\uace0\uc790 \ud588\uc73c\ub098, \uac1c\ubc1c\ud658\uacbd \ubc84\uc804\uacfc \uc77c\uce58\ud558\uc9c0 \uc54a\uc544 \ubb38\uc81c\uac00 \uc0dd\uacbc\ub2e4. \uc774 \ud3ec\uc2a4\ud305\uc5d0\uc11c\ub294 \uc9c1\uc811 JavaFX\ub97c \ud65c\uc6a9\ud558\uc5ec desktop(jvm)\uc744 \uc704\ud55c \uc6f9\ubdf0 \uad6c\ud604\ubc29\ubc95\uc744 \uc18c\uac1c\ud558\uace0\uc790 \ud55c\ub2e4. JavaFX\ub780? &#8220;JavaFX is an open source, next generation client application [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"om_disable_all_campaigns":false,"inline_featured_image":false,"_monsterinsights_skip_tracking":false,"_monsterinsights_sitenote_active":false,"_monsterinsights_sitenote_note":"","_monsterinsights_sitenote_category":0},"categories":[5,42],"tags":[],"aioseo_notices":[],"_links":{"self":[{"href":"https:\/\/charlezz.com\/index.php?rest_route=\/wp\/v2\/posts\/46693"}],"collection":[{"href":"https:\/\/charlezz.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/charlezz.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/charlezz.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/charlezz.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=46693"}],"version-history":[{"count":2,"href":"https:\/\/charlezz.com\/index.php?rest_route=\/wp\/v2\/posts\/46693\/revisions"}],"predecessor-version":[{"id":46695,"href":"https:\/\/charlezz.com\/index.php?rest_route=\/wp\/v2\/posts\/46693\/revisions\/46695"}],"wp:attachment":[{"href":"https:\/\/charlezz.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=46693"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/charlezz.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=46693"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/charlezz.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=46693"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}