Jetpack Compose中WebView重定向引发IllegalStateException问题求助
我在Jetpack Compose中使用WebView加载URL时,遇到WebView内部重定向的场景:重定向会触发重新加载带新URL和标题的同一个Composable,但由于重定向速度过快,Composable还未完全组合完成就触发导航,直接抛出崩溃异常:
java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first.
当前使用Ram Costa的Compose Destinations库,导航代码如下:
navigator.value?.navigate(direction = MenuViewDestination,onlyIfResumed = true)
此前使用官方导航组件也出现相同问题。设置onlyIfResumed=true后页面无法正常导航,且该异常为内部抛出,无法捕获。
加载URL的Menu Composable代码
/* Adding custom accompanist WebView*/ @SuppressLint("SetJavaScriptEnabled") @Composable fun LoadMenuWebView(mainViewModel: MainViewModel, webViewModel: MenuWebViewModel, url: String?) { Timber.i("LoadWebView from menu $url") val context = LocalContext.current var extraHeaders: Map<String, String?>? webViewModel.menuWebViewState.value = url?.let { rememberWebViewState( it ) } mainViewModel.currentWebViewClient.value = remember { getWebViewClient( context, mainViewModel.backEnabled, mainViewModel.progressVisible, mainViewModel.cookieManager, mainViewModel ) } val state by remember { webViewModel.menuWebViewState } val navigator = rememberWebViewNavigator() // A custom WebChromeClient can be provided via subclassing if (state != null) { ObMenuWebView( state = state!!, captureBackPresses = false, onCreated = { webview -> webview.settings.apply { javaScriptEnabled = true builtInZoomControls = false displayZoomControls = false loadWithOverviewMode = true cacheMode = WebSettings.LOAD_CACHE_ELSE_NETWORK javaScriptCanOpenWindowsAutomatically = true mediaPlaybackRequiresUserGesture = false mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW useWideViewPort = true domStorageEnabled = true // Allow open _blank pages in browser setSupportMultipleWindows(true) } webview.addJavascriptClient(mainViewModel, context) }, navigator = navigator, client = remember { mainViewModel.currentWebViewClient.value }, chromeClient = remember { ExternalPagesClient(context, mainViewModel._showExternalLinkDialog) }, webViewModel = webViewModel ) } }
自定义Accompanist WebView代码
说明:修改了Accompanist WebView,将WebView实例存储在ViewModel中,避免原Accompanist WebView在Composable间导航重组时导致WebView重载。已知存储View实例可能引发内存泄漏,但暂无其他替代方案:
/** * A wrapper around the Android View WebView to provide a basic WebView composable. * * If you require more customisation you are most likely better rolling your own and using this * wrapper as an example. * * @param state The webview state holder where the Uri to load is defined. * @param captureBackPresses Set to true to have this Composable capture back presses and navigate * the WebView back. * @param navigator An optional navigator object that can be used to control the WebView's * navigation from outside the composable. * @param onCreated Called when the WebView is first created, this can be used to set additional * settings on the WebView. WebChromeClient and WebViewClient should not be set here as they will be * subsequently overwritten after this lambda is called. * @param client Provides access to WebViewClient via subclassing * @param chromeClient Provides access to WebChromeClient via subclassing * @sample com.google.accompanist.sample.webview.BasicWebViewSample */ @Composable fun ObMenuWebView( state: com.ob_core_framework.base.WebViewState, modifier: Modifier = Modifier, captureBackPresses: Boolean = true, navigator: WebViewNavigator = rememberWebViewNavigator(), onCreated: (WebView) -> Unit = {}, client: com.ob_core_framework.base.AccompanistWebViewClient = remember { com.ob_core_framework.base.AccompanistWebViewClient() }, chromeClient: com.ob_core_framework.base.AccompanistWebChromeClient = remember { com.ob_core_framework.base.AccompanistWebChromeClient() }, webViewModel: MenuWebViewModel ) { var existingWebView by remember { webViewModel.existingWebView } BackHandler(captureBackPresses && navigator.canGoBack) { existingWebView?.goBack() } LaunchedEffect(existingWebView, navigator) { with(navigator) { existingWebView?.handleNavigationEvents() } } // Set the state of the client and chrome client // This is done internally to ensure they always are the same instance as the // parent Web composable client.stateLocal = state client.navigatorLocal = navigator chromeClient.stateLocal = state AndroidView( factory = { context -> existingWebView ?: WebView(context).apply { onCreated(this) layoutParams = ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT ) webChromeClient = chromeClient webViewClient = client }.also { existingWebView = it } }, modifier = modifier ) { view -> when (val content = state.content) { is WebContent.Url -> { val url = content.url if (url.isNotEmpty() && url != view.url) { view.loadUrl(url, content.additionalHttpHeaders.toMutableMap()) } } is WebContent.Data -> { view.loadDataWithBaseURL(content.baseUrl, content.data, null, "utf-8", null) } } navigator.canGoBack = view.canGoBack() navigator.canGoForward = view.canGoForward() } }
核心原因
崩溃根源是ViewModel中存储的WebView实例在Composable快速重组/导航时,尚未从原父View中移除,就被新的AndroidView尝试添加为子View,触发"已有父View"的异常。
修复步骤
1. 复用WebView前先移除父View
修改ObMenuWebView的AndroidView factory逻辑,复用existingWebView前先检查并移除其原父布局:
AndroidView( factory = { context -> existingWebView?.let { webView -> // 从父布局中移除已有WebView (webView.parent as? ViewGroup)?.removeView(webView) webView } ?: WebView(context).apply { onCreated(this) layoutParams = ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT ) webChromeClient = chromeClient webViewClient = client }.also { existingWebView = it } }, modifier = modifier ) { view -> // 原有逻辑不变 }
2. 控制重定向导航时机
在自定义WebViewClient中,延迟导航触发时机,确保Composable完成组合后再执行导航:
// 自定义WebViewClient示例 override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean { val url = request?.url?.toString() ?: return super.shouldOverrideUrlLoading(view, request) // 延迟执行导航,等待Composable组合完成 view?.postDelayed({ navigator.value?.navigate(direction = MenuViewDestination) }, 200) // 延迟时间可根据实际场景调整 return true }
3. 优化WebView生命周期管理
在ViewModel的onCleared方法中主动销毁WebView,避免内存泄漏:
class MenuWebViewModel : ViewModel() { var existingWebView by mutableStateOf<WebView?>(null) override fun onCleared() { super.onCleared() existingWebView?.let { webView -> (webView.parent as? ViewGroup)?.removeView(webView) webView.destroy() existingWebView = null } } }
4. 替换onlyIfResumed为组合状态判断
使用Compose的LaunchedEffect配合awaitFrame()确保Composable完成组合后再导航:
// 在导航触发处添加 LaunchedEffect(Unit) { awaitFrame() // 等待Compose完成一帧绘制,确保组合完成 navigator.value?.navigate(direction = MenuViewDestination) }
内容的提问来源于stack exchange,提问作者bharath v

