Appearance
Flutter 接入
接入方式
使用 flutter_inappwebview 创建 WebView,通过以下两个入口进行交互:
| 方向 | H5 代码 | Flutter 代码 |
|---|---|---|
| H5 调 Native | htmlToFlutterEvent.postMessage(JSON字符串) | addJavaScriptHandler 接收,onHtmlToFlutterEvent 分发并返回结果 |
| Native 调 H5 | flutterToHtmlEvent(对象) 接收并返回结果 | 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() 用于最终释放监听和控制器引用。
联调检查
- H5 调
getAppInfo,检查 Dart 收到的 JSON、返回字符串及 H5 解析结果。 - H5 调
setAppBarTitle,确认原生导航标题变化;调用openNativePage,确认订单参数到达 Flutter 页面并及时收到跳转响应。 - Flutter 调
getVideoInfo,确认 H5 收到对象并返回视频宽高。 - App 切换前后台,确认 H5 收到
appLifecycle;关闭前收到pageLifecycle / dispose。 - 刷新和完整导航后重复以上步骤,再补齐账号、分享、支付等实际业务事件。