This project explains how to save and restore the WebView state during navigation or recomposition.
It provides two solutions.
- Saving and restoring bundle of the last retrieved WebView.
- Saving and restoring WebView instances using a custom stack.
This is the simplest way to save and restore the state of the WebView. The process consists of the following steps :
- Save the WebView state in a bundle when the Composable is disposed, using
rememberSaveablewith a unique key. - 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
- 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.
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.
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)
}First, use a backStack as an argument to store WebView instances.
class DefaultWebViewController(
private val context: Context,
private val backStack: HashMap<String, WebView> = hashMapOf()
) : WebViewControllerNext, override the retrieve function to retrieve a WebView for a specific composable.
This function follows two steps:
- Get a WebView instance from the
backStackusing the specified key. - If no instance exists in the
backStack, create one and add it to thebackStack.
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
}
}
}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()
}
}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 }
)
}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()
)
}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
}
}
)
}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
- 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).
- 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.