@@ -26,7 +26,10 @@ import androidx.compose.ui.draw.alpha
2626import androidx.compose.ui.text.font.FontFamily
2727import androidx.compose.ui.unit.dp
2828import androidx.compose.ui.platform.LocalContext
29+ import androidx.compose.ui.platform.LocalLifecycleOwner
2930import androidx.compose.ui.viewinterop.AndroidView
31+ import androidx.lifecycle.Lifecycle
32+ import androidx.lifecycle.LifecycleEventObserver
3033import androidx.lifecycle.viewmodel.compose.viewModel
3134import kotlinx.serialization.json.jsonArray
3235import kotlinx.serialization.json.jsonObject
@@ -334,6 +337,11 @@ fun ToolCallCard(
334337
335338/* *
336339 * WebView composable that handles full MCP Apps protocol communication.
340+ *
341+ * Implements proper lifecycle management to handle:
342+ * - Activity pause/resume (calls WebView.onPause/onResume)
343+ * - Composable disposal (cleans up WebView references)
344+ * - LazyColumn recycling (preserves WebView state via key)
337345 */
338346@Composable
339347fun McpAppWebView (
@@ -345,6 +353,7 @@ fun McpAppWebView(
345353 modifier : Modifier = Modifier
346354) {
347355 val context = LocalContext .current
356+ val lifecycleOwner = LocalLifecycleOwner .current
348357 val coroutineScope = rememberCoroutineScope()
349358 val json = remember { kotlinx.serialization.json.Json { ignoreUnknownKeys = true } }
350359 var webViewRef by remember { mutableStateOf<WebView ?>(null ) }
@@ -353,6 +362,50 @@ fun McpAppWebView(
353362 var teardownCompleted by remember { mutableStateOf(false ) }
354363 var currentHeight by remember { mutableIntStateOf(toolCall.preferredHeight) }
355364
365+ // Track whether the WebView is currently paused
366+ var isPaused by remember { mutableStateOf(false ) }
367+
368+ // Lifecycle observer to pause/resume WebView when activity lifecycle changes
369+ DisposableEffect (lifecycleOwner) {
370+ val observer = LifecycleEventObserver { _, event ->
371+ when (event) {
372+ Lifecycle .Event .ON_PAUSE -> {
373+ android.util.Log .d(" McpAppWebView" , " Lifecycle ON_PAUSE - pausing WebView" )
374+ webViewRef?.onPause()
375+ isPaused = true
376+ }
377+ Lifecycle .Event .ON_RESUME -> {
378+ android.util.Log .d(" McpAppWebView" , " Lifecycle ON_RESUME - resuming WebView" )
379+ if (isPaused) {
380+ webViewRef?.onResume()
381+ isPaused = false
382+ }
383+ }
384+ Lifecycle .Event .ON_DESTROY -> {
385+ android.util.Log .d(" McpAppWebView" , " Lifecycle ON_DESTROY - cleaning up WebView" )
386+ webViewRef?.let { wv ->
387+ wv.stopLoading()
388+ wv.destroy()
389+ }
390+ webViewRef = null
391+ }
392+ else -> {}
393+ }
394+ }
395+ lifecycleOwner.lifecycle.addObserver(observer)
396+ onDispose {
397+ android.util.Log .d(" McpAppWebView" , " DisposableEffect onDispose - cleaning up" )
398+ lifecycleOwner.lifecycle.removeObserver(observer)
399+ // Clean up WebView when composable is disposed
400+ webViewRef?.let { wv ->
401+ wv.stopLoading()
402+ // Don't destroy here - just clear the reference
403+ // The WebView will be garbage collected or reused
404+ }
405+ webViewRef = null
406+ }
407+ }
408+
356409 // Inject bridge script into HTML
357410 val injectedHtml = remember(toolCall.htmlContent) {
358411 injectBridgeScript(toolCall.htmlContent!! )
@@ -574,6 +627,19 @@ fun McpAppWebView(
574627 loadDataWithBaseURL(null , injectedHtml, " text/html" , " UTF-8" , null )
575628 }
576629 },
630+ update = { webView ->
631+ // Update webViewRef if it changed (e.g., after recreation)
632+ if (webViewRef != webView) {
633+ android.util.Log .d(" McpAppWebView" , " WebView reference updated" )
634+ webViewRef = webView
635+ }
636+ // Sync paused state with current lifecycle
637+ if (isPaused && lifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle .State .RESUMED )) {
638+ android.util.Log .d(" McpAppWebView" , " Resuming WebView after lifecycle sync" )
639+ webView.onResume()
640+ isPaused = false
641+ }
642+ },
577643 modifier = modifier
578644 )
579645}
0 commit comments