Flutter WebView 内嵌 H5 页面的鸿蒙化适配与实战指南

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net


各位小伙伴们好呀!👋 我是那个上海某高校的大一计算机学生,继续来给大家分享 Flutter for OpenHarmony 开发的学习心得!

今天要聊的是 WebView 内嵌 H5 页面!很多 App 都会内嵌一些 Web 页面,比如:

  • 📜 隐私政策、用户协议
  • 🎮 H5 游戏、活动页面
  • 📰 新闻资讯

用 Flutter 原生写这些页面太麻烦了,直接用 WebView 嵌入现成的网页不香吗?今天就来详细分享一下!


一、功能引入介绍 📱

1.1 什么时候用 WebView?

  • 📄 隐私政策、用户协议(更新频繁,适合 H5)
  • 🎯 活动页面、促销专题(营销需求,快速上线)
  • 🎮 H5 小游戏(无需原生开发)
  • 📰 新闻详情、内容页(内容管理系统产出)

1.2 Flutter WebView 方案

插件 优点 缺点
webview_flutter 官方维护、功能完善 ✅ 配置稍复杂
flutter_webview_plugin API 简单 已停止维护

我们选择 webview_flutter


二、环境与依赖配置 🔧

2.1 pubspec.yaml 依赖

dependencies:
  flutter:
    sdk: flutter
  
  # ========== WebView ==========
  webview_flutter: ^4.10.0

三、分步实现完整代码 🚀

3.1 基础 WebView 页面

import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

/// WebView 页面
/// 
/// 用于内嵌 H5 页面,支持加载进度显示、导航控制等
class WebViewPage extends StatefulWidget {
  /// 网页 URL
  final String url;
  
  /// 页面标题(可选)
  final String? title;
  
  /// 是否显示 AppBar
  final bool showAppBar;

  const WebViewPage({
    super.key,
    required this.url,
    this.title,
    this.showAppBar = true,
  });

  
  State<WebViewPage> createState() => _WebViewPageState();
}

class _WebViewPageState extends State<WebViewPage> {
  /// WebView 控制器
  late final WebViewController _controller;
  
  /// 是否正在加载
  bool _isLoading = true;
  
  /// 加载进度(0.0 - 1.0)
  double _loadingProgress = 0;

  
  void initState() {
    super.initState();
    _initWebView();
  }

  /// 初始化 WebView
  void _initWebView() {
    _controller = WebViewController()
      // 允许 JavaScript
      ..setJavaScriptMode(JavaScriptMode.unrestricted)
      // 设置背景色
      ..setBackgroundColor(Colors.white)
      // 设置导航代理
      ..setNavigationDelegate(
        NavigationDelegate(
          // 加载进度回调
          onProgress: (int progress) {
            setState(() {
              _loadingProgress = progress / 100;
            });
          },
          // 页面开始加载
          onPageStarted: (String url) {
            setState(() => _isLoading = true);
          },
          // 页面加载完成
          onPageFinished: (String url) {
            setState(() => _isLoading = false);
          },
          // 加载错误
          onWebResourceError: (WebResourceError error) {
            debugPrint('WebView Error: ${error.description}');
          },
          // 导航请求拦截
          onNavigationRequest: (NavigationRequest request) {
            // 只允许 http/https 链接
            if (request.url.startsWith('https://') || 
                request.url.startsWith('http://')) {
              return NavigationDecision.navigate;
            }
            return NavigationDecision.prevent;
          },
        ),
      )
      // 加载网页
      ..loadRequest(Uri.parse(widget.url));
  }

  /// 处理返回按钮
  Future<bool> _onWillPop() async {
    // 如果可以返回上一页
    if (await _controller.canGoBack()) {
      _controller.goBack();
      return false;
    }
    return true;
  }

  
  Widget build(BuildContext context) {
    return PopScope(
      canPop: false,  // 禁用默认返回行为
      onPopInvokedWithResult: (bool didPop, dynamic result) async {
        if (didPop) return;
        final shouldPop = await _onWillPop();
        if (shouldPop && context.mounted) {
          Navigator.of(context).pop();
        }
      },
      child: Scaffold(
        appBar: widget.showAppBar ? _buildAppBar() : null,
        body: Column(
          children: [
            // 加载进度条
            if (_isLoading)
              LinearProgressIndicator(
                value: _loadingProgress,
                backgroundColor: Colors.grey[200],
                valueColor: const AlwaysStoppedAnimation<Color>(
                  Color(0xFF6366F1),
                ),
              ),
            // WebView 主体
            Expanded(
              child: WebViewWidget(controller: _controller),
            ),
          ],
        ),
      ),
    );
  }

  PreferredSizeWidget _buildAppBar() {
    return AppBar(
      backgroundColor: Colors.white,
      elevation: 1,
      leading: IconButton(
        icon: const Icon(Icons.arrow_back, color: Color(0xFF1E293B)),
        onPressed: () async {
          if (await _controller.canGoBack()) {
            _controller.goBack();
          } else {
            if (context.mounted) Navigator.pop(context);
          }
        },
      ),
      title: Text(
        widget.title ?? '加载中...',
        style: const TextStyle(
          color: Color(0xFF1E293B),
          fontSize: 18,
          fontWeight: FontWeight.w600,
        ),
      ),
      actions: [
        IconButton(
          icon: const Icon(Icons.refresh, color: Color(0xFF1E293B)),
          onPressed: () => _controller.reload(),
        ),
        PopupMenuButton<String>(
          icon: const Icon(Icons.more_vert, color: Color(0xFF1E293B)),
          onSelected: _handleMenuAction,
          itemBuilder: (context) => [
            const PopupMenuItem(value: 'share', child: Text('分享')),
            const PopupMenuItem(value: 'open_browser', child: Text('浏览器打开')),
            const PopupMenuItem(value: 'copy_link', child: Text('复制链接')),
          ],
        ),
      ],
    );
  }

  /// 处理菜单操作
  void _handleMenuAction(String action) {
    switch (action) {
      case 'share':
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('分享功能开发中...')),
        );
        break;
      case 'open_browser':
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('浏览器打开功能开发中...')),
        );
        break;
      case 'copy_link':
        // 复制链接到剪贴板
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('链接已复制')),
        );
        break;
    }
  }
}

3.2 WebView 页面工厂

为了方便复用,创建一些常用的 WebView 页面:

/// WebView 页面工厂
/// 
/// 提供常用的 WebView 页面配置
class WebViewPages {
  // 常用页面 URL
  static const String privacyPolicy = 'https://example.com/privacy';
  static const String userAgreement = 'https://example.com/terms';
  static const String helpCenter = 'https://example.com/help';
  static const String promotionActivity = 'https://example.com/promotion';

  /// 构建 WebView 页面
  static Widget buildPage(String url, {String? title}) {
    return WebViewPage(url: url, title: title);
  }

  /// 隐私政策页面
  static Widget privacyPolicyPage() {
    return const WebViewPage(
      url: privacyPolicy,
      title: '隐私政策',
    );
  }

  /// 用户协议页面
  static Widget userAgreementPage() {
    return const WebViewPage(
      url: userAgreement,
      title: '用户协议',
    );
  }

  /// 帮助中心页面
  static Widget helpCenterPage() {
    return const WebViewPage(
      url: helpCenter,
      title: '帮助中心',
    );
  }

  /// 活动专区页面
  static Widget promotionPage() {
    return const WebViewPage(
      url: promotionActivity,
      title: '活动专区',
    );
  }
}

3.3 JavaScript 交互

Flutter 和 WebView 之间可以互相调用:

import 'package:flutter/dartz.dart';

class WebViewWithJSPage extends StatefulWidget {
  final String url;
  final String? title;

  const WebViewWithJSPage({
    super.key,
    required this.url,
    this.title,
  });

  
  State<WebViewWithJSPage> createState() => _WebViewWithJSPageState();
}

class _WebViewWithJSPageState extends State<WebViewWithJSPage> {
  late final WebViewController _controller;

  
  void initState() {
    super.initState();
    _initController();
  }

  void _initController() {
    _controller = WebViewController()
      ..setJavaScriptMode(JavaScriptMode.unrestricted)
      ..setNavigationDelegate(
        NavigationDelegate(
          onPageFinished: (url) {
            // 页面加载完成后,可以执行 JavaScript
            _controller.runJavaScript('''
              console.log('Flutter says: WebView loaded!');
            ''');
          },
        ),
      )
      ..loadRequest(Uri.parse(widget.url));
  }

  /// 调用 JavaScript 方法
  Future<void> _callJavaScript() async {
    // 调用网页中的 getUserInfo() 方法
    final result = await _controller.runJavaScriptReturningResult(
      'getUserInfo()',
    );
    debugPrint('JavaScript 返回: $result');
  }

  /// 向网页传递数据
  Future<void> _sendDataToWeb() async {
    // 传递 JSON 数据给网页
    await _controller.runJavaScript('''
      window.flutterData = {
        userId: '12345',
        token: 'abc123',
        timestamp: ${DateTime.now().millisecondsSinceEpoch}
      };
      window.onFlutterDataReceived && window.onFlutterDataReceived(window.flutterData);
    ''');
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(widget.title ?? 'WebView')),
      body: Column(
        children: [
          // 操作按钮
          Padding(
            padding: const EdgeInsets.all(16),
            child: Row(
              children: [
                ElevatedButton(
                  onPressed: _callJavaScript,
                  child: const Text('调用 JS'),
                ),
                const SizedBox(width: 16),
                ElevatedButton(
                  onPressed: _sendDataToWeb,
                  child: const Text('传数据给 JS'),
                ),
              ],
            ),
          ),
          // WebView
          Expanded(
            child: WebViewWidget(controller: _controller),
          ),
        ],
      ),
    );
  }
}

3.4 在路由中使用 WebView

// 在 app_router.dart 中添加
GoRoute(
  path: '/webview',
  name: 'webView',
  builder: (context, state) {
    final url = state.uri.queryParameters['url'] ?? 'https://example.com';
    final title = state.uri.queryParameters['title'];
    return WebViewPage(url: url, title: title);
  },
),

// 使用示例
// context.push('/webview?url=https://xxx.com&title=活动页面')

// 或者使用工厂方法
GoRoute(
  path: '/privacy',
  builder: (context, state) => WebViewPages.privacyPolicyPage(),
),

四、开发踩坑与挫折 😤

4.1 踩坑一:WebView 不显示

问题描述
页面一片空白,什么都不显示。

排查过程

  1. 检查 URL 是否正确
  2. 检查网络权限
  3. 检查 JavaScript 是否启用

解决方案

_controller = WebViewController()
  ..setJavaScriptMode(JavaScriptMode.unrestricted)  // 确保启用 JS
  ..loadRequest(Uri.parse(widget.url));

4.2 踩坑二:Android 11+ 无法加载 HTTP

问题描述
Android 11+ 系统默认禁止加载 HTTP 链接。

解决方案
android/app/src/main/AndroidManifest.xml 中添加:

<uses-permission android:name="android.permission.INTERNET"/>
<!-- 如果需要加载 HTTP(非 HTTPS),添加: -->
<application
  android:usesCleartextTraffic="true"
  ...>

4.3 踩坑三:页面返回问题

问题描述
按返回键直接退出 App,而不是返回 WebView 的上一页。

解决方案
使用 PopScope + canGoBack()

PopScope(
  canPop: false,
  onPopInvokedWithResult: (didPop, result) async {
    if (await _controller.canGoBack()) {
      _controller.goBack();
    } else if (context.mounted) {
      Navigator.of(context).pop();
    }
  },
  child: ...,
)

五、鸿蒙专属适配 🔧

5.1 鸿蒙设备权限配置

在鸿蒙设备的 module.json5 中配置网络权限:

{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.INTERNET"
      }
    ]
  }
}

六、最终实现效果 📸

(此处附鸿蒙设备上成功运行的截图)

在这里插入图片描述

在这里插入图片描述

七、个人学习总结 📝

通过 WebView 的学习,我收获了很多:

  1. ✅ 学会了如何在 Flutter 中嵌入 H5 页面
  2. ✅ 学会了 WebView 的各种配置
  3. ✅ 学会了 Flutter 和 JavaScript 的交互

作者:上海某高校大一学生,Flutter 爱好者
发布时间:2026年4月

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐