Flutter webview_flutter如何显示加载屏直到JS执行完成
问题核心原因
onPageFinished回调会在页面内子资源加载、iframe加载、页面内跳转时多次触发,原代码每次触发只要pageLoaded为false就新建一个弹窗,自然会出现多个弹窗堆叠的问题。- 弹窗触发时机错误:原逻辑等
onPageFinished触发才弹加载框,此时网页已经完成首帧渲染,用户会先看到未处理的原始页面,再看到加载提示,不符合“全程不展示未处理页面”的需求。 - 关闭弹窗时使用的是WebView组件的context,而非弹窗对应的路由context,组件重绘后context关联的路由关系失效,就会出现弹窗无法正常关闭的问题。
- 原逻辑中WebView从初始化开始就是可见状态,JS执行清理的过程用户能直接看到页面元素变化,不符合需求。
实现方案
放弃用showDialog实现加载提示的思路,改用Stack布局直接在WebView上层叠加全屏不可交互的加载遮罩,从应用启动就显示遮罩,等页面加载、JS清理全部完成后再隐藏遮罩,逻辑完全可控,不会出现多弹窗、关不掉的问题。
完整可运行代码如下:
import 'dart:async'; import 'package:flutter/material.dart'; import 'package:webview_flutter/webview_flutter.dart'; void main() => runApp(const MaterialApp(home: PageLoadApp())); class PageLoadApp extends StatefulWidget { const PageLoadApp({Key? key}) : super(key: key); @override State<PageLoadApp> createState() => _PageLoadAppState(); } class _PageLoadAppState extends State<PageLoadApp> { // 标记是否所有初始化流程(页面加载+JS清理)完成 bool _allFlowDone = false; // 标记初始页面是否已经触发过onPageFinished,避免重复执行清理逻辑 bool _initialPageLoaded = false; late final WebViewController _webViewController; // 模拟拉取目标URL的延迟逻辑 late final Future<String> _targetUrlFuture = _getTargetUrl(); Future<String> _getTargetUrl() async { await Future.delayed(const Duration(seconds: 10)); return 'https://www.canada.ca/en/immigration-refugees-citizenship/corporate/publications-manuals/discover-canada/read-online/canadas-history.html'; } // 执行页面元素清理JS Future<void> _cleanupPageElements() async { const List<String> jsCommands = [ 'document.getElementById("wb-lng").hidden = true;', 'document.getElementById("wb-srch").hidden = true', 'document.getElementsByClassName("gcweb-menu")[0].hidden = true', 'document.getElementsByClassName("mwsalerts")[0].hidden = true', 'document.getElementById("wb-bc").hidden = true', 'document.getElementsByClassName("pagination")[0].hidden = true', 'document.getElementsByClassName("pagedetails")[0].hidden = true', 'document.getElementsByClassName("global-footer")[0].hidden = true' ]; for (final js in jsCommands) { await _webViewController.runJavascript(js); } } @override void initState() { super.initState(); // 初始化WebViewController _webViewController = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) ..setBackgroundColor(const Color(0x00000000)) ..setNavigationDelegate( NavigationDelegate( onPageStarted: (String url) { debugPrint('开始加载页面: $url'); }, onProgress: (int progress) { debugPrint('页面加载进度: $progress%'); }, onPageFinished: (String url) async { debugPrint('页面加载完成: $url'); // 只在首次加载目标页时执行清理,避免子页面、iframe加载重复触发 if (!_initialPageLoaded) { _initialPageLoaded = true; // 执行所有清理JS await _cleanupPageElements(); debugPrint('页面清理完成'); // 所有流程结束,隐藏加载层 if (mounted) { setState(() { _allFlowDone = true; }); } } }, ), ); } @override Widget build(BuildContext context) { return Scaffold( body: SafeArea( child: FutureBuilder<String>( future: _targetUrlFuture, builder: (context, snapshot) { // URL还在拉取中,显示加载指示器 if (!snapshot.hasData) { return const Center( child: CircularProgressIndicator(), ); } // URL拉取完成,加载WebView _webViewController.loadRequest(Uri.parse(snapshot.data!)); return Stack( children: [ // WebView层 WebViewWidget(controller: _webViewController), // 加载遮罩层,未完成全部流程时显示 if (!_allFlowDone) Positioned.fill( // 拦截所有用户交互,无法手动关闭 child: AbsorbPointer( absorbing: true, child: Container( color: Colors.white, child: const Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ CircularProgressIndicator(), SizedBox(height: 16), Text('Loading ...'), ], ), ), ), ), ), ], ); }, ), ), ); } }
关键逻辑说明
- 加载遮罩从WebView初始化开始就存在,不存在延迟弹出、重复弹出的问题,通过
AbsorbPointer拦截所有触摸事件,用户无法手动关闭。 - 加了
_initialPageLoaded标记,避免onPageFinished多次触发时重复执行JS清理逻辑。 - 等所有JS清理逻辑执行完成后,才更新状态隐藏加载遮罩,用户全程看不到未清理的原始页面,也看不到JS修改元素的过程。
- 完全通过状态控制UI显示隐藏,不需要调用
Navigator操作弹窗路由,从根源上避免了context不匹配导致的弹窗无法关闭问题。 - 流程完全符合需求:启动后拉取URL → 加载网页 → 执行JS清理 → 清理完成展示最终页面,全程显示不可关闭的加载提示。
内容的提问来源于stack exchange,提问作者daydreamer
相关产品推荐
相关产品推荐

