← Back to writing

5 WebView Bugs That Weren't in the Web Page, and How I Fixed Them

Blank screens, crashes and broken video in Android apps on classroom panels. Each looked like a web page problem, and each was really in how Android created, sized or hosted the WebView.

  • Android
  • WebView
  • System apps
  • Jetpack Compose

The question

When a WebView screen is blank, the instinct is to open DevTools and look at the page. Over two apps, the Teachmint phone app and an AI assistant that runs as a system app on classroom panels, most of my WebView bugs weren’t in the page. They were in how Android creates, sizes, renders and hosts WebView. Some never got as far as loading a URL.

My initial mental model

WebView is a View. You construct it, give it a URL, and the page’s problems are the page’s problems.

WebView is actually a separate, independently updated system package, loaded into your process at runtime, with rules about which processes may load it at all. Each of the five bugs below breaks one of those assumptions.

1. A system app may not use WebView at all

To get system-level permissions without asking each OEM to allowlist them, I made two OEM flavours of the assistant run as the system user, using android:sharedUserId="android.uid.system" and the platform certificate. The next day it was reverted.

Running as uid 1000, the first WebView in the process threw:

WebViewFactory: WebView is not allowed in privileged processes

It’s a deliberate security rule. WebView renders untrusted web content, so Android refuses to load it into the system user’s processes. The chat’s link viewer and the YouTube player both crashed. The fix was to go back to a normal per-app uid, as a platform-signed privileged app, and get permissions the ordinary way.

The lesson generalises. “Run as system” isn’t a free upgrade. It changes which parts of the platform will work in your process at all, and WebView is the first to refuse.

2. The WebView provider can change under you

WebView is supplied by a separate, updatable package. That has two consequences most apps never see, until they ship on OEM ROMs:

  • It can be missing or disabled. On some devices the provider package is absent or turned off, and WebView(context) throws. In the phone app, every WebView screen now checks for a provider first:

    fun isWebViewPackageAvailable(context: Context): Boolean {
        val provider = WebView.getCurrentWebViewPackage() ?: return false
        return try {
            context.packageManager.getPackageInfo(provider.packageName, 0); true
        } catch (e: PackageManager.NameNotFoundException) { false }
    }

    If it fails, the screen shows a fallback with a way to install it, instead of crashing.

  • It can be updated while your process is running. On a panel, the YouTube player’s WebView constructor threw Resources$NotFoundException: failed to redirect ResourcesImpl. The provider had changed under the running process, and its resources no longer resolved. The fix is unglamorous: construct WebView inside try, and show a placeholder that opens the video externally. A blank player is better than losing the whole chat.

3. 100vh was zero

Our AI tutor generates small interactive lessons, such as a racing game or a canvas animation, and shows them in a WebView inside Compose. On the panel some of them were blank, even though a server-side check in headless Chrome passed them.

WebView DevTools on the panel showed the cause in one number: 100vh was 0, while window.innerHeight was 582. Every element sized in viewport units collapsed. The game’s container was 4 pixels tall, and its canvas had height 0.

The WebView was created inside a Compose AndroidView, which gives its child WRAP_CONTENT layout params by default. A WebView that wraps its content gives Chromium a zero-height layout viewport to compute vh against, so the page has no viewport height to fill. The fix is one line in the factory:

AndroidView(factory = { ctx ->
    WebView(ctx).apply {
        layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)
        // …
    }
})

After it, vh resolved to 582 on the panel and the game rendered and played. This is the bug I’d warn any Compose developer about. It looks like a broken page, it passes every off-device check, and it’s a layout-params default.

4. Creating WebView early costs every feature that needs an Activity

In the phone app, opening a web feature cold meant starting Chromium and then loading the page. I tried the obvious optimisation: create and warm the WebView in Application.onCreate, before any screen needed it.

It was reverted the next day. A WebView created in Application has no Activity, so the manager’s activity reference had to become nullable, and so did everything that needs one:

  • fullscreen video, through onShowCustomView
  • new windows, through onCreateWindow
  • the file chooser
  • camera capture

Each of those became a silent failure path. The better answer to cold loads turned out to be keeping WebViews that had already loaded alive across navigation, in a small LRU cache.

5. A software layer breaks video

A WebView in the phone app was switched to a software layer to work around a rendering problem. Two months later it was switched back, with the commit message “due to video playing issue”. A software layer turns off hardware acceleration for the view, and video and accelerated canvases depend on it. It’s a lever to pull only with a test for every kind of content the page shows, and video should be first on that list.

Practical consequences

A checklist I now run for any WebView screen:

Check Why
Not running as android.uid.system WebView refuses privileged processes
Provider present and enabled, before construction It can be missing or disabled
Constructor inside try, with a fallback UI The provider can change under a running process
MATCH_PARENT inside Compose AndroidView WRAP_CONTENT gives the page a zero-height viewport
An Activity behind the WebView Fullscreen, new windows and pickers need one
Hardware layer, and a video test before changing it Software layers break video and accelerated canvas

What I would remember

  1. WebView is a separate, updatable package loaded at runtime. Treat creating it as fallible.
  2. The system uid is the one place WebView won’t run.
  3. Inside Compose, set WebView’s layout params explicitly, or vh may be zero.
  4. When a page works everywhere except your app, suspect the host before the page.
  5. Faster creation isn’t worth losing the Activity. Cache loaded WebViews instead.

Further reading