Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first in jetpack compose

I have a webview with one URL, whenever it redirects within the webview, I load the same composable again with a new URL with a new title, as the redirection happens so fast it is happening before the actual composable composes entirely, so it crashes saying java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first. I am currently using ram costa's compose destination library

navigator.value?.navigate(direction = MenuViewDestination,onlyIfResumed = true)

, I have used compose provided navigation as well before, I was facing the same problem in both cases, if I navigate with only resumed true then the page is not navigated itself for some reason, and I can't handle the exception as it is happening internally. Here i have attached the composable used in menu composable to load URL,

/* 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
        )
    }

}

important thing to note here is i have modified the accompanist webview a bit and using ViewModel to store the instance of the existing webview because accompanist webview recomposes everytime i navigate between composable within app which leads to reloads of webview, so did that workaround for now, i know storing view instances in ViewModel might cause memory leaks but i had no other way,

/**
 * 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()
    }
}
like image 852
bharath v Avatar asked Jan 31 '26 05:01

bharath v


1 Answers

An Android View cannot have more than one parents.

In your AndroidView lambda, you can check if the view has already been given a parent, which will be true during recompositions because you are remembering your View. If it has a parent, you should remove the view from the parent's view hierarchy.

You can modify the factory in ObMenuWebView and add the following

factory = { context ->
        existingWebView ?: WebView(context).apply {
            onCreated(this)

            layoutParams = ViewGroup.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT
            )

            webChromeClient = chromeClient
            webViewClient = client
        }.also {
            if(it.parent != null) (it.parent as ViewGroup).removeView(it) // this line is very crucial
            existingWebView = it
        }
    },
like image 66
Rafsanjani Avatar answered Feb 01 '26 19:02

Rafsanjani



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!