1. 前言
依托于与 Skia 的深度定制及优化,Flutter 给我们提供了很多关于渲染的控制和支持,能够实现绝对的跨平台应用层渲染一致性。但对于一个应用而言,除了应用层视觉显示和对应的交互逻辑处理之外,有时还需要原生操作系统(Android、iOS)提供的底层能力支持。比如,我们前面提到的数据持久化,以及推送、摄像头硬件调用等。
由于 Flutter 只接管了应用渲染层,因此这些系统底层能力是无法在 Flutter 框架内提供支持的;而另一方面,Flutter 还是一个相对年轻的生态,因此原生开发中一些相对成熟的 Java、C 或 Objective-C 代码库,比如图片处理、音视频编解码等,可能在 Flutter 中还没有相关实现。
Flutter 项目中添加原生功能主要可以从两个方面考虑
- Flutter 和原生平台的通信
- Flutter 页面中嵌入原生页面
2. Flutter 和原生平台的通信
了解决调用原生系统底层能力以及相关代码库复用问题,Flutter 为开发者提供了一个轻量级的解决方案,即逻辑层的方法通道(Method Channel)机制。基于方法通道,我们可以将原生代码所拥有的能力,以接口形式暴露给 Dart,从而实现 Dart 代码与原生代码的交互,就像调用了一个普通的 Dart API 一样。
当在Flutter中调用原生方法时,调用信息通过平台通道传递到原生,原生收到调用信息后方可执行指定的操作,如需返回数据,则原生会将数据再通过平台通道传递给Flutter。值得注意的是消息传递是异步的,这确保了用户界面在消息传递时不会被挂起。
▐ 2.1 平台通信的 3 中方式
Flutter 与 Native 端通信有如下3个方法:
- MethodChannel:Flutter 与 Native 端相互调用,调用后可以返回结果,可以 Native 端主动调用,也可以 Flutter 主动调用,属于双向通信。此方式为最常用的方式, Native 端调用需要在主线程中执行。
- BasicMessageChannel:用于使用指定的编解码器对消息进行编码和解码,属于双向通信,可以 Native 端主动调用,也可以Flutter主动调用。
- EventChannel:用于数据流(event streams)的通信, Native 端主动发送数据
▐ 2.2 Android、iOS 和 Dart 平台间的常见数据类型转换
平台通道使用标准消息编/解码器对消息进行编解码,它可以高效的对消息进行二进制序列化与反序列化。由于 Dart 与原生平台之间数据类型有所差异,下面我们列出数据类型之间的映射关系。
当在发送和接收值时,这些值在消息中的序列化和反序列化会自动进行。
▐ 2.3 如何获取平台信息
Flutter 中提供了一个全局变量 defaultTargetPlatform 来获取当前应用的平台信息,defaultTargetPlatform 定义在 platform.dart 中,它的类型是 TargetPlatform,这是一个枚举类,定义如下:
代码语言:javascript复制enum TargetPlatform { android, fuchsia, iOS, linux, macOS, windows,}
可以看到目前 Flutter 只支持这三个平台。我们可以通过如下代码判断平台
代码语言:javascript复制if(defaultTargetPlatform == TargetPlatform.android){ // 是安卓系统,do something }else if(defaultTargetPlatform == TargetPlatform.iOS){ // 是iOS系统,do something }
▐ 2.3 使用示例
加入我们Flutter要向原生传递一个字典 {"flutter":"我是flutter"},原生向 Flutter 传递一个数组 [1,2,3]
2.3.1 Flutter如何实现一次方法调用请求
首先,我们需要确定一个唯一的字符串标识符,来构造一个命名通道;然后,在这个通道之上,Flutter 通过指定方法名 flutter_postData 来发起一次方法调用请求。
可以看到,这和我们平时调用一个 Dart 对象的方法完全一样。因为方法调用过程是异步的,所以我们需要使用非阻塞(或者注册回调)来等待原生代码给予响应。
代码语言:javascript复制// 声明 MethodChannelconst platform = MethodChannel('flutter_postData');
// 处理按钮点击onPressed: () async{ List result; try{ result = await platform.invokeMethod('flutter_postData',{"flutter":"我是flutter"}); }catch(e){ result = []; } print(result.toString());},
2.3.2 iOS端的方法调用响应如何实现
首先打开Xcode中Flutter应用程序的iOS部分:
在 iOS 平台,方法调用的处理和响应是在 Flutter 应用的入口,也就是在 Applegate 中的 rootViewController(即 FlutterViewController)里实现的,因此我们需要打开 Flutter 的 iOS 宿主 App,找到 AppDelegate.m 文件,并添加相关逻辑。
代码语言:javascript复制@UIApplicationMain@objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { GeneratedPluginRegistrant.register(with: self) // 创建命名方法通道 let methodChannel = FlutterMethodChannel.init(name: "flutter_postData", binaryMessenger: self.window.rootViewController as! FlutterBinaryMessenger) // 往方法通道注册方法调用处理回调 methodChannel.setMethodCallHandler { (call, result) in if("flutter_postData" == call.method){ //打印flutter传来的值 print(call.arguments ?? {}) //向flutter传递值 DispatchQueue.main.async { result(["1","2","3"]); } } } return super.application(application, didFinishLaunchingWithOptions: launchOptions) }}
点击按钮打印
2.3.3 android 端的方法调用响应如何实现
首先在 Android Studio 中打开您的 Flutter 应用的 Android 部分:
在 Android 平台,方法调用的处理和响应是在 Flutter 应用的入口,也就是在 MainActivity 中的 FlutterView 里实现的,因此我们需要打开 Flutter 的 Android 宿主 App,找到 MainActivity.java 文件,并在其中添加相关的逻辑。
接下来,在 onCreate 里创建 MethodChannel 并设置一个 MethodCallHandler。确保使用和 Flutter 客户端中使用的通道名称相同的名称。
代码语言:javascript复制import android.os.Bundle;
import io.flutter.Log;import io.flutter.app.FlutterActivity;import io.flutter.plugin.common.MethodCall;import io.flutter.plugin.common.MethodChannel;import io.flutter.plugin.common.MethodChannel.MethodCallHandler;import io.flutter.plugin.common.MethodChannel.Result;
public class MainActivity extends FlutterActivity { private static final String CHANNEL = "flutter_postData"; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); new MethodChannel(getFlutterView(), CHANNEL).setMethodCallHandler( new MethodCallHandler() { @Override public void onMethodCall(MethodCall call, Result result) { // TODO if(call.method.equals("flutter_postData")){ //打印flutter传来的值 Log.e(call.arguments); //向flutter传递值 result.success(new String[]{"1", "2","3"}); } } }); }}
2.3.4 总结
Flutter 发起方法调用请求开始,请求经由唯一标识符指定的方法通道到达原生代码宿主,而原生代码宿主则通过注册对应方法实现、响应并处理调用请求,最后将执行结果通过消息通道,回传至 Flutter。
需要注意的是,方法通道是非线程安全的。这意味着原生代码与 Flutter 之间所有接口调用必须发生在主线程。Flutter 是单线程模型,因此自然可以确保方法调用请求是发生在主线程(Isolate)的;而原生代码在处理方法调用请求时,如果涉及到异步或非主线程切换,需要确保回调过程是在原生系统的 UI 线程(也就是 Android 和 iOS 的主线程)中执行的,否则应用可能会出现奇怪的 Bug,甚至是 Crash。
3. Flutter 视图中嵌套原生视图
我们来分析一下构建一个复杂 App 都需要什么?我们先按照四象限分析法,把能力和渲染分解成四个维度,分析构建一个复杂 App 都需要什么。
经过分析,我们终于发现,原来构建一个 App 需要覆盖那么多的知识点,通过 Flutter 和方法通道只能搞定应用层渲染、应用层能力和底层能力,对于那些涉及到底层渲染,比如浏览器、相机、地图,以及原生自定义视图的场景,自己在 Flutter 上重新开发一套显然不太现实。
在这种情况下,使用混合视图看起来是一个不错的选择。我们可以在 Flutter 的 Widget 树中提前预留一块空白区域,在 Flutter 的画板中(即 FlutterView 与 FlutterViewController)嵌入一个与空白区域完全匹配的原生视图,就可以实现想要的视觉效果了。
但是,采用这种方案极其不优雅,因为嵌入的原生视图并不在 Flutter 的渲染层级中,需要同时在 Flutter 侧与原生侧做大量的适配工作,才能实现正常的用户交互体验。
幸运的是,Flutter 提供了一个平台视图(Platform View)的概念。它提供了一种方法,允许开发者在 Flutter 里面嵌入原生系统(Android 和 iOS)的视图,并加入到 Flutter 的渲染树中,实现与 Flutter 一致的交互体验。
这样一来,通过平台视图,我们就可以将一个原生控件包装成 Flutter 控件,嵌入到 Flutter 页面中,就像使用一个普通的 Widget 一样
使用方法
- 首先,由作为客户端的 Flutter,通过向原生视图的 Flutter 封装类(在 iOS 和 Android 平台分别是 UIKitView 和 AndroidView)传入视图标识符,用于发起原生视图的创建请求;
- 然后,原生代码侧将对应原生视图的创建交给平台视图工厂(PlatformViewFactory)实现;
- 最后,在原生代码侧将视图标识符与平台视图工厂进行关联注册,让 Flutter 发起的视图创建请求可以直接找到对应的视图创建工厂。
▐ 3.1 Flutter 如何实现原生视图的接口调用
代码语言:javascript复制class MyFlutterView extends StatelessWidget { @override Widget build(BuildContext context) { // 使用 Android 平台的 AndroidView,传入唯一标识符 MyFlutterView if (defaultTargetPlatform == TargetPlatform.android) { return AndroidView(viewType: 'MyFlutterView'); } else { // 使用 iOS 平台的 UIKitView,传入唯一标识符 MyFlutterView return UiKitView(viewType: 'MyFlutterView'); } }}
▐ 3.2 嵌入原生 View-iOS
1、创建 FlutterPlatformView
代码语言:javascript复制import Foundationimport Flutter
class MyFlutterView: NSObject,FlutterPlatformView { let label = UILabel() init(_ frame: CGRect,viewID: Int64,args :Any?,messenger :FlutterBinaryMessenger) { label.text = "我是 iOS View" } func view() -> UIView { return label }}
2、注册工厂类 MyFlutterViewFactory
代码语言:javascript复制import Foundationimport Flutterclass MyFlutterViewFactory: NSObject,FlutterPlatformViewFactory { var messenger:FlutterBinaryMessenger init(messenger:FlutterBinaryMessenger) { self.messenger = messenger super.init() } func create(withFrame frame: CGRect, viewIdentifier viewId: Int64, arguments args: Any?) -> FlutterPlatformView { return MyFlutterView(frame,viewID: viewId,args: args,messenger: messenger) } func createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol { return FlutterStandardMessageCodec.sharedInstance() }}
3、在 AppDelegate 中注册
代码语言:javascript复制let registrar:FlutterPluginRegistrar = self.registrar(forPlugin: "plugins.flutter.io/custom_platform_view_plugin")!let factory = MyFlutterViewFactory(messenger: registrar.messenger())registrar.register(factory, withId: "MyFlutterView")
▐ 3.3 嵌入原生View-Android
1、在 App 项目的 java/ 包名 目录下创建嵌入 Flutter 中的 Android View,此 View 继承 PlatformView
代码语言:javascript复制// 原生视图封装类class MyFlutterView implements PlatformView { private final TextView textView;// 缓存原生视图 // 初始化方法,提前创建好视图 public MyFlutterView(Context context, int id, BinaryMessenger messenger) { textView = new TextView(context); textView.setText("我是 Android View"); }
// 返回原生视图 @Override public View getView() { return textView; } // 原生视图销毁回调 @Override public void dispose() { }}
2、注册工厂类 MyFlutterViewFactory
代码语言:javascript复制// 视图工厂类public class MyFlutterViewFactory extends PlatformViewFactory { private final BinaryMessenger messenger; // 初始化方法 public MyFlutterViewFactory(BinaryMessenger msger) { super(StandardMessageCodec.INSTANCE); messenger = msger; } // 创建原生视图封装类,完成关联 @Override public PlatformView create(Context context, int id, Object obj) { return new MyFlutterView(context, id, messenger); }}
3、在 App 中 MainActivity 中注册
代码语言:javascript复制Registrar registrar = registrarFor("plugins.flutter.io/custom_platform_view_plugin");// 生成注册类MyFlutterViewFactory playerViewFactory = new MyFlutterViewFactory(registrar.messenger());// 生成视图工厂registrar.platformViewRegistry().registerViewFactory("MyFlutterView", playerViewFactory);// 注册视图工厂
▐ 3.3 总结
由于 Flutter 与原生渲染方式完全不同,因此转换不同的渲染数据会有较大的性能开销。如果在一个界面上同时实例化多个原生控件,就会对性能造成非常大的影响,所以我们要避免在使用 Flutter 控件也能实现的情况下去使用内嵌平台视图。
因为这样做,一方面需要分别在 Android 和 iOS 端写大量的适配桥接代码,违背了跨平台技术的本意,也增加了后续的维护成本;另一方面毕竟除去地图、WebView、相机等涉及底层方案的特殊情况外,大部分原生代码能够实现的 UI 效果,完全可以用 Flutter 实现。
如果觉得不错,素质三连、或者点个「赞」、「在看」都是对笔者莫大的支持,谢谢各位大佬啦~
推荐阅读
Flutter 如何跨组件传递数据
化身面试官出 30 Vue 面试题,超级干货(附答案)
实战总结 Vue 学习看这一篇就够了
了解「网罗开发」领书籍、源码