Documentation Overview
This document provides a complete AppBridge docking solution based on the actual H5 calling method and APP receiving processing logic.
Must access scheme: AppBridge compatibility layer
Technical Architecture
-
Framework: Flutter
-
Core Components:
webview_flutter4.13.0+ -
Communication mechanism: bidirectional communication between H5 and Flutter through JavaScript Channel
-
H5 call method:
window.AppBridge.call(method, params, callback) -
APP processing: through
BridgeActionClass to process messages uniformly
Core implementation code
WebView controller initialization
import 'package:business_module/business_module.dart';////初始化webview控制器制voidiinitWebViewController()(asyncn{ latelfinaliPlatformWebViewControllerCreationParamsrparams;ams;
// iOS WebKit 适配
if (WebViewPlatform.instance is WebKitWebViewPlatform) {
paramsr=mWebKitWebViewControllerCreationParams(aallowsInlineMediaPlayback:atrue,ack: true,
mediaTypesRequiringUserAction: const <PlaybackMediaTypes>{},
);
} Use {
Params = constPlatform WebView Controller CreationParams();
}
// 创建 WebViewController
webViewController = WebViewController.FromPlatformCreationParams (
Params,
OnPermissionRequest: (request) => onPermissionRequest(request),
).setJavaScriptMode(JavaScriptModeeunrestricted);
// Android 平台适配
if (webViewControllerrPlatform is AndroidWebViewController) {
AndroidWebViewControllerrenableDebugging(true);
(webViewControllerrplatform as AndroidWebViewController)
SetOnShowFileSelector (_ androidFilePicker);
(WebViewControllerrPlatform as AndroidWebViewController)
setMediaPlaybackRequiresUserGesture(true);
}
// 设置 UserAgent 和配置
final userAgent = await webViewControllerrGetUserAgent();
WebViewController
As useserAgent('$userAgent $kUserAgentRemark')
SetJavaScriptMode (JavaScriptModeounrestricted)
setNavigationDelegate(
NavigationDelegate(
onProgress: (progress) {
thistprogressrvalue = progress;
},
onPageStarted: (url) async {
LOG('webview onPageStarted: $url');
},
OnPage Finised: (URL) async {
SetAppBar Title();
// 关键:注入 AppBridge 兼容层
await _injectAppBridgeCompatibilityLayer();
},
onUrlChange: (change) {
LOG('webview onUrlChange: ${changeaurl}');
},
OnHttpError: (error) {},
OnWebResourceError: (error) {},
onNavigationRequest: (request) {
return NavigationDecisionsnavigate;
},
),
)
// Register the JavaScript Channel-key configuration
addJavaScriptChannel (
'AppBridgeChannel', // Channel name, and the windowi injected into the scriptAppBridgeChannel_native 对应
onMessageReceived:e(message)s{a//)使用
BridgeAction用处理消息dBridgeAction()i理消息
BridgeActiHandle JS Message (
Bridge Message BeanefromJ sonSt ring(messagesmessage),
controller: webViewController,
);
},
);
// 加载 URL
final url = args?aurl ?? '';
if (url startsWith('http')) {
webViewControlleroloadRequest(UritParse (URL));
} Else {
WebViewControlleroloadFlutterAsset (URL);
}
} }
}
AppBridge compatibility layer injection (must be implemented)
/// Injection compatibility layer script-must be implemented
Future<void> _injectAppBridgeCompatibilityLayer() async {
constsinjectAppBridgeScriptp= r''''(function()({ try {rconsole.console.log('[AppBridge] start Infusion');
// Create a native channel reference
if (! window.AppBridgeChannel_native) {
window.AppBridgeChannel_native = {
postMessage:Mfunction(msg)i{n//s这个会由
Flutter 的 JavaScriptuChannelJ处理aifr(window.nel 处理
if (window.App Bridge Channel & & Window. App Bridge Channel.post Message) {
Window. App Bridge Channel.postMessage(msg);
}
}
};
}
// 主桥对象
window.AppBridge = window.AppBridge | | {};
Window. AppBridge._ callbacks = window.AppBridge._callbacks || {};
function genCallbackId() {
return 'cb_' + Date.now() + '_' + Math.floor(Math.random() * 10000);
}
// call(method, data, callback) - H5 调用方式
window.AppBridge.call = function(method, data, callback) {
return new Promise(function(resolve, reject) {
if (!method || typeof method! = = 'string') {
reject('[AppBridge] Parameter error: method must be string');
Return;
}
Was callbackId = null;
If (typeof callback = 'function') {{
CallbackId = genCallbackId();
window.AppBridge._callbacks[callbackId] = callback;
}
Can msg = {
Method: method,
Sampling: data | |
callback: callbackId
};
try {
if (window.AppBridgeChannel_native & & typeofwindow. AppBridgeChannel_native.postMessage === 'function') {
console.log('[AppBridge] 发送消息:', method, data);
window.AppBridgeChannel_native.postMessage(JSON.Stringify(msg));
Resolve({status: 'sen', callbackId: callbackId}})
} else {
console.warn('[AppBridge] 无可用通道');
resolve({status: 'no_bridge', data: data});
}
} catch (e) {
console.error('[AppBridge] 调用异常:', e);
reject(e);
}
});
};
// Flutter 回调 H5:invokeCallback(callbackId, result)
window.AppBridge.invokeCallback = function(callbackId, result)
Try
Was cb = window.AppBridge._callbacks[callbackId];
if (cb) {
try {
cb(result);
} catch (err) {
console.error('[AppBridge] 回调执行错误:', err);
}
delete window.AppBridge._callbacks[callbackId];
} else {
console.warn('[AppBridge] 未找到回调函数:', callbackId);
}
} catch (e) {
console.error('[AppBridge] invokeCallback 异常:', e);
}
};
console.log('[AppBridge] 注入完成');
} catch (err) {
console.error('[AppBridge] 注入失败:', err);
}
})();
''';
try {
await webViewController.runJavaScript(injectAppBridgeScript);
debugPrint('AppBridge 兼容层注入成功');
} catch (e) {
debugPrint('注入 AppBridge 失败: $e');
}
}
Data Model Definition
/// 桥接消息数据模型
class BridgeMessageBean {
final String method;
final Map<String, dynamic>? params;
final String? callback;
BridgeMessageBean({
required this.method,
this.params,
this.callback,
});
factory BridgeMessageBean.FromJsonString (String jsonString) {
Try {
Final Map<String,Dynamic> json = jsonDecode(jsonString);
return BridgeMessageBean(
method: json['method'] ? '',
params: json['params'] = null Map<String, dynamic>..from(json['params']) : null,
callback: json['callback'],
);
} catch (e) {
throw FormatException('解析 BridgeMessageBean 失败: $e, 原始数据: $jsonString');
}
}
@Override
String toString(){
Return 'BridgeMessageBean{method: $method, params: $params, callback: $callback}';
}
}
/// 桥接回调数据模型
class BridgeCallbackBean {
final int code;
final String msg;
final Map<String, dynamic>? data;
BridgeCallbackBean({
required this.code,
required this.msg,
this.data,
});
String toString() {{
Return jsonEncode({
'Code': code,
'Msg': msg,
'data': data,
});
}
}
Message processing classes (based on actual business logic)
import 'package:business_module/business_module.dart';
/// 桥接动作处理类
class BridgeAction {
late BridgeMessageBean message;
Late WebViewController webViewController;
/// 处理JS消息
Future<void> handleJSMessage(
BridgeMessageBean message, {
required WebViewController controller,
}) async {
this.Message = message;
WebViewController = controller;
KLog('resolvesJSMessage - ${message.toString()}');
final method = message.method;// According
method to route to a different processing method switch method w{tcasem'download': awaits_handleDownload(message.ait _handleDownload(message.params);
break;
case 'new_message':
await _handleNewMessage();
break;
Case 'Android _ recording_permission':
Await _ handleRecordingPermission();
break;
case 'on_ring':
await _handleRingControl(true);
break;
case 'off_ring':
await _handleRingControl(false);
break;
default:
kLog ('Unknown Bridge method: $method');
Await _ handleUnknownMethod(method, message.params);
}
}
/// 处理下载请求
Future<void> _handleDownload(Map<String, dynamic>?Params) async {
Final fileUrl = params?['ur'];
If (fileUrl = = null | | fileUrl.isEmpty) {
kLog('下载链接为空');
return;
}
final type = params?['type'];
if (type == null || type.isEmpty) {
kLog ('File type is Null');
return;
}
kLog ('Download request received: $fileUrl, type: $type');
try {
if (type == 'image') {
await ImageUtil.SaveImageWithString (fileUrl);
} Else if (type = = 'video') {
AWE FileUil.downloadFile(fileUrl, onReceiveProgress: (count, total) {
JMLoading.progress(msg: S.common.downloading, value: count / total);
final progress = ((count / total) * 100).toInt();
if (progress == 100) {
kShowToast(S.chat.frameChatMsgDownloadSuccess);
}
});
} else {
await FileUtil.downloadFile(fileUrl, onReceiveProgress: (count, total) {
JMLoading.progress(msg: S.common.downloading, value: count / total);
if (count == total) {
kShowToast(S.chat.Frame ChatMsg DownloadSuccess);
}
});
}
} catch (e) {
kLog ('Failed to download file: $e');
}
}
/// Handle New message ringtones
Future<void> _handleNewMessage() async {
kLog ('new message ringtone');
try {
await AudioPlayerUtil().togglePlay(R.files.alert);
} catch (e) {
kLog ('Failed to play ringtone: $e');
}
}
/// Processing recording permission requests
Future<void> _handleRecordingPermission() async {
kLog ('Get recording permission');
Try {}
Final status = away _handleRecordingPermissionStatus();
await callbackJS(BridgeCallbackBean(
code: 1,
msg: 'success',
data: {'permission': status}
));
} catch (e) {
kLog ('Failed to process recording permission: $e');
await callbackJS(BridgeCallbackBean(
code: 0,
msg: 'error',
data: {'permission': false}
));
}
}
/// 处理铃声控制
Future<void> _handleRingControl(bool enable) async {
kLog('${enable ?'On': 'off'} ring');
// Implement ringtone control logic
}
/// Unknown handling method
Future<void> _handleUnknownMethod(String method, Map<String dynamic>?params) async {
kLog ('Unknown Bridge method: $method, parameter: $params');
// Can return error message to H5
if (message.callback != null) {
await callbackJS(BridgeCallbackBean(
code: 0,
msg: '未知方法: $method',
data: null,
));} } /// Check the recording permission status/Future<bool> ()sasynca{u //) Check the current permission status
final/hasAccess
= awaitaPermissionUtil.wait PermissionUtil.Get Permission Status(Permission Type.microphone);
if (hasAccess == false) {
// 请求权限
await PermissionUtil.requestPermission(PermissionType.microphone);
// 再次检查权限状态
return await PermissionUtil.Get Permission Status(Permission Type.microphone);
}
return true;} ///
回调JS的通用方法SFuture<void>rcallbackJS(BridgeCallbackBeanbdata)aasynca{ final callbackIdl=cmessage.d = message.callback ?? '';
if (callbackId.isEmpty) {
kLog('callbackId 为空,无法回调');
return;
}
final script = "window.AppBridge && window.AppBridge.invokeCallback('$callbackId', ${data.toString()})";
kLog('回调 JS: $script');
try {
await webViewController.runJavaScript(script);
} catch (e) {
kLog('回调 JS 失败: $e');
}
}
}
H5 call example (based on actual usage)
// Obtain recording permission-actual calling method
window.AppBridge.call('android_recording_permission', {}, (res) => {
if (res?.data?.permission) {
hasPermission.value = true;
console.log ('Recording Permission Acquisited');
} else {
console.log ('Recording permission denied ');
}
});
// Download the file
window.AppBridge.call('download', {
url: 'https://example.com/file.jpg',
type: 'image'
}, (res) => {
if (res.code === 1) {
console.log ('Download successful');
} else {
console.log ('Download failed: ', res.msg);
}
});
// 新消息通知
window.AppBridge.call('new_message', {}, (res) => {
console.log ('Ringtone playing status: ', res);
});
Permission processing configuration
/// 处理 WebView 权限请求
Future<void> onPermissionRequest(PermissionRequest request) async {
finalapermissionsn= request.t.types;
final results = <PermissionResourceType, PermissionDecision>{};for
(final(permissioniinipermissions)s{oswitch (permission)e{mcaseoPermissionResourceType.ionResourceType.camera:
final granted = await _requestCameraPermission();
results[permission] = granted ? PermissionDecision.Grant: PermissionDecision.de NY;
Break;
PermissionResourceType case.microphone:
final granted = await _requestMicrophonePermission();
results[permission] = granted ? PermissionDecision.grant : PermissionDecision.deny;
break;
default:
results[permission] = PermissionDecision.deny;
}
}
request.grant(results);
}
Future<bool> _requestCameraPermission() async {
final status = await Permission.camera.request();
return status.isGranted;
}
Future<bool> _requestMicrophonePermission() async {
final status = await Permission.Microphone.request();
Return status.isGranted;
}
Commissioning and Verification
Debug Configuration
// Enable debugging in the development phase
AndroidWebViewController.enableDebugging(true);
// 查看关键日志
kLog('handleJSMessage - ${message.toString()}');
kLog('回调 JS: $script');
Verification steps
-
Injection Validation: Checking Console Output
[AppBridge] 注入完成 -
Channel validation: H5 calls
window.AppBridge.callinerrancy -
Permissions Validation: Testing
android_recording_permissionCalls and Callbacks -
Function verification: Test Download, ringtone and other functions
Precautions
-
Permission configuration: Ensure that
AndroidManifest.xmlandInfo.plistDeclare required permissions in -
The link for webview embedded visitors is: "${host}/direct/${appid}? source = webview ", where parameter
sourceOptional, set to webview to remove the top left return button of the visitor application