导航与WebView:在Navigation中集成WebView
说实话,WebView 在 Android 开发里是个老话题了。但放到 Navigation 组件里,事情就变得有点微妙。我见过不少团队,把 WebView 直接扔进 Fragment 里,然后返回键一按,整个页面就退出了——用户还在看文章呢,这体验太糟糕了。
今天我们就来聊聊,怎么在 Navigation 架构下,把 WebView 玩得转。
为什么要在 Navigation 里集成 WebView?
你想想看,现在很多 App 都是混合开发。部分页面用原生,部分页面用 H5。如果每个 WebView 页面都单独开一个 Activity,那 Navigation 的「单 Activity 架构」就形同虚设了。
我个人习惯的做法是:把 WebView 封装成一个独立的 Fragment,然后通过 Navigation 来管理它的生命周期和返回栈。这样,WebView 的页面跳转、返回键处理,都能和原生页面保持一致。
核心思路:WebView 不是「页面」,而是「组件」。它应该被 Fragment 包裹,由 Navigation 控制进出。
第一步:创建 WebViewFragment
我们先写一个基础的 WebViewFragment。注意,这里有个坑——WebView 的销毁时机。我曾经在项目里遇到过,Fragment 被销毁了,但 WebView 还在后台加载,导致内存泄漏。
class WebViewFragment : Fragment() {
private var _binding: FragmentWebviewBinding? = null
private val binding get() = _binding!!
private lateinit var webView: WebView
private var currentUrl: String? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentWebviewBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupWebView()
loadUrl(arguments?.getString("url") ?: "about:blank")
}
private fun setupWebView() {
webView = binding.webView.apply {
settings.javaScriptEnabled = true
settings.domStorageEnabled = true
settings.loadWithOverviewMode = true
settings.useWideViewPort = true
webViewClient = object : WebViewClient() {
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
currentUrl = url
// 可以在这里显示加载进度
}
override fun shouldOverrideUrlLoading(
view: WebView?,
request: WebResourceRequest?
): Boolean {
// 这里可以拦截特定 URL,跳转到原生页面
return false
}
}
}
}
override fun onDestroyView() {
super.onDestroyView()
// 重要:清理 WebView
binding.webView.destroy()
_binding = null
}
}
小技巧:在 onDestroyView 里调用 webView.destroy(),而不是 onDestroy。因为 Fragment 的视图可能被重建,但 WebView 实例需要彻底清理。
第二步:处理 WebView 的返回键
这是最容易被忽略的地方。默认情况下,用户在 WebView 里点返回,会直接退出 Fragment。但用户的本意是「回退到上一页」。
怎么做?我们需要拦截 Fragment 的返回事件,先让 WebView 自己处理。
class WebViewFragment : Fragment() {
// ... 前面的代码 ...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupWebView()
loadUrl(arguments?.getString("url") ?: "about:blank")
// 拦截返回键
requireActivity().onBackPressedDispatcher.addCallback(
viewLifecycleOwner,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (webView.canGoBack()) {
webView.goBack()
} else {
// 没有历史记录了,交给 Navigation 处理
isEnabled = false
requireActivity().onBackPressedDispatcher.onBackPressed()
}
}
}
)
}
}
嗯,这里要注意一点:OnBackPressedCallback 的 isEnabled 属性。当 WebView 可以回退时,我们拦截返回事件;当不能回退时,我们把控制权交还给 Navigation。这样,用户按返回键时,体验就和浏览器一样自然。
第三步:WebView 与 Fragment 通信
实际项目中,WebView 经常需要和原生代码交互。比如,H5 页面点击「购买」按钮,需要调起原生的支付页面。
我推荐使用 JavaScriptInterface 来实现。但要注意,这个接口是双向的——H5 可以调用原生,原生也可以调用 H5。
H5 调用原生
// 定义一个接口类
class WebAppInterface(private val fragment: WebViewFragment) {
@JavascriptInterface
fun showToast(message: String) {
// 在 UI 线程执行
fragment.requireActivity().runOnUiThread {
Toast.makeText(fragment.requireContext(), message, Toast.LENGTH_SHORT).show()
}
}
@JavascriptInterface
fun navigateToNative(destination: String) {
// 通过 Navigation 跳转到原生页面
val action = WebViewFragmentDirections.actionWebViewToNative(destination)
fragment.findNavController().navigate(action)
}
}
// 在 setupWebView 中注册
webView.addJavascriptInterface(WebAppInterface(this), "AndroidBridge")
H5 端调用方式:
// JavaScript 代码
AndroidBridge.showToast("Hello from H5!");
AndroidBridge.navigateToNative("payment");
安全警告:@JavascriptInterface 方法会在后台线程执行。如果你要更新 UI,记得切到主线程。另外,不要暴露敏感操作,防止 XSS 攻击。
原生调用 H5
有时候,我们需要从原生主动通知 H5。比如,用户登录成功后,告诉 H5 刷新用户信息。
// 在 Fragment 中调用
fun notifyLoginSuccess(userId: String) {
val script = "javascript:onLoginSuccess('$userId')"
webView.evaluateJavascript(script, null)
}
H5 端需要定义这个函数:
function onLoginSuccess(userId) {
console.log("User logged in: " + userId);
// 刷新页面数据
}
整体架构图
下面这张图,展示了 Navigation 与 WebView 的完整交互流程。我画的时候特意把「返回键处理」单独标出来了,因为这是最容易出问题的地方。
避坑指南
我在实际项目中踩过不少 WebView 的坑,这里列几个最常见的:
- WebView 内存泄漏:记得在 onDestroyView 中调用 destroy(),并且将 WebView 从父容器中移除。
- 返回键被吞掉:如果你用了多个 OnBackPressedCallback,注意它们的优先级。后注册的优先级更高。
- JavaScriptInterface 线程问题:所有 @JavascriptInterface 方法都在后台线程执行,更新 UI 必须切到主线程。
- URL 拦截:在 shouldOverrideUrlLoading 中,如果要跳转到原生页面,记得返回 true 阻止 WebView 加载。
我的经验:我曾经在一个电商项目里,WebView 加载的商品详情页,用户点「加入购物车」后,H5 通过 JavaScriptInterface 通知原生,原生再通过 Navigation 跳转到购物车页面。整个过程非常流畅,用户完全感觉不到是在混合开发。
总结
Navigation 组件和 WebView 的集成,说白了就是三件事:
- 把 WebView 封装到 Fragment 里,用 Navigation 管理生命周期
- 拦截返回键,让 WebView 先处理页面回退
- 用 JavaScriptInterface 和 evaluateJavascript 实现双向通信
做到这三点,你的 WebView 在 Navigation 架构下就能跑得很稳。下次遇到混合开发的需求,不妨试试这个方案。