Skip to content

This project explains how to save and restore the WebView state during navigation or recomposition.

Notifications You must be signed in to change notification settings

easternkite/RecomposableWebview

Repository files navigation

RecomposableWebview

This project explains how to save and restore the WebView state during navigation or recomposition.

It provides two solutions.

  1. Saving and restoring bundle of the last retrieved WebView.
  2. Saving and restoring WebView instances using a custom stack.

1. Saving and restoring bundle of the last retrieved WebView.

This is the simplest way to save and restore the state of the WebView. The process consists of the following steps :

  1. Save the WebView state in a bundle when the Composable is disposed, using rememberSaveable with a unique key.
  2. Restore the state from the bundle if it is not empty.
@Composable
fun WebView(
  key: String,
  modifier: Modifier = Modifier,
) {
  val savedBundle: Bundle = rememberSaveable(key) { Bundle() }
  var canGoBack by remember {
    mutableStateOf(false)
  }

  val webView = remember(key) {
    WebView(context).apply { /* ... */ }
  }

  WebViewInternal(
    modifier = modifier,
    webView = webView,
    canGoBack = canGoBack,
    onDispose = {
      webView.saveState(savedBundle)
      webView.destroy()
    }
  )
}

@Composable
fun WebViewInternal(
  webView: WebView,
  canGoBack: Boolean,
  onDispose: () -> Unit,
  modifier: Modifier = Modifier,
) {
  DisposableEffect(webView) {
    onDispose(onDispose)
  }

  BackHandler(canGoBack) {
    webView.goBack()
  }

  AndroidView(
    modifier = modifier,
    factory = { webView }
  )
}
solution_1.mp4

pros and cons.

  • Advantages
    • Simple to implement.
  • Disadvantages
    • The Webview instance is recreated frequantly when navigating between screens.
    • Only works on Android; not compatible with Kotlin Multiplatform (KMP)
    • Cannot restore video playback state after navigation or recomposition.

Saving and Restoring WebView Instances Using a Custom Stack

This solution provides a way to save WebView instances using a HashMap.
To implement this solution, define a WebViewController interface to control WebView instances stored in a HashMap.

Implementing a WebViewController

This interface provides methods to retrieve and dispose of WebViews.

interface WebViewController {  
    fun retrieve(key: String, onUpdateCanGoBack: (Boolean) -> Unit): WebView  
    fun dispose(key: String, isPop: Boolean)  
}

Implementation

First, use a backStack as an argument to store WebView instances.

class DefaultWebViewController(  
    private val context: Context,  
    private val backStack: HashMap<String, WebView> = hashMapOf()  
) : WebViewController

Next, override the retrieve function to retrieve a WebView for a specific composable.
This function follows two steps:

  1. Get a WebView instance from the backStack using the specified key.
  2. If no instance exists in the backStack, create one and add it to the backStack.
override fun retrieve(  
    key: String,  
    onUpdateCanGoBack: (Boolean) -> Unit  
): WebView {  
    return backStack[key] ?: run {  
        WebView(context).apply {  
            settings.apply {  
                javaScriptEnabled = true  
                domStorageEnabled = true  
            }  
  
            webViewClient = object : WebViewClient() {  
                override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {  
                    super.onPageStarted(view, url, favicon)  
                    onUpdateCanGoBack(view?.canGoBack() ?: false)  
                }  
  
                override fun onPageFinished(view: WebView?, url: String?) {  
                    super.onPageFinished(view, url)  
                    onUpdateCanGoBack(view?.canGoBack() ?: false)  
                }  
            }  
            loadUrl("https://www.google.com")  
            webChromeClient = WebChromeClient()  
            backStack[key] = this  
        }  
    }
}

Disposing of WebView Instances

The dispose function releases WebView resources and removes them from the backStack.
When disposing of a WebView, it is essential to determine whether the WebView composable has been removed from the NavBackStack to avoid unnecessary destruction.
The isPop argument indicates whether the WebView is being popped from the back stack.

override fun dispose(key: String, isPop: Boolean) {  
    if (isPop) {  
        val removed = backStack.remove(key)  
        removed?.destroy()  
    }  
}

Composable Implementation

Here’s the WebView composable that uses the controller to retrieve and dispose of WebView instances.

fun WebView(  
    key: String,  
    controller: WebViewController,  
    isPop: () -> Boolean,  
    modifier: Modifier = Modifier,  
) {  
    var canGoBack by rememberSaveable(key) { mutableStateOf(false) }  
    val webView = controller.retrieve(key) { canGoBack = it }  
  
    WebViewInternal(  
        webView = webView,  
        canGoBack = canGoBack,  
        onDispose = { controller.dispose(key, isPop()) },  
        modifier = modifier  
    )  
}

@Composable  
fun WebViewInternal(  
    webView: WebView,  
    canGoBack: Boolean,  
    onDispose: () -> Unit,  
    modifier: Modifier = Modifier,  
) {  
    DisposableEffect(webView) {  
        onDispose(onDispose)  
    }  
  
    BackHandler(canGoBack) {  
        webView.goBack()  
    }  
  
    AndroidView(  
        modifier = modifier,  
        factory = { webView }  
    )  
}

Caller Site Implementation

The caller site needs to pass an isPop() lambda to determine whether the WebView is being popped from the back stack.
The isPop function checks if the given ID is still present in the current back stack.

fun isPop(id: String, isBottomItem: Boolean) = if (isBottomItem) {  
    false  
} else {  
    controller  
        .currentBackStack.value  
        .none { it.id == id }  
}

Then, you can call the WebView composable as follows:

composable(Routes.TWO.route) {  
    WebView(  
        key = it.id,  
        controller = webViewController,  
        isPop = { isPop(Routes.TWO.route, true) },  
        modifier = Modifier.fillMaxSize()  
    )  
}

Retaining BackStack State from Configuration Changes Using RememberSaveable

When a configuration change occurs (such as screen rotation), all saved WebView instances can be lost because the activity is recreated. To address this issue, we can implement a custom Saver to retain backStack instances across configuration changes.

companion object {
    private var savedBackStack: HashMap<String, WebView>? = null

    fun Saver(context: Context) = listSaver(
        save = {
            savedBackStack = it.backStack
            listOf("")
        },
        restore = {
            DefaultWebViewController(context, savedBackStack ?: hashMapOf()).apply {
                savedBackStack = null
            }
        }
    )
}

Explanation

1. Temporary Storage:

  • Uses a companion object to store the backStack temporarily.
  • This allows the state to persist through configuration changes.

2. Custom Saver Implementation:

  • The save function stores the current backStack in the companion object.
  • The restore function creates a new DefaultWebViewController with the saved backStack or an empty one if none exists.
  • After restoration, the saved instance is cleared to avoid memory leaks.

By implementing a custom Saver, we ensure that WebView instances are retained even during configuration changes, maintaining seamless user experiences.

solution.2.mp4

Pros and Cons

Advantages

  • Perfect State Restoration: Perfectly saves and restores WebView state, including WebView history and video playback progress.
  • Multiplatform Potential: Can potentially support multiplatform (e.g., WKWebView for iOS, JCEF for JVM).

Disadvantages

  • Memory Usage Issues: Can cause increased memory consumption, especially with multiple WebView instances.
  • Implementation Complexity: More challenging to implement compared to simpler state-saving approaches.

About

This project explains how to save and restore the WebView state during navigation or recomposition.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages