https://kotlinlang.org logo
#compose
Title
# compose
j

Joseph Hawkes-Cates

05/11/2022, 2:59 PM
Good morning and Happy Google I/O day to everyone! I have a question about the Accompanist WebView. I think this may be an issue, but wanted to check before I logged it. Details in 🧵
We have a WebView in one of our screens and we’re running into a scenario where we navigate to another screen while keeping our WebView screen on the backstack and then we come back to our WebView screen. This causes the WebView to enter a brand new Composition when we come back and be essentially recreated. This is all fine and good, but we’re seeing an issue with the WebViewState when this happens.
The structure of our code is like this:
Copy code
var webView: WebView? by remember { mutableStateOf(null) }
val webViewState = rememberWebViewStateWithHTMLData(
   data = internalHtml,
   baseUrl = webViewUrl
)

 WebView(
     state = webViewState,
     captureBackPresses = false,
     onCreated = {
                webView = it
                it.clearCache(true)
                it.clearHistory()
                it.settings.javaScriptEnabled = true
                it.addJavascriptInterface(webHandler, webHandler.interfaceName)
                Timber.d("Created WebView")
     },
     client = webViewClient,
     chromeClient = remember { MyWebChromeClient() },
)

if(!webViewState.isLoading) {
   applyJavascript(webView)
}
The issue we’re seeing in this scenario is that we update the internalHTML mutableState after the initial recomposition which results in a new instance of WebViewState. This instance is not being set on the AccompanistWebViewClient, though, because the AndroidView factory lambda inside the WebView composable is not being called again after that update.
This results in the webViewState that the AccompanistWebView is updating and the WebViewState that we’re referencing being different which makes isLoading get stuck as true indefinitely.
The state is set here for the AccompanistWebViewClient, but that isn’t called again after the initial composition.
We have worked around this by checking the state on the webViewClient directly, but that is tricky because that state field is a lateinit field so we have to do this to use it safely:
Copy code
val isLoading = webView?.webViewClient?.let {
        webViewClient.state.isLoading
    } ?: true
This works because the webViewClient isn’t set on the WebView instance until after the state is set on line 96 that I linked to above, but it’s very dependent on the internal impl of the WebView composable which isn’t great
I think the proposal would be to update the state instance on the AccompanistWebViewClient and the chrome client when the state is changed.
I actually found a better workaround for this. Instead of using the provided rememberWebViewStateWithHTMLData, I can do this:
Copy code
val webViewData = remember(webViewHtml, webViewUrl) {
        WebContent.Data(internalHtml, webViewUrl)
    }
    // Only create this state instance once since the WebView Composable does not fully update for new instances
    val webViewState = remember { WebViewState(webViewData) }
    // When the webViewData changes, set it on the webViewState. Content is a mutableState field so updating it will trigger the appropriate recompositons.
    LaunchedEffect(webViewData) {
        webViewState.content = webViewData
    }
this is more verbose than my first workaround, but it is not dependent on the order of when things are set within the internal WebView creation code.
So my question is am I missing anything in my usage here or is this worth logging an issue for?
b

Ben Trengrove [G]

05/11/2022, 8:36 PM
Definitely sounds like an issue, please file
👍 1
j

Joseph Hawkes-Cates

05/12/2022, 10:07 PM
b

Ben Trengrove [G]

05/12/2022, 10:13 PM
Thank you!
👍 1
28 Views