Flutter项目中怎么混编原生功能
依托于与 Skia 的深度定制及优化,Flutter 给我们提供了很多关于渲染的控制和支持,能够实现绝对的跨平台应用层渲染一致性。但对于一个应用而言,除了应用层视觉显示和对应的交互逻辑处理之外,有时还需要原生操作系统(Android、iOS)提供的底层能力支持。比如,我们前面提到的数据持久化,以及推送、摄像头硬件调用等。
由于 Flutter 只接管了应用渲染层,因此这些系统底层能力是无法在 Flutter 框架内提供支持的;而另一方面,Flutter 还是一个相对年轻的生态,因此原生开发中一些相对成熟的 Java、C++ 或 Objective-C 代码库,比如图片处理、音视频编解码等,可能在 Flutter 中还没有相关实现。
Flutter项目中添加原生功能主要可以从两个方面考虑
- 1、Flutter和原生平台的通信
- 2、Flutter页面中嵌入原生页面
1、Flutter和原生平台的通信
了解决调用原生系统底层能力以及相关代码库复用问题,Flutter 为开发者提供了一个轻量级的解决方案,即逻辑层的方法通道(Method Channel)机制。基于方法通道,我们可以将原生代码所拥有的能力,以接口形式暴露给 Dart,从而实现 Dart 代码与原生代码的交互,就像调用了一个普通的 Dart API 一样。
当在Flutter中调用原生方法时,调用信息通过平台通道传递到原生,原生收到调用信息后方可执行指定的操作,如需返回数据,则原生会将数据再通过平台通道传递给Flutter。值得注意的是消息传递是异步的,这确保了用户界面在消息传递时不会被挂起。
平台通信的3中方式
Flutter 与 Native 端通信有如下3个方法:
- MethodChannel:Flutter 与 Native 端相互调用,调用后可以返回结果,可以 Native 端主动调用,也可以Flutter主动调用,属于双向通信。此方式为最常用的方式, Native 端调用需要在主线程中执行。
- BasicMessageChannel:用于使用指定的编解码器对消息进行编码和解码,属于双向通信,可以 Native 端主动调用,也可以Flutter主动调用。
- EventChannel:用于数据流(event streams)的通信, Native 端主动发送数据给
Android、iOS 和 Dart 平台间的常见数据类型转换
平台通道使用标准消息编/解码器对消息进行编解码,它可以高效的对消息进行二进制序列化与反序列化。由于Dart与原生平台之间数据类型有所差异,下面我们列出数据类型之间的映射关系。
当在发送和接收值时,这些值在消息中的序列化和反序列化会自动进行。
如何获取平台信息
Flutter 中提供了一个全局变量defaultTargetPlatform
来获取当前应用的平台信息,defaultTargetPlatform
定义在platform.dart
中,它的类型是TargetPlatform
,这是一个枚举类,定义如下:
enum TargetPlatform {
android,
fuchsia,
iOS,
linux,
macOS,
windows,
}
可以看到目前Flutter只支持这三个平台。我们可以通过如下代码判断平台
if(defaultTargetPlatform == TargetPlatform.android){
// 是安卓系统,do something
}else if(defaultTargetPlatform == TargetPlatform.iOS){
// 是iOS系统,do something
}
使用示例
加入我们Flutter要向原生传递一个字典{"flutter":"我是flutter"}
,原生向Flutter传递一个数组[1,2,3]
Flutter如何实现一次方法调用请求
首先,我们需要确定一个唯一的字符串标识符,来构造一个命名通道;然后,在这个通道之上,Flutter 通过指定方法名flutter_postData
来发起一次方法调用请求。
可以看到,这和我们平时调用一个 Dart 对象的方法完全一样。因为方法调用过程是异步的,所以我们需要使用非阻塞(或者注册回调)来等待原生代码给予响应。
// 声明 MethodChannel
const platform = MethodChannel('flutter_postData');
// 处理按钮点击
onPressed: () async{
List result;
try{
result = await platform.invokeMethod('flutter_postData',{"flutter":"我是flutter"});
}catch(e){
result = [];
}
print(result.toString());
},
iOS端的方法调用响应如何实现
首先打开Xcode中Flutter应用程序的iOS部分:
在 iOS 平台,方法调用的处理和响应是在 Flutter 应用的入口,也就是在 Applegate 中的 rootViewController(即 FlutterViewController)里实现的,因此我们需要打开 Flutter 的 iOS 宿主 App,找到 AppDelegate.m 文件,并添加相关逻辑。
@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)
}
}
点击按钮打印
android端的方法调用响应如何实现
首先在Android Studio中打开您的Flutter应用的Android部分:
在 Android 平台,方法调用的处理和响应是在 Flutter 应用的入口,也就是在 MainActivity 中的 FlutterView 里实现的,因此我们需要打开 Flutter 的 Android 宿主 App,找到 MainActivity.java 文件,并在其中添加相关的逻辑。
接下来,在onCreate里创建MethodChannel并设置一个MethodCallHandler。确保使用和Flutter客户端中使用的通道名称相同的名称。
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"});
}
}
});
}
}
总结
Flutter 发起方法调用请求开始,请求经由唯一标识符指定的方法通道到达原生代码宿主,而原生代码宿主则通过注册对应方法实现、响应并处理调用请求,最后将执行结果通过消息通道,回传至 Flutter。
需要注意的是,方法通道是非线程安全的。这意味着原生代码与 Flutter 之间所有接口调用必须发生在主线程。Flutter 是单线程模型,因此自然可以确保方法调用请求是发生在主线程(Isolate)的;而原生代码在处理方法调用请求时,如果涉及到异步或非主线程切换,需要确保回调过程是在原生系统的 UI 线程(也就是 Android 和 iOS 的主线程)中执行的,否则应用可能会出现奇怪的 Bug,甚至是 Crash。
2、Flutter视图中嵌套原生视图
我们来分析一下构建一个复杂 App 都需要什么?我们先按照四象限分析法,把能力和渲染分解成四个维度,分析构建一个复杂 App 都需要什么。
经过分析,我们终于发现,原来构建一个 App 需要覆盖那么多的知识点,通过 Flutter 和方法通道只能搞定应用层渲染、应用层能力和底层能力,对于那些涉及到底层渲染,比如浏览器、相机、地图,以及原生自定义视图的场景,自己在 Flutter 上重新开发一套显然不太现实。
在这种情况下,使用混合视图看起来是一个不错的选择。我们可以在 Flutter 的 Widget 树中提前预留一块空白区域,在 Flutter 的画板中(即 FlutterView 与 FlutterViewController)嵌入一个与空白区域完全匹配的原生视图,就可以实现想要的视觉效果了。
但是,采用这种方案极其不优雅,因为嵌入的原生视图并不在 Flutter 的渲染层级中,需要同时在 Flutter 侧与原生侧做大量的适配工作,才能实现正常的用户交互体验。
幸运的是,Flutter 提供了一个平台视图(Platform View)的概念。它提供了一种方法,允许开发者在 Flutter 里面嵌入原生系统(Android 和 iOS)的视图,并加入到 Flutter 的渲染树中,实现与 Flutter 一致的交互体验。
这样一来,通过平台视图,我们就可以将一个原生控件包装成 Flutter 控件,嵌入到 Flutter 页面中,就像使用一个普通的 Widget 一样
使用方法
- 1、首先,由作为客户端的 Flutter,通过向原生视图的 Flutter 封装类(在 iOS 和 Android 平台分别是 UIKitView 和 AndroidView)传入视图标识符,用于发起原生视图的创建请求;
- 2、然后,原生代码侧将对应原生视图的创建交给平台视图工厂(PlatformViewFactory)实现;
- 3、最后,在原生代码侧将视图标识符与平台视图工厂进行关联注册,让 Flutter 发起的视图创建请求可以直接找到对应的视图创建工厂。
Flutter 如何实现原生视图的接口调用
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');
}
}
}
嵌入原生View-iOS
- 1、创建FlutterPlatformView
import Foundation
import 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
import Foundation
import Flutter
class 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 中注册
let registrar:FlutterPluginRegistrar = self.registrar(forPlugin: "plugins.flutter.io/custom_platform_view_plugin")!
let factory = MyFlutterViewFactory(messenger: registrar.messenger())
registrar.register(factory, withId: "MyFlutterView")
嵌入原生View-Android
- 1、在App 项目的 java/包名 目录下创建嵌入 Flutter 中的 Android View,此 View 继承 PlatformView
// 原生视图封装类
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
// 视图工厂类
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 中注册
Registrar registrar = registrarFor("plugins.flutter.io/custom_platform_view_plugin");// 生成注册类
MyFlutterViewFactory playerViewFactory = new MyFlutterViewFactory(registrar.messenger());// 生成视图工厂
registrar.platformViewRegistry().registerViewFactory("MyFlutterView", playerViewFactory);// 注册视图工厂
总结
由于 Flutter 与原生渲染方式完全不同,因此转换不同的渲染数据会有较大的性能开销。如果在一个界面上同时实例化多个原生控件,就会对性能造成非常大的影响,所以我们要避免在使用 Flutter 控件也能实现的情况下去使用内嵌平台视图。
因为这样做,一方面需要分别在 Android 和 iOS 端写大量的适配桥接代码,违背了跨平台技术的本意,也增加了后续的维护成本;另一方面毕竟除去地图、WebView、相机等涉及底层方案的特殊情况外,大部分原生代码能够实现的 UI 效果,完全可以用 Flutter 实现
代码地址:github.com/SunshineBro… 本文主要参考:www.kancloud.cn/alex_wsc/fl…
转载自:https://juejin.cn/post/6933478020923064327