Skip to content

Commit e32aa97

Browse files
ochafikclaude
andcommitted
fix(kotlin-host): add proper WebView lifecycle management
- Add DisposableEffect to handle WebView cleanup on composable disposal - Add LifecycleEventObserver to pause/resume WebView on activity lifecycle changes - Properly clean up WebView reference when composable is disposed - Add update lambda to sync WebView state with lifecycle This fixes the issue where WebView content was killed when going out of view (e.g., scrolled off in LazyColumn) and failed to reload because the lifecycle wasn't properly managed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent e873118 commit e32aa97

File tree

1 file changed

+66
-0
lines changed
  • examples/basic-host-kotlin/src/main/kotlin/com/example/mcpappshost

1 file changed

+66
-0
lines changed

examples/basic-host-kotlin/src/main/kotlin/com/example/mcpappshost/MainActivity.kt

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ import androidx.compose.ui.draw.alpha
2626
import androidx.compose.ui.text.font.FontFamily
2727
import androidx.compose.ui.unit.dp
2828
import androidx.compose.ui.platform.LocalContext
29+
import androidx.compose.ui.platform.LocalLifecycleOwner
2930
import androidx.compose.ui.viewinterop.AndroidView
31+
import androidx.lifecycle.Lifecycle
32+
import androidx.lifecycle.LifecycleEventObserver
3033
import androidx.lifecycle.viewmodel.compose.viewModel
3134
import kotlinx.serialization.json.jsonArray
3235
import 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
339347
fun 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

Comments
 (0)