Skip to content

Flutter 接入 ​

接入方式 ​

使用 flutter_inappwebview 创建 WebView,通过以下两个入口进行交互:

方向H5 代码Flutter 代码
H5 调 NativehtmlToFlutterEvent.postMessage(JSON字符串)addJavaScriptHandler 接收,onHtmlToFlutterEvent 分发并返回结果
Native 调 H5flutterToHtmlEvent(对象) 接收并返回结果evaluateJavascript 执行,读取 H5 返回值

以下示例按 flutter_inappwebview: 6.1.5 编写。Flutter 直接使用插件自带的 Promise 通道,无需下载额外的请求配对脚本。观看地址及账号打通见 接入总览,消息字段见 交互协议。

一、初始化 WebView 示例 ​

在 pubspec.yaml 的 dependencies 中添加依赖,配置示例:

yaml
flutter_inappwebview: 6.1.5

下列页面负责注册通道、注入 H5 入口及配置 UA。后续各节中的 Dart 方法均放入同一个 _LiveH5PageState,共同组成页面实现。

dart
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';

class LiveH5Page extends StatefulWidget {
  const LiveH5Page({super.key, required this.watchUrl});
  final String watchUrl;

  @override
  State<LiveH5Page> createState() => _LiveH5PageState();
}

class _LiveH5PageState extends State<LiveH5Page> with WidgetsBindingObserver {
  InAppWebViewController? _controller;
  String _title = '直播间';
  bool _loaded = false;

  static const bridgeScript = r'''
    (function() {
      window.htmlToFlutterEvent = window.htmlToFlutterEvent || {};
      window.htmlToFlutterEvent.postMessage = function(msg) {
        var payload = typeof msg === 'string' ? msg : JSON.stringify(msg);
        if (window.flutter_inappwebview &&
            typeof window.flutter_inappwebview.callHandler === 'function') {
          return window.flutter_inappwebview.callHandler('htmlToFlutterEvent', payload);
        }
        return Promise.resolve(null);
      };
    })();
  ''';

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
  }

  @override
  Widget build(BuildContext context) {
    final platform = Platform.isIOS ? 'iOS' : 'Android';
    return Scaffold(
      appBar: AppBar(title: Text(_title)),
      body: InAppWebView(
        initialUrlRequest: URLRequest(url: WebUri(widget.watchUrl)),
        initialSettings: InAppWebViewSettings(
          javaScriptEnabled: true,
          allowsInlineMediaPlayback: true,
          mediaPlaybackRequiresUserGesture: false,
          applicationNameForUserAgent: 'channel/APP$platform version/1.0.0 '
              'package/com.example.live wxappid/example_wechat_appid scheme/examplelive',
        ),
        onWebViewCreated: (controller) async {
          _controller = controller;
          controller.addJavaScriptHandler(
            handlerName: 'htmlToFlutterEvent',
            callback: (args) async {
              if (args.isEmpty || args.first is! String) return null;
              return onHtmlToFlutterEvent(args.first as String);
            },
          );
          await controller.evaluateJavascript(source: bridgeScript);
        },
        onLoadStart: (controller, url) { _loaded = false; },
        onLoadStop: (controller, url) async {
          await controller.evaluateJavascript(source: bridgeScript);
          _loaded = true;
        },
      ),
    );
  }

  // 将后文的事件接收、事件发送和生命周期方法放在此处。

  @override
  void dispose() {
    _loaded = false;
    WidgetsBinding.instance.removeObserver(this);
    _controller?.removeJavaScriptHandler(handlerName: 'htmlToFlutterEvent');
    _controller = null;
    super.dispose();
  }
}

UA 在首次加载前设置,应用名、版本、包名、微信 AppID 和 scheme 使用自己的配置。页面完整导航会重建 JavaScript 上下文,因此除创建时注入外,还应在每次加载完成后重新注入。仅在可信观看页面提供桥接能力,实际项目还需按观看域名限制导航。

二、H5 调 Native示例:查询应用信息、修改标题、打开页面 ​

1. H5 发起调用并读取结果示例 ​

H5 使用的 postMessage 是上一步注入的方法;其返回的 Promise 直接来自插件的 callHandler。

javascript
async function callNative(event, data) {
  if (!window.htmlToFlutterEvent ||
      typeof window.htmlToFlutterEvent.postMessage !== 'function') {
    throw new Error('App 通道尚未就绪');
  }
  var response = await window.htmlToFlutterEvent.postMessage(JSON.stringify({
    event: event,
    data: data
  }));
  if (response == null) throw new Error('App 通道尚未就绪');
  var result = typeof response === 'string' ? JSON.parse(response) : response;
  if (Number(result.code) !== 200) throw new Error(result.message || 'App 调用失败');
  return result.data;
}

// 在 H5 确认桥接就绪后调用,例如用户点击页面按钮时。
async function onQueryAppInfoClick() {
  var info = await callNative('getAppInfo', {});
  console.log(info.appName, info.version, info.platform);
}

async function onChangeTitleClick() {
  await callNative('setAppBarTitle', '活动直播间');
}

async function onOpenNativePageClick() {
  await callNative('openNativePage', {
    page: 'orderDetail',
    params: { orderId: 'order_12345' }
  });
}

2. Flutter 接收、分发并返回 JSON 字符串示例 ​

将方法放入第一节的 _LiveH5PageState。示例处理 getAppInfo、setAppBarTitle,并通过自定义事件 openNativePage 打开 Flutter 路由页面。实际应用信息及目标页面请替换为项目实现。

dart
Future<String> onHtmlToFlutterEvent(String message) async {
  Map<String, dynamic> request;
  try {
    request = Map<String, dynamic>.from(jsonDecode(message) as Map);
  } catch (_) {
    return jsonEncode({'code': 400, 'message': 'JSON 解析失败', 'data': null});
  }
  final event = (request['event'] ?? '').toString();
  final result = <String, dynamic>{
    'event': event,
    'timestamp': DateTime.now().toIso8601String(),
    'code': 200,
    'message': '',
    'data': null,
  };
  switch (event) {
    case 'getAppInfo':
      result['data'] = {
        'appName': '示例应用', 'packageName': 'com.example.live',
        'version': '1.0.0', 'buildNumber': '1',
        'platform': Platform.isIOS ? 'iOS' : 'Android',
        'scheme': 'examplelive://',
      };
      break;
    case 'setAppBarTitle':
      if (mounted) setState(() => _title = (request['data'] ?? '').toString());
      break;
    case 'openNativePage':
      final data = request['data'];
      final params = data is Map ? data['params'] : null;
      final orderId = params is Map ? params['orderId'] : null;
      if (data is! Map || data['page'] != 'orderDetail' ||
          orderId is! String || orderId.trim().isEmpty) {
        result['code'] = 400;
        result['message'] = '页面或订单参数错误';
      } else if (!mounted) {
        result['code'] = 500;
        result['message'] = '当前页面已关闭';
      } else {
        try {
          // 演示页面;替换为接入项目的订单详情页面。
          // 不等待 push 返回的 Future,它在目标页面退出时才完成。
          Navigator.of(context).push<void>(MaterialPageRoute<void>(
            settings: RouteSettings(name: '/order/detail', arguments: {'orderId': orderId}),
            builder: (context) => Scaffold(
              appBar: AppBar(title: const Text('订单详情')),
              body: Center(child: Text('订单号:$orderId')),
            ),
          ));
          result['data'] = {'page': 'orderDetail'};
        } catch (_) {
          result['code'] = 500;
          result['message'] = '无法打开 App 页面';
        }
      }
      break;
    default:
      result['code'] = 400;
      result['message'] = '未知事件';
  }
  final responseText = jsonEncode(result);
  // 兼容历史 callback;callback 与 Promise 均可返回,业务方选择一个消费。
  final callback = (request['callback'] ?? '').toString();
  final validPath = RegExp(r'^(?:window\.)?[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$');
  final forbidden = callback.split('.').any(
    (part) => const ['__proto__', 'prototype', 'constructor'].contains(part),
  );
  if (callback.isNotEmpty && validPath.hasMatch(callback) && !forbidden) {
    // 将 JSON 结果再次编码成 JS 字符串字面量,正确处理引号及换行。
    try {
      await _controller?.evaluateJavascript(
        source: '$callback(${jsonEncode(responseText)});',
      );
    } catch (_) {
      // callback 失败时仍通过 Promise 返回本次结果。
    }
  }
  return responseText;
}

查询应用信息时,H5 读取到的结果示例:

json
{
  "event": "getAppInfo",
  "timestamp": "2026-11-02T16:00:00.000",
  "code": 200,
  "message": "",
  "data": {
    "appName": "示例应用",
    "packageName": "com.example.live",
    "version": "1.0.0",
    "buildNumber": "1",
    "platform": "Android",
    "scheme": "examplelive://"
  }
}

需要兼容 callback 时,H5 调用示例(回调参数也是 JSON 字符串):

javascript
window.globalCallback = function (responseText) {
  console.log(JSON.parse(responseText));
};
window.htmlToFlutterEvent.postMessage(JSON.stringify({
  event: 'getAppInfo', data: {}, callback: 'globalCallback'
}));

三、Native 调 H5示例:获取视频尺寸、发送前后台通知 ​

1. Flutter 执行 H5 函数并读取返回值示例 ​

将此方法放入同一个 State,通过 flutterToHtmlEvent 发送消息,兼容插件返回 Map 或 JSON 字符串。

dart
Future<Map<String, dynamic>?> flutterToHtmlEvent(String event, {dynamic data}) async {
  if (!_loaded || _controller == null) return null;
  final message = {
    'event': event,
    'timestamp': DateTime.now().toIso8601String(),
    'data': data,
  };
  dynamic result = await _controller!.evaluateJavascript(
    source: 'window.flutterToHtmlEvent && '
        'window.flutterToHtmlEvent(${jsonEncode(message)});',
  );
  if (result is String && result.isNotEmpty) {
    try { result = jsonDecode(result); } catch (_) { return null; }
  }
  if (result is Map) {
    return result.map((key, value) => MapEntry(key.toString(), value));
  }
  return null;
}

调用示例,同样放入 State,在页面加载完成后按需触发:

dart
Future<void> queryVideoSize() async {
  final result = await flutterToHtmlEvent('getVideoInfo');
  if (result == null || result['code'].toString() != '200') return;
  final video = result['data'] as Map;
  final width = video['width'];
  final height = video['height'];
  debugPrint('H5 视频尺寸:$width x $height');
}

Future<void> notifyAppResumed() async {
  await flutterToHtmlEvent('appLifecycle', data: 'resumed');
}

2. H5 接收 Native 事件并返回结果示例 ​

现有直播间 H5 已实现 flutterToHtmlEvent,App 直接调用即可。下面是自建测试 H5 的对应实现,用来说明 H5 如何接收和返回;不要注入覆盖现有直播间的处理函数。

javascript
window.flutterToHtmlEvent = function (message) {
  var result = {
    event: message.event,
    timestamp: Date.now(),
    code: 200,
    message: '',
    data: null
  };
  switch (message.event) {
    case 'getVideoInfo':
      var video = document.querySelector('video');
      if (!video) {
        result.code = 401;
        result.message = '播放器尚未就绪';
      } else {
        result.data = { width: video.videoWidth, height: video.videoHeight };
      }
      break;
    case 'appLifecycle':
      if (message.data === 'resumed') console.log('App 回到前台,可恢复页面业务');
      break;
    case 'pageLifecycle':
      if (message.data === 'dispose') console.log('容器即将关闭,在此清理页面资源');
      break;
    default:
      result.code = 400;
      result.message = '未知事件';
  }
  return result; // 返回对象,供 evaluateJavascript 读取。
};

这一方向的请求直接传入 JavaScript 对象;H5 返回对象,Flutter 读取执行结果。timestamp 在 App 消息中是时间字符串,在上述 H5 返回值中是毫秒时间戳,调用方不要混用两者类型。

四、App 生命周期和页面关闭示例 ​

将以下方法放入同一个 State。关闭入口应调用 closePage(),发送 dispose 并等待 JavaScript 执行完成后再关闭路由;系统返回键也应接入该关闭流程。

dart
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
  flutterToHtmlEvent('appLifecycle', data: state.name);
}

Future<void> closePage() async {
  await flutterToHtmlEvent('pageLifecycle', data: 'dispose');
  if (mounted) Navigator.of(context).pop();
}

页面被其他路由覆盖、重新显示时,按实际路由事件发送 didPushNext、didPopNext。关闭前通知放在关闭流程中,dispose() 用于最终释放监听和控制器引用。

联调检查 ​

  1. H5 调 getAppInfo,检查 Dart 收到的 JSON、返回字符串及 H5 解析结果。
  2. H5 调 setAppBarTitle,确认原生导航标题变化;调用 openNativePage,确认订单参数到达 Flutter 页面并及时收到跳转响应。
  3. Flutter 调 getVideoInfo,确认 H5 收到对象并返回视频宽高。
  4. App 切换前后台,确认 H5 收到 appLifecycle;关闭前收到 pageLifecycle / dispose。
  5. 刷新和完整导航后重复以上步骤,再补齐账号、分享、支付等实际业务事件。