Skip to content

Commit 561880f

Browse files
committed
Add logging control and improve WebView lifecycle management
- Introduce `LOG_ENABLED` and `log_enabled()` for optional debug logging. - Enhance WebView creation with threading and fallback mechanisms to prevent redundant attempts. - Add destroy timer and conditional checks for smoother cleanup. - Refactor composable WebView integration with `movableContentOf` for better state management.
1 parent 6bc0221 commit 561880f

3 files changed

Lines changed: 134 additions & 47 deletions

File tree

  • demo-shared/src/commonMain/kotlin/io/github/kdroidfilter/webview/demo
  • wrywebview/src/main

demo-shared/src/commonMain/kotlin/io/github/kdroidfilter/webview/demo/App.kt

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@ import androidx.compose.material3.VerticalDivider
1616
import androidx.compose.material3.darkColorScheme
1717
import androidx.compose.runtime.Composable
1818
import androidx.compose.runtime.DisposableEffect
19+
import androidx.compose.runtime.ExperimentalComposeApi
1920
import androidx.compose.runtime.LaunchedEffect
2021
import androidx.compose.runtime.getValue
22+
import androidx.compose.runtime.movableContentOf
2123
import androidx.compose.runtime.mutableStateListOf
2224
import androidx.compose.runtime.mutableStateOf
2325
import androidx.compose.runtime.remember
@@ -35,7 +37,7 @@ import io.github.kdroidfilter.webview.web.rememberWebViewState
3537
import kotlinx.coroutines.launch
3638

3739
@Composable
38-
@OptIn(ExperimentalMaterial3Api::class)
40+
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeApi::class)
3941
fun App() {
4042
BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
4143
val isCompact = maxWidth < 900.dp
@@ -83,6 +85,17 @@ fun App() {
8385
backgroundColor = androidx.compose.ui.graphics.Color.White
8486
}
8587
val jsBridge = rememberWebViewJsBridge(navigator)
88+
val webViewContent =
89+
remember(webViewState, navigator, jsBridge) {
90+
movableContentOf<Modifier> { webViewModifier ->
91+
WebView(
92+
state = webViewState,
93+
navigator = navigator,
94+
webViewJsBridge = jsBridge,
95+
modifier = webViewModifier,
96+
)
97+
}
98+
}
8699

87100
var urlText by remember { mutableStateOf("https://httpbin.org/html") }
88101

@@ -192,12 +205,7 @@ fun App() {
192205

193206
if (isCompact) {
194207
Column(modifier = Modifier.fillMaxSize()) {
195-
WebView(
196-
state = webViewState,
197-
navigator = navigator,
198-
webViewJsBridge = jsBridge,
199-
modifier = Modifier.weight(1f, fill = true).fillMaxWidth(),
200-
)
208+
webViewContent(Modifier.weight(1f, fill = true).fillMaxWidth())
201209

202210
AnimatedVisibility(visible = toolsVisible) {
203211
DemoToolsPanel(
@@ -416,12 +424,7 @@ fun App() {
416424
VerticalDivider(modifier = Modifier.fillMaxHeight())
417425
}
418426

419-
WebView(
420-
state = webViewState,
421-
navigator = navigator,
422-
webViewJsBridge = jsBridge,
423-
modifier = Modifier.fillMaxSize(),
424-
)
427+
webViewContent(Modifier.fillMaxSize())
425428
}
426429
}
427430
}

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

Lines changed: 87 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import java.awt.event.MouseEvent
88
import javax.swing.JPanel
99
import javax.swing.SwingUtilities
1010
import javax.swing.Timer
11+
import kotlin.concurrent.thread
1112

1213

1314
class WryWebViewPanel(
@@ -24,6 +25,8 @@ class WryWebViewPanel(
2425
private var pendingHeaders: Map<String, String> = emptyMap()
2526
private var pendingHtml: String? = null
2627
private var createTimer: Timer? = null
28+
private var destroyTimer: Timer? = null
29+
private var createInFlight: Boolean = false
2730
private var gtkTimer: Timer? = null
2831
private var windowsTimer: Timer? = null
2932
private var skikoInitialized: Boolean = false
@@ -45,14 +48,15 @@ class WryWebViewPanel(
4548

4649
override fun addNotify() {
4750
super.addNotify()
51+
stopDestroyTimer()
4852
log("addNotify displayable=${host.isDisplayable} showing=${host.isShowing} size=${host.width}x${host.height}")
4953
SwingUtilities.invokeLater { scheduleCreateIfNeeded() }
5054
}
5155

5256
override fun removeNotify() {
5357
log("removeNotify")
5458
stopCreateTimer()
55-
destroyIfNeeded()
59+
scheduleDestroyIfNeeded()
5660
super.removeNotify()
5761
}
5862

@@ -282,6 +286,7 @@ class WryWebViewPanel(
282286

283287
private fun createIfNeeded(): Boolean {
284288
if (webviewId != null) return true
289+
if (createInFlight) return false
285290
if (!host.isDisplayable || !host.isShowing) return false
286291
if (host.width <= 0 || host.height <= 0) return false
287292
// On Windows, wait for the window to be fully visible
@@ -306,48 +311,69 @@ class WryWebViewPanel(
306311
parentHandle = resolved.handle
307312
parentIsWindow = resolved.isWindow
308313
log("createIfNeeded handle=$parentHandle parentIsWindow=$parentIsWindow size=${host.width}x${host.height}")
309-
return try {
310-
val width = host.width.coerceAtLeast(1)
311-
val height = host.height.coerceAtLeast(1)
312-
val userAgent = customUserAgent
313-
314-
webviewId =
314+
val width = host.width.coerceAtLeast(1)
315+
val height = host.height.coerceAtLeast(1)
316+
val userAgent = customUserAgent
317+
val initialUrl = pendingUrl
318+
val handleSnapshot = parentHandle
319+
createInFlight = true
320+
stopCreateTimer()
321+
thread(name = "wry-webview-create", isDaemon = true) {
322+
val createdId = try {
315323
if (userAgent == null) {
316-
NativeBindings.createWebview(parentHandle, width, height, pendingUrl)
324+
NativeBindings.createWebview(handleSnapshot, width, height, initialUrl)
317325
} else {
318-
NativeBindings.createWebviewWithUserAgent(parentHandle, width, height, pendingUrl, userAgent)
326+
NativeBindings.createWebviewWithUserAgent(handleSnapshot, width, height, initialUrl, userAgent)
327+
}
328+
} catch (e: RuntimeException) {
329+
System.err.println("Failed to create Wry webview: ${e.message}")
330+
e.printStackTrace()
331+
null
332+
}
333+
SwingUtilities.invokeLater {
334+
createInFlight = false
335+
if (createdId == null) {
336+
scheduleCreateIfNeeded()
337+
return@invokeLater
338+
}
339+
if (webviewId != null) {
340+
NativeBindings.destroyWebview(createdId)
341+
return@invokeLater
342+
}
343+
if (!host.isDisplayable || !host.isShowing) {
344+
NativeBindings.destroyWebview(createdId)
345+
return@invokeLater
319346
}
320-
updateBounds()
321-
startGtkPumpIfNeeded()
322-
startWindowsPumpIfNeeded()
323-
// Apply any pending content that requires an explicit call after creation.
324-
val id = webviewId
325-
val html = pendingHtml
326-
val urlWithHeaders = pendingUrlWithHeaders
327-
val headers = pendingHeaders
328-
if (id != null) {
347+
webviewId = createdId
348+
updateBounds()
349+
startGtkPumpIfNeeded()
350+
startWindowsPumpIfNeeded()
351+
// Apply any pending content that requires an explicit call after creation.
352+
val html = pendingHtml
353+
val urlWithHeaders = pendingUrlWithHeaders
354+
val headers = pendingHeaders
329355
when {
330356
html != null -> {
331357
pendingHtml = null
332-
NativeBindings.loadHtml(id, html)
358+
NativeBindings.loadHtml(createdId, html)
333359
}
334360
urlWithHeaders != null && headers.isNotEmpty() -> {
335361
pendingUrlWithHeaders = null
336362
pendingHeaders = emptyMap()
337-
NativeBindings.loadUrlWithHeaders(id, urlWithHeaders, headers)
363+
NativeBindings.loadUrlWithHeaders(createdId, urlWithHeaders, headers)
364+
}
365+
pendingUrl != initialUrl -> {
366+
NativeBindings.loadUrl(createdId, pendingUrl)
338367
}
339368
}
369+
log("createIfNeeded success id=$webviewId")
340370
}
341-
log("createIfNeeded success id=$webviewId")
342-
true
343-
} catch (e: RuntimeException) {
344-
System.err.println("Failed to create Wry webview: ${e.message}")
345-
e.printStackTrace()
346-
true
347371
}
372+
return true
348373
}
349374

350375
private fun destroyIfNeeded() {
376+
stopDestroyTimer()
351377
stopGtkPump()
352378
stopWindowsPump()
353379
stopBoundsTimer()
@@ -364,7 +390,7 @@ class WryWebViewPanel(
364390
private fun updateBounds() {
365391
val id = webviewId ?: return
366392
val bounds = boundsInParent()
367-
if (IS_LINUX) {
393+
if (IS_LINUX || IS_MAC) {
368394
pendingBounds = bounds
369395
if (boundsTimer == null) {
370396
boundsTimer = Timer(16) {
@@ -411,7 +437,7 @@ class WryWebViewPanel(
411437
}
412438

413439
private fun scheduleCreateIfNeeded() {
414-
if (webviewId != null || createTimer != null) return
440+
if (webviewId != null || createTimer != null || createInFlight) return
415441
log("scheduleCreateIfNeeded")
416442
val delay = if (IS_WINDOWS) 100 else 16
417443
createTimer = Timer(delay) {
@@ -426,6 +452,25 @@ class WryWebViewPanel(
426452
createTimer = null
427453
}
428454

455+
private fun scheduleDestroyIfNeeded() {
456+
if (destroyTimer != null) return
457+
if (webviewId == null && !createInFlight) return
458+
destroyTimer = Timer(400) {
459+
stopDestroyTimer()
460+
if (!host.isDisplayable || !host.isShowing) {
461+
destroyIfNeeded()
462+
}
463+
}.apply {
464+
isRepeats = false
465+
start()
466+
}
467+
}
468+
469+
private fun stopDestroyTimer() {
470+
destroyTimer?.stop()
471+
destroyTimer = null
472+
}
473+
429474
private fun stopBoundsTimer() {
430475
boundsTimer?.stop()
431476
boundsTimer = null
@@ -442,7 +487,9 @@ class WryWebViewPanel(
442487
}
443488

444489
private fun log(message: String) {
445-
System.err.println("[WryWebViewPanel] $message")
490+
if (LOG_ENABLED) {
491+
System.err.println("[WryWebViewPanel] $message")
492+
}
446493
}
447494

448495
private fun resolveParentHandle(): ParentHandle? {
@@ -531,6 +578,17 @@ class WryWebViewPanel(
531578
private val IS_LINUX = OS_NAME.contains("linux")
532579
private val IS_MAC = OS_NAME.contains("mac")
533580
private val IS_WINDOWS = OS_NAME.contains("windows")
581+
private val LOG_ENABLED = run {
582+
val raw = System.getProperty("composewebview.wry.log") ?: System.getenv("WRYWEBVIEW_LOG")
583+
when {
584+
raw == null -> false
585+
raw == "1" -> true
586+
raw.equals("true", ignoreCase = true) -> true
587+
raw.equals("yes", ignoreCase = true) -> true
588+
raw.equals("debug", ignoreCase = true) -> true
589+
else -> false
590+
}
591+
}
534592
}
535593
}
536594

wrywebview/src/main/rust/lib.rs

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ mod state;
1111
use std::str::FromStr;
1212
use std::sync::atomic::Ordering;
1313
use std::sync::Arc;
14+
use std::sync::OnceLock;
1415

1516
use wry::cookie::time::OffsetDateTime;
1617
use wry::cookie::{Cookie, Expiration, SameSite};
@@ -146,6 +147,18 @@ fn cookie_from_record(cookie: WebViewCookie) -> Result<Cookie<'static>, WebViewE
146147
Ok(builder.build())
147148
}
148149

150+
fn log_enabled() -> bool {
151+
static LOG_ENABLED: OnceLock<bool> = OnceLock::new();
152+
*LOG_ENABLED.get_or_init(|| {
153+
std::env::var("WRYWEBVIEW_LOG")
154+
.map(|value| {
155+
let value = value.trim().to_ascii_lowercase();
156+
matches!(value.as_str(), "1" | "true" | "yes" | "debug")
157+
})
158+
.unwrap_or(false)
159+
})
160+
}
161+
149162
// ============================================================================
150163
// WebView Creation
151164
// ============================================================================
@@ -329,10 +342,12 @@ pub fn create_webview_with_user_agent(
329342
// ============================================================================
330343

331344
fn set_bounds_inner(id: u64, x: i32, y: i32, width: i32, height: i32) -> Result<(), WebViewError> {
332-
eprintln!(
333-
"[wrywebview] set_bounds id={} pos=({}, {}) size={}x{}",
334-
id, x, y, width, height
335-
);
345+
if log_enabled() {
346+
eprintln!(
347+
"[wrywebview] set_bounds id={} pos=({}, {}) size={}x{}",
348+
id, x, y, width, height
349+
);
350+
}
336351
let bounds = make_bounds(x, y, width, height);
337352
with_webview(id, |webview| webview.set_bounds(bounds).map_err(WebViewError::from))
338353
}
@@ -778,12 +793,23 @@ fn destroy_webview_inner(id: u64) -> Result<(), WebViewError> {
778793

779794
#[uniffi::export]
780795
pub fn destroy_webview(id: u64) -> Result<(), WebViewError> {
796+
#[cfg(target_os = "macos")]
797+
{
798+
if MainThreadMarker::new().is_some() {
799+
return destroy_webview_inner(id);
800+
}
801+
DispatchQueue::main().exec_async(move || {
802+
let _ = destroy_webview_inner(id);
803+
});
804+
return Ok(());
805+
}
806+
781807
#[cfg(target_os = "linux")]
782808
{
783809
return run_on_gtk_thread(move || destroy_webview_inner(id));
784810
}
785811

786-
#[cfg(not(target_os = "linux"))]
812+
#[cfg(any(target_os = "windows", not(any(target_os = "linux", target_os = "macos"))))]
787813
run_on_main_thread(move || destroy_webview_inner(id))
788814
}
789815

0 commit comments

Comments
 (0)