在 Compose 中使用 AndroidView 加载 WebView,明明 HTML 加载了,JS 也执行了,但页面就是一片空白。浏览器中正常,百度正常,唯独自己的 H5 页面在 WebView 中“隐身”了。本文记录了一次完整的排查过程,并揭示了一个容易被忽视的布局参数陷阱。

一、现象描述

我们开发一个 Android 应用,其中有一个“刷题助手”功能,需要在 Compose 界面中嵌入一个 WebView 来加载一个在线 H5 页面(单页应用,使用 Hash 路由)。

代码看起来很简单:

kotlin

AndroidView(
    factory = { context ->
        WebView(context).apply {
            settings.javaScriptEnabled = true
            settings.domStorageEnabled = true
            // ... 其他设置
            webViewClient = WebViewClient()
            webChromeClient = WebChromeClient()
            loadUrl("http://example.com/exam_H5/#/")
        }
    },
    modifier = Modifier.fillMaxSize()
)

然而,运行后页面一片空白。但诡异的是:

  • 直接在手机浏览器中输入该 URL,页面正常显示。
  • 将 URL 换成 https://www.baidu.com,WebView 可以正常显示百度首页。
  • 使用 Chrome 远程调试(chrome://inspect)发现 HTML 已经加载,DOM 树也存在,但似乎没有渲染出来。

问题来了:为什么百度正常,而自己的 H5 页面空白?


二、尝试与猜想

1. 是不是 JavaScript 没有执行?

我们已经明确设置了 settings.javaScriptEnabled = true,而且百度正常,说明 JS 引擎没问题。

2. 是不是布局遮挡或高度为 0?

我们在 Compose 的 Column 中使用了标题栏 + 分割线 + WebView,使用 Modifier.fillMaxSize() 让 WebView 占据剩余空间。但看起来 WebView 确实铺满了屏幕(因为百度能正常显示),所以高度似乎不是问题。

为了验证,我们手动给 WebView 指定固定高度,比如 400dp,结果依然是空白。说明高度问题不是唯一原因

3. 是不是页面本身对 WebView 环境不兼容?

我们添加了各种 WebView 设置:启用 DOM 存储、混合内容允许、视口设置等,均无改善。远程调试控制台也没有明显的 JavaScript 错误。

就在一筹莫展之际,我们尝试了一个小改动——将 WebView 放在一个 FrameLayout 容器中再返回给 AndroidView,奇迹出现了:页面正常显示了!


三、关键差异:直接返回 View vs 返回容器

下面两段代码,前者空白,后者正常。

❌ 空白版本(直接返回 WebView)

kotlin

AndroidView(
    factory = { context ->
        WebView(context).apply {
            // ... 设置
            loadUrl(targetUrl)
        }
    },
    modifier = Modifier.weight(1f).fillMaxWidth()
)

✅ 正常版本(使用 FrameLayout 包裹)

kotlin

AndroidView(
    factory = { context ->
        FrameLayout(context).apply {
            val webView = WebView(context).apply {
                // ... 设置
                loadUrl(targetUrl)
            }
            addView(webView)
        }
    },
    modifier = Modifier.weight(1f).fillMaxWidth()
)

为什么多了一层 FrameLayout 就解决了?


四、根本原因分析

4.1 Compose 的 AndroidView 与 View 系统的交互

AndroidView 是 Compose 中用于嵌入传统 View 的桥接组件。当我们在 factory 中直接返回一个 View时,Compose 会将该 View 添加到 AndroidView 内部的 ViewGroup 中,并尝试通过 Modifier 传递尺寸约束。

然而,在 Column 布局中使用 weight 修饰符时,AndroidView 的尺寸计算可能会出现问题。具体表现为:

  • 外部 Modifier.weight(1f) 能够正确计算出宽高,但在传递给内部的 View 时,可能因为布局参数(LayoutParams)未正确设置而导致 View 的实际测量宽高为 0
  • WebView 对自身尺寸非常敏感,如果测量宽高为 0,它可能不会绘制任何内容(即使 HTML 已加载)。

4.2 为什么百度正常?

百度首页对页面尺寸有较好的自适应能力,即使 WebView 测量高度为 0,百度可能通过 CSS 或 JavaScript 自行调整,或者百度页面内容较少,即使高度为 0 也可能因为某些默认样式而显示部分内容?实际上,更可能的是百度在初始化时,即使高度为 0 也会强制显示,但多数 SPA 页面(如我们的考试 H5)会依赖一个固定的根容器(如 #app),其高度依赖于父容器高度,如果父容器高度为 0,则整个应用容器高度为 0,渲染结果就是空白。

4.3 为什么加了一层 FrameLayout 就好了?

当我们返回一个 FrameLayout 时:

  1. AndroidView 将尺寸约束传递给 FrameLayoutFrameLayout 获得正确的宽高。
  2. 我们在 FrameLayout 中添加 WebView,并使用默认的 LayoutParams.MATCH_PARENT,这样 WebView就会填满父容器(即 FrameLayout)。
  3. 这样一来,WebView 的测量尺寸就正确了,页面得以正常渲染。

本质上,FrameLayout 充当了一个“尺寸中转站”,确保内部的 WebView 获得了非零的宽高。


五、解决方案

方案一:使用 FrameLayout 包裹(推荐)

kotlin

AndroidView(
    factory = { context ->
        FrameLayout(context).apply {
            val webView = WebView(context).apply {
                // 所有设置...
                loadUrl(targetUrl)
            }
            addView(webView, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
            // 或者不指定,默认就是 MATCH_PARENT
        }
    },
    modifier = Modifier.weight(1f).fillMaxWidth()
)

方案二:直接为 WebView 设置 LayoutParams

kotlin

AndroidView(
    factory = { context ->
        WebView(context).apply {
            // 所有设置...
            layoutParams = ViewGroup.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT
            )
            loadUrl(targetUrl)
        }
    },
    modifier = Modifier.weight(1f).fillMaxWidth()
)

经过验证,方案二也能解决问题,说明关键就是让 WebView 的 LayoutParams 正确设置为 MATCH_PARENT。但在某些复杂布局中,方案一(容器)更为稳健。


六、经验总结

  1. 不要想当然地认为 AndroidView 会正确处理布局参数。尤其是在 Column + weight 的场景下,直接返回的 View 可能尺寸为 0。
  2. WebView 对尺寸极其敏感,如果宽高为 0,即使 HTML 加载完毕也不会显示任何内容。调试时可以通过 view.post { Log.d("size", "width=${width}, height=${height}") } 来验证。
  3. 百度正常不代表你的页面正常,因为百度可能对零尺寸有容错处理,而大多数 SPA 应用(如 Vue/React)依赖根容器的高度,若高度为 0 则整个应用不渲染。
  4. 多一层容器无伤大雅FrameLayout 是轻量级容器,增加一个层级不会对性能产生明显影响。相反,它能保证尺寸传递的正确性。
  5. 调试工具:务必开启 WebView 远程调试(WebView.setWebContentsDebuggingEnabled(true)),并查看 Console 和 Elements 面板。如果发现 DOM 存在但不可见,很可能就是尺寸问题。

七、结语

这次排查让我们深刻体会到 Compose 与传统 View 系统交互时的细节陷阱。虽然 Compose 已经极大地简化了 UI 开发,但在混合使用 AndroidView 时,仍然需要对 View 的布局参数有清晰的理解。

希望这篇文章能帮助遇到类似问题的开发者少走弯路。如果您的 WebView 也突然“隐身”了,不妨检查一下它的尺寸是否真的非零——也许一层 FrameLayout 就能让页面重见天日。