Solving WebView Lifecycle Crashes in Android: A Lifecycle-Aware Approach with Sealed Classes and Flow
Introduction
WebViews are a powerful tool in Android development, allowing you to load web content right inside your app. Whether you’re building an in-app browser or integrating third-party content, WebViews can handle it all.
But WebViews come with their own set of quirks. One common headache? Crashes caused by lifecycle mismatches.
If you’ve ever seen an error like this:
This happens because WebView callbacks like onPageStarted
and onPageFinished
don’t respect the lifecycle of the Fragment
they’re running in.
In this blog, we’ll explore:
- Why these crashes occur ?
- How we can leverage separation of concerns using
ViewModel
, Kotlin Flow, and sealed classes to handle WebView callbacks safely and cleanly ?
The Problem
When WebViews load content, they use WebViewClient
callbacks like:
onPageStarted
– called when a page starts loading.onPageFinished
– called when a page finishes loading.shouldOverrideUrlLoading
– called when navigating to a new URL.
These callbacks are asynchronous and don’t know about the lifecycle of your Fragment
or Activity
.
If they try to update a UI element (like a progress bar) after the Fragment
's view has been destroyed, you’ll encounter a crash.
Why WebView Callbacks Aren’t Lifecycle-Aware
- Independent of the UI Lifecycle: WebView callbacks are tied to the loading process, not the lifecycle of the UI hosting them.
- UI Updates After Destruction: If a callback tries to update the
Fragment
after its view has been destroyed, it throws anIllegalStateException
.
The Solution: Why ViewModel Helps
To handle WebView callbacks safely, we’ll:
- Use a ViewModel to abstract WebView logic from the UI.
- Implement sealed classes for WebView events (e.g.,
onPageStarted
,onPageFinished
). - Use Kotlin Flow for clean, asynchronous event handling.
- Observe these events in the
Fragment
only when itsView
is active.
Step-by-Step Implementation
Step 1: Define WebView Events with a Sealed Class
Create a sealed class to represent WebView events:
sealed class WebViewEvent {
data class LoadingComplete(val status: String) : WebViewEvent()
data class Error(val error: String) : WebViewEvent()
data class Next(val url: String, val partner: String) : WebViewEvent()
}
Step 2: Handle WebView Events in the ViewModel
The ViewModel
emits these events using MutableSharedFlow
, ensuring lifecycle awareness.
class WebViewModel : ViewModel() {
private val _webViewEvents = MutableSharedFlow<WebViewEvent>()
val webViewEvents: SharedFlow<WebViewEvent> = _webViewEvents
fun onLoadingComplete(status: String) {
viewModelScope.launch {
_webViewEvents.emit(WebViewEvent.LoadingComplete(status))
}
}
fun onError(error: String) {
viewModelScope.launch {
_webViewEvents.emit(WebViewEvent.Error(error))
}
}
fun onNext(url: String, partner: String) {
viewModelScope.launch {
_webViewEvents.emit(WebViewEvent.Next(url, partner))
}
}
}
Step 3: Use a Custom WebViewClient
Route WebView events to the ViewModel
:
class CustomWebClient(private val viewModel: WebViewModel) : WebViewClient() {
override fun onPageFinished(view: WebView, url: String) {
viewModel.onLoadingComplete("Page Finished Loading")
}
override fun onReceivedError(view: WebView?, request: WebResourceRequest?, error: WebResourceError?) {
viewModel.onError("Error loading page")
}
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
val url = request?.url.toString()
viewModel.onNext(url, "PartnerName")
return true
}
}
Step 4: Observe WebView Events in the Fragment
Use Flow
to observe events and update the UI only when the Fragment
's view is active.
class WebViewFragment : Fragment() {
private val viewModel: WebViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
viewModel.webViewEvents
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
.collectLatest { event ->
when (event) {
is WebViewEvent.LoadingComplete -> displayContent()
is WebViewEvent.Error -> showToast(event.error)
is WebViewEvent.Next -> loadNextUrl(event.url)
}
}
}
}
private fun displayContent() {
// Show the content after loading
}
private fun loadNextUrl(url: String) {
binding.webView.loadUrl(url)
}
}
Step 4: Clean Up Resources
To ensure no memory leaks, clear the WebView’s client when the Fragment
is destroyed.
override fun onDestroyView() {
binding.webView.webViewClient = null
super.onDestroyView()
}
Final Thoughts
By using ViewModel
, Kotlin Flow
, and sealed classes, we:
- Ensure WebView callbacks respect the
Fragment
lifecycle. - Decouple WebView logic from UI updates.
- Create a robust, maintainable solution for handling WebView events.
This approach not only solves lifecycle issues but also improves the overall architecture of your app. Have you faced similar challenges with WebViews? Share your thoughts and solutions in the comments below!