You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 16:45:45