KevinnZou / compose-webview-multiplatform

WebView for JetBrains Compose Multiplatform
https://kevinnzou.github.io/compose-webview-multiplatform/
Apache License 2.0
532 stars 69 forks source link

WebView recompositions when navigate from other tab/screen #53

Closed nickfaces closed 7 months ago

nickfaces commented 1 year ago

There are several screens and tabs in my application. On one of the screens in the tab there is a webview. Every time you switch between screens or tablets, webview refreshes the page. I'm not sure if the problem is in the webview or in voyager navigation, or even in my code. For the accompanist, I found a similar problem where the solution was to use rememberSaveableWebViewState https://github.com/google/accompanist/pull/1557 However, there is no such class in your library.

KevinnZou commented 1 year ago

@nickfaces Thank you for your feedback. I understand the issue you encountered and I want to support this feature in the library design. However, I am concerned that we may not be able to support it in a multiplatform situation. This is because the WebView in accompanist relies on Android-specific methods like saveState and restoreState to implement this feature. Unfortunately, these methods cannot be used on other platforms. As a result, the library is currently unable to save the state. I will explore alternative approaches to support this feature in the future, but it may take some time.

However, for your situation, I think the problem is not in my library. The main issue now is that Voyager destroys the Compose page when switching tabs, which results in a complete page refresh upon re-entry. In my opinion, this is actually unreasonable because tab switching on the home page is very frequent, and if every switch causes the destruction and reconstruction of the Compose page, it will lead to performance issues. We also encounter similar situations in our Android applications, where the tabs on our home page are generally composed of fragments. To avoid refreshing the view every time we switch, we use fragment.show/hide to toggle tabs, so it doesn't cause page reconstruction. I'm not quite sure how Voyager implements tab switching, but I don't think it should destroy the Compose page, resulting in a page reconstruction upon re-entry. Perhaps you can raise this issue with them and see if they have any suggestions.

Vaibhav2002 commented 9 months ago

Hi @KevinnZou This problem is there even with AnimatedContent, my web view state is lost when switching between composables using AnimatedContent

This is my code

@Composable
fun ReaderScreen(
    url: String,
    component: ReaderComponent
) {
    val listState = rememberLazyListState()
    val readerMode by component.readerMode.collectAsState()
    val state = rememberWebViewState(url)

    DisposableEffect(Unit){
        state.webSettings.apply {
            isJavaScriptEnabled = true
            backgroundColor = Color.White
            androidWebSettings.apply {
                domStorageEnabled = true
                safeBrowsingEnabled = true
                isAlgorithmicDarkeningAllowed = true
            }
        }

        onDispose {  }
    }

    BaseScreenContent(component = component){
        AnimatedContent(
            targetState = readerMode,
            transitionSpec = { fadeIn() togetherWith fadeOut() }
        ){
            when(it){
                ReaderMode.Reader -> InAppReader(Modifier.fillMaxSize(), listState, component)
                ReaderMode.Web -> WebReader(state, Modifier.fillMaxSize())
            }
        }
    }
}
KevinnZou commented 9 months ago

@nickfaces Yes, as long as the onDispose is called, the Webview state is lost. The only solution for this problem is to use rememberSaveableWebViewState. However, supporting it is challenging because iOS does not have built-in functions like saveState and restoreState for WKWebview. Therefore, we might have to seek external assistance to resolve this issue.

nickfaces commented 8 months ago

There is an interesting thing in WKWebView https://developer.apple.com/documentation/webkit/wkwebview/3752236-interactionstate Maybe it could help, but only for iOS 15+

KevinnZou commented 8 months ago

@nickfaces Thanks for your suggestions! It appears that interactionState is an NSObject? We need to find a way to serialize and deserialize it like Android.

nickfaces commented 8 months ago

@KevinnZou If I understand it correctly, then this may help https://developer.apple.com/documentation/foundation/nscoding

KevinnZou commented 8 months ago

@KevinnZou If I understand it correctly, then this may help https://developer.apple.com/documentation/foundation/nscoding

@nickfaces Thanks for your information! Yes, it works for the iOS side. However, we need to find a KMP way to handle the state save and restore which could be challenging.

skillAndroid commented 7 months ago

Hello as I see you still did not solve the problem when navigating to another page and returning to the same page when i have web view still crushing ! Pls brother this is a big problem i think!

grandleaf commented 7 months ago

Hello Kevinn, thank you so much for the wonderful webView implementation. I encountered the same problem for the state save/restore of the webView. Without this functionality, the webView can only be displayed as static without any animation and navigation, which is a huge limitation. I just wonder if you can elevate this issue and find a temporarily solution? If it is not possible to make it work both for Android and iOS for now, would you please make it work for Android first, then make it work in the iOS in future release? Thanks.

KevinnZou commented 7 months ago

@grandleaf Thank you for your understanding. As stated earlier, the problem is caused by the tab's force recomposition, and can only be temporarily resolved by state save/restore. I've been researching state save/restore support on iOS, but haven't found a way to make it work in a multiplatform environment. I will recheck next week and update you on the progress.

grandleaf commented 7 months ago

@KevinnZou, is it possible to provide a temporarily support for Android, then support the iOS part in the future? The reason for this is because the recomposition happens when the WebView in a navigation or even animation. Without a state save/restore, the usage of this component is very limited.

KevinnZou commented 7 months ago

@grandleaf Yes, I will start working on supporting at least the Android side next month. I apologize for any inconvenience caused by this issue.

grandleaf commented 7 months ago

@KevinnZou. Thank you for the help. I think the root cause might be the Single Activity design for the Composable. Any activity will be dispose if it is not in the foreground (unnecessary IMHO).

KevinnZou commented 7 months ago

@grandleaf I have added state save/restore functionality for Android and iOS 15+ in this pull request. It allows the webview to restore its state after recomposition. However, there may still be a flickering effect initially due to the recomposition. To resolve this issue completely, we may require official support to avoid recomposition in such situations.

grandleaf commented 7 months ago

@grandleaf I have added state save/restore functionality for Android and iOS 15+ in this pull request. It allows the webview to restore its state after recomposition. However, there may still be a flickering effect initially due to the recomposition. To resolve this issue completely, we may require official support to avoid recomposition in such situations.

@KevinnZou. Thank so much for your wonderful work. I do not expect the iOS part is also be accomplished 👍 One thing puzzled me a little bit: for such a common component, why these big companies do not even provide a basic support?

Thank you again!

grandleaf commented 7 months ago

@grandleaf I have added state save/restore functionality for Android and iOS 15+ in this pull request. It allows the webview to restore its state after recomposition. However, there may still be a flickering effect initially due to the recomposition. To resolve this issue completely, we may require official support to avoid recomposition in such situations.

The flicker issue is hard to solve since the WebView is too expensive to be recomposed even its states can be restored. This might be an issue for the single activity design of the framework. There is one approach that I found might solve this problem: simply hide the WebView. I planned to implement my own navigation instead of using the standard way.

Vaibhav2002 commented 6 months ago

@KevinnZou The rememberSaveable overload is only for URL Can you also add it for HTML data, this one

@Composable
fun rememberWebViewStateWithHTMLData(
    data: String,
    baseUrl: String? = null,
    encoding: String = "utf-8",
    mimeType: String? = null,
    historyUrl: String? = null,
)
KevinnZou commented 6 months ago

@Vaibhav2002 I don't have to look into it deeply now. But I did a quick test and it seems that Android WebView does not support restore state for HTML data. You are welcomed to submit a PR if you find a way to make it workable. Thanks!

Vaibhav2002 commented 6 months ago

@KevinnZou the fix rememberSaveableWebViewState still loads the url again when switching composables using a AnimatedContent

This is my code, am i doing anything wrong

@Composable
internal fun WebReader(
    url: String,
    modifier:Modifier = Modifier
) {
    val state = rememberSaveableWebViewState(url)
    val navigator = rememberWebViewNavigator()

    DisposableEffect(Unit){
        state.webSettings.apply {
            isJavaScriptEnabled = true
            backgroundColor = Color.White
            androidWebSettings.apply {
                domStorageEnabled = true
                safeBrowsingEnabled = true
                isAlgorithmicDarkeningAllowed = true
            }
        }

        onDispose {  }
    }

    LaunchedEffect(navigator){
        navigator.loadUrl(url)
    }

    Column(modifier = modifier) {
        if (state.loadingState !is LoadingState.Finished) {
            LinearProgressIndicator(
                modifier = Modifier.fillMaxWidth().height(3.dp),
                trackColor = MaterialTheme.colorScheme.surface,
                color = LocalContentColor.current
            )
        }
        WebView(
            state = state,
            navigator = navigator,
            modifier = Modifier.fillMaxSize().navigationBarsPadding()
        )
    }
}

My animated content code

AnimatedContent(
    targetState = readerMode,
    transitionSpec = { fadeIn() togetherWith fadeOut() }
){
    when(it){
        ReaderMode.Reader -> InAppReader(Modifier.fillMaxSize(), listState, component)
        ReaderMode.Web -> WebReader(url, Modifier.fillMaxSize())
    }
}
KevinnZou commented 6 months ago

@Vaibhav2002 Does AnimatedContent implement SaveableStateHolder? rememberSaveableWebViewState only works with SaveableStateHolder.

Vaibhav2002 commented 6 months ago

@Vaibhav2002 Does AnimatedContent implement SaveableStateHolder? rememberSaveableWebViewState only works with SaveableStateHolder.

@KevinnZou I does not work even without Animated Content

This is my code

when (readerMode) {
    ReaderMode.Reader -> InAppReader(Modifier.fillMaxSize(), listState, component)
    ReaderMode.Web -> WebReader(url, Modifier.fillMaxSize())
}

This is my webview composable

@Composable
internal fun WebReader(
    url: String,
    modifier:Modifier = Modifier
) {
    val state = rememberSaveableWebViewState(url)
    val navigator = rememberWebViewNavigator()

    LaunchedEffect(url){
        navigator.loadUrl(url)
    }

    DisposableEffect(Unit){
        state.webSettings.apply {
            isJavaScriptEnabled = true
            backgroundColor = Color.White
            androidWebSettings.apply {
                domStorageEnabled = true
                safeBrowsingEnabled = true
                isAlgorithmicDarkeningAllowed = true
            }
        }

        onDispose {  }
    }

    Column(modifier = modifier) {
        if (state.loadingState !is LoadingState.Finished) {
            LinearProgressIndicator(
                modifier = Modifier.fillMaxWidth().height(3.dp),
                trackColor = MaterialTheme.colorScheme.surface,
                color = LocalContentColor.current
            )
        }
        WebView(
            state = state,
            navigator = navigator,
            modifier = Modifier.fillMaxSize().navigationBarsPadding()
        )
    }
}
KevinnZou commented 6 months ago

@Vaibhav2002 rememberSavable only works with SaveableStateHolder, you can check this sample for reference.

Vaibhav2002 commented 6 months ago

@KevinnZou this works, but there is a blink and my web view scroll state is lost and it does not work when i use AnimatedContent to add animation Also this SaveableStateHolder approach does not work when WebView is a part of a LazyColumn Item.