You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Jetpack Compose中WebView重定向引发IllegalStateException问题求助

问题:Jetpack Compose中WebView重定向导致崩溃

我在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.21 20:57:19