Skip to content

Commit 96600d3

Browse files
committed
Add WebViewState for state management and enhance WebView navigation controls
- Introduce `WebViewState` and associated utilities for managing WebView state, including URL, loading status, and navigation history. - Implement back, forward, reload, and load URL functionality. - Enhance `App` composable with navigation controls and progress indicators.
1 parent 328e723 commit 96600d3

5 files changed

Lines changed: 354 additions & 7 deletions

File tree

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,39 @@
11
package io.github.kdroidfilter.composewebview
22

3+
import androidx.compose.foundation.layout.Column
4+
import androidx.compose.foundation.layout.Row
35
import androidx.compose.foundation.layout.fillMaxSize
6+
import androidx.compose.foundation.layout.fillMaxWidth
7+
import androidx.compose.material3.Button
8+
import androidx.compose.material3.CircularProgressIndicator
49
import androidx.compose.material3.MaterialTheme
10+
import androidx.compose.material3.Text
511
import androidx.compose.runtime.Composable
612
import androidx.compose.ui.Modifier
713
import androidx.compose.ui.tooling.preview.Preview
814

915
@Composable
10-
@Preview
1116
fun App() {
12-
MaterialTheme {
13-
WryWebView(
14-
url = "https://jetbrains.com/",
15-
modifier = Modifier.fillMaxSize(),
16-
)
17+
val webViewState = rememberWebViewState("https://google.com")
1718

19+
Column {
20+
Text("URL: ${webViewState.currentUrl}")
21+
22+
if (webViewState.isLoading) {
23+
CircularProgressIndicator()
24+
}
25+
26+
Row(modifier = Modifier.fillMaxWidth()) {
27+
Button(onClick = { webViewState.goBack() }) { Text("Back") }
28+
Button(onClick = { webViewState.goForward() }) { Text("Forward") }
29+
Button(onClick = { webViewState.reload() }) { Text("Reload") }
30+
Button(onClick = { webViewState.loadUrl("https://github.com") }) {
31+
Text("Go to GitHub")
32+
}
33+
}
34+
35+
36+
37+
WryWebView(state = webViewState, modifier = Modifier.fillMaxSize())
1838
}
19-
}
39+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package io.github.kdroidfilter.composewebview
2+
3+
import androidx.compose.runtime.*
4+
import io.github.kdroidfilter.composewebview.wry.WryWebViewPanel
5+
6+
/**
7+
* State holder for WebView navigation and loading state.
8+
*
9+
* @param initialUrl The initial URL to load
10+
*/
11+
@Stable
12+
class WebViewState(initialUrl: String) {
13+
internal var panel: WryWebViewPanel? = null
14+
private var lastRequestedUrl: String = initialUrl
15+
private var hasLoadedOnce: Boolean = false
16+
17+
/**
18+
* The target URL to navigate to. Changing this will trigger navigation.
19+
*/
20+
var url: String by mutableStateOf(initialUrl)
21+
22+
/**
23+
* The current URL displayed in the WebView.
24+
* Updated after navigation completes.
25+
*/
26+
var currentUrl: String by mutableStateOf("")
27+
internal set
28+
29+
/**
30+
* Whether the WebView is currently loading content.
31+
*/
32+
var isLoading: Boolean by mutableStateOf(true)
33+
internal set
34+
35+
/**
36+
* Whether the WebView can navigate back in history.
37+
*/
38+
var canGoBack: Boolean by mutableStateOf(false)
39+
internal set
40+
41+
/**
42+
* Whether the WebView can navigate forward in history.
43+
*/
44+
var canGoForward: Boolean by mutableStateOf(false)
45+
internal set
46+
47+
/**
48+
* Navigate back in the browsing history.
49+
*/
50+
fun goBack() {
51+
isLoading = true
52+
panel?.goBack()
53+
}
54+
55+
/**
56+
* Navigate forward in the browsing history.
57+
*/
58+
fun goForward() {
59+
isLoading = true
60+
panel?.goForward()
61+
}
62+
63+
/**
64+
* Reload the current page.
65+
*/
66+
fun reload() {
67+
isLoading = true
68+
panel?.reload()
69+
}
70+
71+
/**
72+
* Load a new URL.
73+
*/
74+
fun loadUrl(newUrl: String) {
75+
url = newUrl
76+
lastRequestedUrl = newUrl
77+
isLoading = true
78+
panel?.loadUrl(newUrl)
79+
}
80+
81+
/**
82+
* Refresh the state by querying the WebView.
83+
*/
84+
internal fun refreshState() {
85+
panel?.let { p ->
86+
if (p.isReady()) {
87+
// Update current URL
88+
p.getCurrentUrl()?.let { newUrl ->
89+
if (newUrl.isNotEmpty() && newUrl != "about:blank") {
90+
val urlChanged = newUrl != currentUrl
91+
currentUrl = newUrl
92+
93+
// If URL changed or we got a valid URL for the first time, we're done loading
94+
if (urlChanged || !hasLoadedOnce) {
95+
hasLoadedOnce = true
96+
isLoading = false
97+
}
98+
}
99+
}
100+
101+
// Also check native loading state
102+
if (isLoading && hasLoadedOnce) {
103+
val nativeLoading = p.isLoading()
104+
if (!nativeLoading) {
105+
isLoading = false
106+
}
107+
}
108+
}
109+
}
110+
}
111+
}
112+
113+
/**
114+
* Creates and remembers a [WebViewState] instance.
115+
*
116+
* @param initialUrl The initial URL to load in the WebView
117+
* @return A remembered [WebViewState] instance
118+
*/
119+
@Composable
120+
fun rememberWebViewState(initialUrl: String = "about:blank"): WebViewState {
121+
return remember(initialUrl) { WebViewState(initialUrl) }
122+
}

wrywebview-compose/src/jvmMain/kotlin/io/github/kdroidfilter/composewebview/WryWebView.kt

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
11
package io.github.kdroidfilter.composewebview
22

33
import androidx.compose.runtime.Composable
4+
import androidx.compose.runtime.DisposableEffect
5+
import androidx.compose.runtime.LaunchedEffect
46
import androidx.compose.ui.Modifier
57
import androidx.compose.ui.awt.SwingPanel
68
import io.github.kdroidfilter.composewebview.wry.WryWebViewPanel
9+
import kotlinx.coroutines.delay
710

11+
/**
12+
* A composable that displays a WebView with the given URL.
13+
*
14+
* @param url The URL to load
15+
* @param modifier The modifier to apply to this layout
16+
*/
817
@Composable
918
fun WryWebView(url: String, modifier: Modifier = Modifier) {
1019
SwingPanel(
@@ -13,3 +22,42 @@ fun WryWebView(url: String, modifier: Modifier = Modifier) {
1322
update = { it.loadUrl(url) },
1423
)
1524
}
25+
26+
/**
27+
* A composable that displays a WebView controlled by a [WebViewState].
28+
*
29+
* @param state The [WebViewState] that controls this WebView
30+
* @param modifier The modifier to apply to this layout
31+
*/
32+
@Composable
33+
fun WryWebView(state: WebViewState, modifier: Modifier = Modifier) {
34+
// Periodically refresh the state to update currentUrl and isLoading
35+
LaunchedEffect(state) {
36+
while (true) {
37+
delay(250) // Poll frequently for better responsiveness
38+
state.refreshState()
39+
}
40+
}
41+
42+
DisposableEffect(Unit) {
43+
onDispose {
44+
state.panel = null
45+
}
46+
}
47+
48+
SwingPanel(
49+
modifier = modifier,
50+
factory = {
51+
WryWebViewPanel(state.url).also { panel ->
52+
state.panel = panel
53+
}
54+
},
55+
update = { panel ->
56+
if (state.panel != panel) {
57+
state.panel = panel
58+
}
59+
// Load new URL if it changed
60+
panel.loadUrl(state.url)
61+
},
62+
)
63+
}

wrywebview/src/main/kotlin/io/github/kdroidfilter/composewebview/wry/WryWebViewPanel.kt

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,60 @@ class WryWebViewPanel(initialUrl: String) : JPanel() {
6161
log("loadUrl url=$url webviewId=$webviewId")
6262
}
6363

64+
fun goBack() {
65+
val action = { webviewId?.let { NativeBindings.goBack(it) } }
66+
if (SwingUtilities.isEventDispatchThread()) {
67+
action()
68+
} else {
69+
SwingUtilities.invokeLater { action() }
70+
}
71+
log("goBack webviewId=$webviewId")
72+
}
73+
74+
fun goForward() {
75+
val action = { webviewId?.let { NativeBindings.goForward(it) } }
76+
if (SwingUtilities.isEventDispatchThread()) {
77+
action()
78+
} else {
79+
SwingUtilities.invokeLater { action() }
80+
}
81+
log("goForward webviewId=$webviewId")
82+
}
83+
84+
fun reload() {
85+
val action = { webviewId?.let { NativeBindings.reload(it) } }
86+
if (SwingUtilities.isEventDispatchThread()) {
87+
action()
88+
} else {
89+
SwingUtilities.invokeLater { action() }
90+
}
91+
log("reload webviewId=$webviewId")
92+
}
93+
94+
fun getCurrentUrl(): String? {
95+
return webviewId?.let {
96+
try {
97+
NativeBindings.getUrl(it)
98+
} catch (e: Exception) {
99+
log("getCurrentUrl failed: ${e.message}")
100+
null
101+
}
102+
}
103+
}
104+
105+
fun isLoading(): Boolean {
106+
return webviewId?.let {
107+
try {
108+
NativeBindings.isLoading(it)
109+
} catch (e: Exception) {
110+
log("isLoading failed: ${e.message}")
111+
true
112+
}
113+
} ?: true
114+
}
115+
116+
fun isReady(): Boolean = webviewId != null
117+
64118
private fun createIfNeeded(): Boolean {
65119
if (webviewId != null) return true
66120
if (!host.isDisplayable || !host.isShowing) return false
@@ -306,6 +360,26 @@ private object NativeBindings {
306360
io.github.kdroidfilter.composewebview.wry.loadUrl(id, url)
307361
}
308362

363+
fun goBack(id: ULong) {
364+
io.github.kdroidfilter.composewebview.wry.goBack(id)
365+
}
366+
367+
fun goForward(id: ULong) {
368+
io.github.kdroidfilter.composewebview.wry.goForward(id)
369+
}
370+
371+
fun reload(id: ULong) {
372+
io.github.kdroidfilter.composewebview.wry.reload(id)
373+
}
374+
375+
fun getUrl(id: ULong): String {
376+
return io.github.kdroidfilter.composewebview.wry.getUrl(id)
377+
}
378+
379+
fun isLoading(id: ULong): Boolean {
380+
return io.github.kdroidfilter.composewebview.wry.isLoading(id)
381+
}
382+
309383
fun destroyWebview(id: ULong) {
310384
io.github.kdroidfilter.composewebview.wry.destroyWebview(id)
311385
}

0 commit comments

Comments
 (0)