我正在参加「掘金·启航计划」
前言
Flutter与H5的结合
Flutter
是Google开发的一个跨平台的UI开发框架,同时也可以使用Dart编写本地代码,可以兼容各种移动端、Web和桌面端平台。H5是一种基于HTML
、CSS
、Javascript
的Web技术,广泛用于构建Web应用程序和网页。
在实际开发中,经常需要将Flutter
与H5
进行结合,以展现更加丰富的应用场景。其中,Flutter
中内嵌H5
页面并且实现与之通信,可以使应用更加交互性,体验性更好,同时也能极大的扩展应用的功能。
为什么要使用Flutter内嵌H5页面?
Flutter内嵌H5页面有以下几个优势:
- 可以快速实现Web应用中的某些功能,而无需将整个应用都重写为
Flutter
; - 可以出色地展示
H5
页面,以便将Web内容移植到App中; - 可以通过
H5
页面与Flutter
进行交互,获得更加流畅、快捷的用户体验。
综上所述,内嵌H5
页面是非常有必要的。
实现方式
Flutter中的WebView组件介绍
在Flutter
中,可以使用flutter_webview_plugin
库实现内嵌H5
页面的功能。这个库是Flutter
的一个用于Web
视图插件的第三方库,依赖于Android
和iOS
的原生Web View
。
安装该库:
dependencies: flutter_webview_plugin: ^0.4.0+1
这个库提供了两种方式加载Web视图:
WebviewScaffold
:显示一个工具栏,并在屏幕上方显示一个带有Web视图的Scaffold。Webview
:直接在Widget树中包含一个Web视图。
下面是一个简单的Webview
使用示例:
import 'package:flutter/material.dart'; import 'package:flutter_webview_plugin/flutter_webview_plugin.dart'; class MyWebView extends StatefulWidget { final String title; final String url; MyWebView({ @required this.title, @required this.url, }); @override _MyWebViewState createState() => _MyWebViewState(); } class _MyWebViewState extends State<MyWebView> { final flutterWebViewPlugin = FlutterWebviewPlugin(); @override void initState() { super.initState(); } @override void dispose() { flutterWebViewPlugin.dispose(); // 组件销毁时需要释放 super.dispose(); } @override Widget build(BuildContext context) { return WebviewScaffold( appBar: AppBar( title: Text(widget.title), ), url: widget.url, initialChild: Center( child: CircularProgressIndicator(), ), ); } }
这里就生成了一个简单的带Appbar
的Webview
视图。
WebView与H5页面通信方式的比较
在Flutter中,有两种方式实现Flutter与H5的通信:
JavascriptChannel
:通过Javascript
通信。MethodChannel
:通过建立Binder
通道和Android
原生代码通信。
JavascriptChannel
在需求短时较为直观、实现相对简单,MethodChannel
在处理较复杂的逻辑时,更加清晰明了。
Flutter与H5交互的核心技术WebViewJavascriptChannel
Flutter
中,内嵌H5
页面并且实现通信最为常见的方式就是使用WebViewJavascriptChannel
了。
关于WebViewJavascriptChannel
,以下是相关的介绍和使用示例。
WebViewJavascriptChannel
接口定义:
abstract class WebViewJavascriptChannel { void postMessage(String message); }
在Flutter
中,需要创建一个WebviewJavascriptChannel
对象,并将其提供给WebView
组件的构造器,其将在H5
中调用。
下面是一个简单的使用WebViewJavascriptChannel
的示例:
import 'package:flutter/material.dart'; import 'package:flutter_webview_plugin/flutter_webview_plugin.dart'; class MyWebView extends StatefulWidget { final String title; final String url; MyWebView({ @required this.title, @required this.url, }); @override _MyWebViewState createState() => _MyWebViewState(); } class _MyWebViewState extends State<MyWebView> { final flutterWebViewPlugin = FlutterWebviewPlugin(); TextEditingController controller = TextEditingController(); @override void initState() { super.initState(); flutterWebViewPlugin.onStateChanged.listen((WebViewStateChanged state) { print("onStateChanged: ${state.type} ${state.url}"); }); flutterWebViewPlugin.onUrlChanged.listen((String url) { print("onUrlChanged: $url"); setState(() { controller.text = url; }); }); flutterWebViewPlugin.onDestroy.listen((_) { print("onDestroy"); }); } @override void dispose() { flutterWebViewPlugin.dispose(); // 组件销毁时需要释放 super.dispose(); } void onPageFinished(BuildContext context) { final channel = MethodChannel('webViewJSChannel'); channel.setMethodCallHandler((MethodCall call) async { if (call.method == 'webToFlu') { String para = call.arguments; //处理接收的参数 print('MethodChannel接收到的内容:' + para); //回传数据到H5 flutterWebViewPlugin.evalJavascript( "jsFunction('${para}', '${DateTime.now().toString()}')"); } }); channel.invokeMethod('fluToWeb', {'msg': 'Flutter start!'}); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Column( children: [ TextField( controller: controller, readOnly: true, decoration: InputDecoration( hintText: 'current url', contentPadding: EdgeInsets.all(10.0), border: OutlineInputBorder( borderRadius: BorderRadius.circular(4.0), ), ), ), Expanded( child: WebviewScaffold( url: widget.url, hidden: true, initialChild: Container( color: Colors.white, child: Center( child: CircularProgressIndicator(), ), ), withJavascript: true, hiddenPage: true, appBar: AppBar( title: Text(widget.title), ), javascriptChannels: <JavascriptChannel>[ JavascriptChannel( name: 'FlutterEvent', onMessageReceived: (JavascriptMessage msg) { print(msg.message); }) ].toSet(), onPageFinished: onPageFinished, withZoom: false, ), ), ], ), ); } }
上面的示例中,在页面加载完成之后,我们首先通过回调方法onPageStarted
在Flutter
中创建了一个MethodChannel
,声明了Flutter
向H5
推送方法的名称和需要传递的数据。随后在H5
中,接收到Flutter
传递的初始化参数后,就可以通过window.Flutter.postMessage()
来向Flutter
发送数据。当Flutter
接收到H5
的消息之后,就可以通过MethodChannel
写入到Flutter
侧的方法,进行处理。
下面是一个Web
页面示例:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>test channel</title> </head> <body> <button id='btn'>Click me to send message to Flutter</button> <p>Tips: 点击按钮即可通过JS打印Hello World并通过MethodChannel发送到Flutter。</p> <script> var channel = null; window.addEventListener("load", function() { channel = new WebViewJavascriptChannel(bridge); }); function bridge(msg) { document.getElementById('log').innerHTML = msg; var para = "Hello World"; channel.postMessage(JSON.stringify({'type':'channelTest', 'content': para})); } function jsFunction(para1, para2) { console.log('接收到的内容:' + para1); console.log('当前时间:' + para2); } document.getElementById('btn').addEventListener('click', function(e) { console.log('clicked!'); console.log(channel); bridge('msg from h5'); }, false); </script> </body> </html>
这个页面中,我们声明了通信的JavaSript Channel
,并通过按钮点击事件完成数据的发送,同时在jsFunction()
方法中,通过flutterWebViewPlugin.evalJavascript()
方法向H5页面回传数据。
实际应用
在Flutter中实现微信、支付宝等第三方授权登陆
在开发中,经常需要使用到微信、支付宝等第三方授权登陆的功能,我们可以在Flutter
中内嵌H5
页面,通过H5
页面来完成授权登陆操作,然后回传数据到Flutter
中进行后续的操作。
以下是一份简单的微信授权登陆的示例代码:
import 'package:flutter/material.dart'; import 'package:flutter_webview_plugin/flutter_webview_plugin.dart'; class WechatAuthorizationPage extends StatefulWidget { @override _WechatAuthorizationPageState createState() => _WechatAuthorizationPageState(); } class _WechatAuthorizationPageState extends State<WechatAuthorizationPage> { final flutterWebViewPlugin = FlutterWebviewPlugin(); String _url = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=wxe15b267e25e3f502&redirect_uri=https%3A%2F%2Fs.jianyi-ai.com%2Fapi%2Fselfdiagnose%2Fv1%2Fwechat%2Fcallback&response_type=code&scope=snsapi_userinfo&state=state&connect_redirect=1#wechat_redirect"; @override void initState() { super.initState(); flutterWebViewPlugin.onStateChanged.listen((WebViewStateChanged state) { print("onStateChanged: ${state.type} ${state.url}"); }); flutterWebViewPlugin.onUrlChanged.listen((String url) { print("onUrlChanged: $url"); }); flutterWebViewPlugin.onDestroy.listen((_) { print("onDestroy"); }); } @override void dispose() { flutterWebViewPlugin.dispose(); // 组件销毁时需要释放 super.dispose(); } void onPageFinished(BuildContext context) { final channel = MethodChannel('webViewJSChannel'); channel.setMethodCallHandler((MethodCall call) async { if (call.method == 'webToFlu') { Map para = json.decode(call.arguments); print("onPageFinished-para=$para"); if (para.containsKey('code')) { // do something with the code } else { // do something with the error } } }); flutterWebViewPlugin.evalJavascript( 'setTimeout(function () { Flutter.postMessage(JSON.stringify({type:"pageLoaded", content:{}})); }, 1000)'); channel.invokeMethod('fluToWeb', {'msg': 'Flutter start!'}); } void onStateChanged(BuildContext context, WebViewStateChanged state) { print( '[INFO]onStateChanged: ${state.type} ${state.url} ${state.canGoBack} ${state.canGoForward}'); if (state.type == WebViewState.finishLoad) { flutterWebViewPlugin.evalJavascript( 'setTimeout(function () { Flutter.postMessage(JSON.stringify({type:"pageLoaded", content:{}})); }, 1000)'); } if (state.type == WebViewState.abortLoad) { // _progressBarVisibility = false; } if (state.type == WebViewState.startLoad) { // _progressBarVisibility = true; } } @override Widget build(BuildContext context) { return Container( child: WebviewScaffold( url: _url, hidden: true, initialChild: Container( color: Colors.white, child: Center( child: CircularProgressIndicator(), ), ), withJavascript: true, hiddenPage: true, appBar: AppBar( title: Text('微信授权登陆'), ), javascriptChannels: <JavascriptChannel>[ JavascriptChannel( name: 'FlutterEvent', onMessageReceived: (JavascriptMessage msg) { print(msg.message); }) ].toSet(), onPageFinished: onPageFinished, withZoom: false, //allowFileAccess: true, userAgent: "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/37.0.0.0 Mobile Safari/537.36", ), ); } }
在H5中与Flutter原生端交互:与Flutter全局变量通信、调用原生组件
在H5
页面中与Flutter
原生端进行交互也是非常重要的。在H5
中可以通过window
调用Flutter
的API
,以便获取Flutter
全局的变量或者调用原生组件进行处理。
在这里我们介绍两个示例:
1.与Flutter全局变量通信
首先需要在Flutter
中注册一个通信事件,例如我们可以创建一个名为globalEvent
的事件,用于H5
传递数据到Flutter
全局变量。
import 'package:flutter/services.dart'; const MethodChannel _channel = MethodChannel('globalEvent'); void main() { _channel.setMethodCallHandler((MethodCall call) async { if(call.method == 'setGlobalVariable'){ // 在这里处理H5传递的数据 String data = call.arguments; globalVariable = data; } }); } // 声明一个全局变量 String globalVariable = '';
接着,在H5
中通过window
对象调用我们注册的事件globalEvent
,将数据传递过来。
// 在H5中传递数据给Flutter window.flutter_inject_global_variable = function(data) { window.flutter_inject_global_variable_callback(data); }; window.flutter_inject_global_variable_callback = function(data) { window.flutter_inject_global_variable_callback = null; } // H5传递数据给Flutter全局变量 window.flutter_inject_global_variable('Hello Flutter!');
当H5调用了window.flutter_inject_global_variable
方法时,Flutter
会接收到一个名为setGlobalVariable
的事件,并将H5
传递的数据存储到全局变量globalVariable
中。
- 调用原生组件
Flutter
中可以通过MethodChannel
调用原生组件,同时需要在原生端注册一个Flutter
的通信事件,例如我们创建一个名为callNativeComponent
的事件。
import 'package:flutter/services.dart'; const MethodChannel _channel = MethodChannel('callNativeComponent'); void main() { _channel.setMethodCallHandler((MethodCall call) async { if(call.method == 'showDialog'){ // 在这里调用原生组件 } }); } // 声明一个全局变量 String globalVariable = '';
接着,在H5
中通过window
对象调用callNativeComponent
事件,使Flutter
调用原生组件进行处理。
// H5中调用原生组件 window.call_native_component = function(type, data) { var params = JSON.stringify({ "type": type, "data": data }); return window.flutter_call_native_component(params); }; // 在H5中调用原生组件 window.call_native_component('show_dialog', {'content': 'Hello Native!'});
当H5
调用window.call_native_component
方法时,Flutter接收到名为showDialog
的事件并使用原生组件处理传递的数据。原生端同时需要实现MethodChannel
的相应方法,例如showDialog
方法。
总结与展望
在本文中,我们介绍了Flutter
内嵌H5
页面的实现方式和与前端通信的技术。通过使用Flutter
的flutter_webview_plugin
插件,我们能够很方便地在Flutter
应用中内嵌H5
页面,并且通过JavaScript Channel
机制实现与前端的通信。
在前端与Flutter
应用之间实现无缝的交互可以带来很多的好处。例如,在Flutter
应用中使用H5
页面可以方便地引入第三方的页面和组件,并且可以快速构建复杂的组合界面。同时,基于Flutter
强大的性能和Flutter Widget
的可组合性,可以实现更加复杂和优雅的界面效果。
不过,在实际的开发过程中,还需要注意一些问题。例如,Flutter
和H5
之间图形渲染的差异和性能问题,以及在调试方面可能遇到的困难。同时,由于Flutter
和H5
都在不断发展中,未来仍然需要持续关注技术的变化和趋势。不过随着Flutter
应用的普及和技术的不断进步,我们相信Flutter
内嵌H5
页面将在移动应用开发中发挥更加重要和广泛的作用。
总之,在实际的项目中,应根据具体情况选择使用Flutter
内嵌H5
页面的方案。在合适的场景下,该技术能够帮助我们更好地利用各类资源,构建更加高效和优秀的移动应用。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】