27 | 如何在Dart层兼容Android/iOS平台特定实现?(二)
下载APP
关闭
渠道合作
推荐作者
27 | 如何在Dart层兼容Android/iOS平台特定实现?(二)
2019-08-29 陈航 来自北京
《Flutter核心技术与实战》
课程介绍
讲述:陈航
时长10:47大小9.88M
你好,我是陈航。
在上一篇文章中,我与你介绍了方法通道,这种在 Flutter 中实现调用原生 Android、iOS 代码的轻量级解决方案。使用方法通道,我们可以把原生代码所拥有的能力,以接口形式提供给 Dart。
这样,当发起方法调用时,Flutter 应用会以类似网络异步调用的方式,将请求数据通过一个唯一标识符指定的方法通道传输至原生代码宿主;而原生代码处理完毕后,会将响应结果通过方法通道回传至 Flutter,从而实现 Dart 代码与原生 Android、iOS 代码的交互。这,与调用一个本地的 Dart 异步 API 并无太多区别。
通过方法通道,我们可以把原生操作系统提供的底层能力,以及现有原生开发中一些相对成熟的解决方案,以接口封装的形式在 Dart 层快速搞定,从而解决原生代码在 Flutter 上的复用问题。然后,我们可以利用 Flutter 本身提供的丰富控件,做好 UI 渲染。
底层能力 + 应用层渲染,看似我们已经搞定了搭建一个复杂 App 的所有内容。但,真的是这样吗?
构建一个复杂 App 都需要什么?
别急,在下结论之前,我们先按照四象限分析法,把能力和渲染分解成四个维度,分析构建一个复杂 App 都需要什么。
图 1 四象限分析法
经过分析,我们终于发现,原来构建一个 App 需要覆盖那么多的知识点,通过 Flutter 和方法通道只能搞定应用层渲染、应用层能力和底层能力,对于那些涉及到底层渲染,比如浏览器、相机、地图,以及原生自定义视图的场景,自己在 Flutter 上重新开发一套显然不太现实。
在这种情况下,使用混合视图看起来是一个不错的选择。我们可以在 Flutter 的 Widget 树中提前预留一块空白区域,在 Flutter 的画板中(即 FlutterView 与 FlutterViewController)嵌入一个与空白区域完全匹配的原生视图,就可以实现想要的视觉效果了。
但是,采用这种方案极其不优雅,因为嵌入的原生视图并不在 Flutter 的渲染层级中,需要同时在 Flutter 侧与原生侧做大量的适配工作,才能实现正常的用户交互体验。
幸运的是,Flutter 提供了一个平台视图(Platform View)的概念。它提供了一种方法,允许开发者在 Flutter 里面嵌入原生系统(Android 和 iOS)的视图,并加入到 Flutter 的渲染树中,实现与 Flutter 一致的交互体验。
这样一来,通过平台视图,我们就可以将一个原生控件包装成 Flutter 控件,嵌入到 Flutter 页面中,就像使用一个普通的 Widget 一样。
接下来,我就与你详细讲述如何使用平台视图。
平台视图
如果说方法通道解决的是原生能力逻辑复用问题,那么平台视图解决的就是原生视图复用问题。Flutter 提供了一种轻量级的方法,让我们可以创建原生(Android 和 iOS)的视图,通过一些简单的 Dart 层接口封装之后,就可以将它插入 Widget 树中,实现原生视图与 Flutter 视图的混用。
一次典型的平台视图使用过程与方法通道类似:
首先,由作为客户端的 Flutter,通过向原生视图的 Flutter 封装类(在 iOS 和 Android 平台分别是 UIKitView 和 AndroidView)传入视图标识符,用于发起原生视图的创建请求;
然后,原生代码侧将对应原生视图的创建交给平台视图工厂(PlatformViewFactory)实现;
最后,在原生代码侧将视图标识符与平台视图工厂进行关联注册,让 Flutter 发起的视图创建请求可以直接找到对应的视图创建工厂。
至此,我们就可以像使用 Widget 那样,使用原生视图了。整个流程,如下图所示:
图 2 平台视图示例
接下来,我以一个具体的案例,也就是将一个红色的原生视图内嵌到 Flutter 中,与你演示如何使用平台视图。这部分内容主要包括两部分:
作为调用发起方的 Flutter,如何实现原生视图的接口调用?
如何在原生(Android 和 iOS)系统实现接口?
接下来,我将分别与你讲述这两个问题。
Flutter 如何实现原生视图的接口调用?
在下面的代码中,我们在 SampleView 的内部,分别使用了原生 Android、iOS 视图的封装类 AndroidView 和 UIkitView,并传入了一个唯一标识符,用于和原生视图建立关联:
调用方的实现搞定了。接下来,我们需要在原生代码中完成视图创建的封装,建立相关的绑定关系。同样的,由于需要同时适配 Android 和 iOS 平台,我们需要分别在两个系统上完成对应的接口实现。
如何在原生系统实现接口?
首先,我们来看看 Android 端的实现。在下面的代码中,我们分别创建了平台视图工厂和原生视图封装类,并通过视图工厂的 create 方法,将它们关联起来:
将原生视图封装类与原生视图工厂完成关联后,接下来就需要将 Flutter 侧的调用与视图工厂绑定起来了。与上一篇文章讲述的方法通道类似,我们仍然需要在 MainActivity 中进行绑定操作:
完成绑定之后,平台视图调用响应的 Android 部分就搞定了。
接下来,我们再来看看 iOS 端的实现。
与 Android 类似,我们同样需要分别创建平台视图工厂和原生视图封装类,并通过视图工厂的 create 方法,将它们关联起来:
然后,我们同样需要把原生视图的创建与 Flutter 侧的调用关联起来,才可以在 Flutter 侧找到原生视图的实现:
需要注意的是,在 iOS 平台上,Flutter 内嵌 UIKitView 目前还处于技术预览状态,因此我们还需要在 Info.plist 文件中增加一项配置,把内嵌原生视图的功能开关设置为 true,才能打开这个隐藏功能:
经过上面的封装与绑定,Android 端与 iOS 端的平台视图功能都已经实现了。接下来,我们就可以在 Flutter 应用里,像使用普通 Widget 一样,去内嵌原生视图了:
如下所示,我们分别在 iOS 和 Android 平台的 Flutter 应用上,内嵌了一个红色的原生视图:
图 3 内嵌原生视图示例
在上面的例子中,我们将原生视图封装在一个 StatelessWidget 中,可以有效应对静态展示的场景。如果我们需要在程序运行时动态调整原生视图的样式,又该如何处理呢?
如何在程序运行时,动态地调整原生视图的样式?
与基于声明式的 Flutter Widget,每次变化只能以数据驱动其视图销毁重建不同,原生视图是基于命令式的,可以精确地控制视图展示样式。因此,我们可以在原生视图的封装类中,将其持有的修改视图实例相关的接口,以方法通道的方式暴露给 Flutter,让 Flutter 也可以拥有动态调整视图视觉样式的能力。
接下来,我以一个具体的案例来演示如何在程序运行时动态调整内嵌原生视图的背景颜色。
在这个案例中,我们会用到原生视图的一个初始化属性,即 onPlatformViewCreated:原生视图会在其创建完成后,以回调的形式通知视图 id,因此我们可以在这个时候注册方法通道,让后续的视图修改请求通过这条通道传递给原生视图。
由于我们在底层直接持有了原生视图的实例,因此理论上可以直接在这个原生视图的 Flutter 封装类上提供视图修改方法,而不管它到底是 StatelessWidget 还是 StatefulWidget。但为了遵照 Flutter 的 Widget 设计理念,我们还是决定将视图展示与视图控制分离,即:将原生视图封装为一个 StatefulWidget 专门用于展示,通过其 controller 初始化参数,在运行期修改原生视图的展示效果。如下所示:
Flutter 的调用方实现搞定了,接下来我们分别看看 Android 和 iOS 端的实现。
程序的整体结构与之前并无不同,只是在进行原生视图初始化时,我们需要完成方法通道的注册和相关事件的处理;在响应方法调用消息时,我们需要判断方法名,如果完全匹配,则修改视图背景,否则返回异常。
Android 端接口实现代码如下所示:
iOS 端接口实现代码:
通过注册方法通道,以及暴露的 changeBackgroundColor 接口,Android 端与 iOS 端修改平台视图背景颜色的功能都已经实现了。接下来,我们就可以在 Flutter 应用运行期间,修改原生视图展示样式了:
运行一下,效果如下所示:
图 4 动态修改原生视图样式
总结
好了,今天的分享就到这里。我们总结一下今天的主要内容吧。
平台视图解决了原生渲染能力的复用问题,使得 Flutter 能够通过轻量级的代码封装,把原生视图组装成一个 Flutter 控件。
Flutter 提供了平台视图工厂和视图标识符两个概念,因此 Dart 层发起的视图创建请求可以通过标识符直接找到对应的视图创建工厂,从而实现原生视图与 Flutter 视图的融合复用。对于需要在运行期动态调用原生视图接口的需求,我们可以在原生视图的封装类中注册方法通道,实现精确控制原生视图展示的效果。
需要注意的是,由于 Flutter 与原生渲染方式完全不同,因此转换不同的渲染数据会有较大的性能开销。如果在一个界面上同时实例化多个原生控件,就会对性能造成非常大的影响,所以我们要避免在使用 Flutter 控件也能实现的情况下去使用内嵌平台视图。
因为这样做,一方面需要分别在 Android 和 iOS 端写大量的适配桥接代码,违背了跨平台技术的本意,也增加了后续的维护成本;另一方面毕竟除去地图、WebView、相机等涉及底层方案的特殊情况外,大部分原生代码能够实现的 UI 效果,完全可以用 Flutter 实现。
思考题
最后,我给你留下一道思考题吧。
请你在动态调整原生视图样式的代码基础上,增加颜色参数,以实现动态变更原生视图颜色的需求。
欢迎你在评论区给我留言分享你的观点,我会在下一篇文章中等待你!感谢你的收听,也欢迎你把这篇文章分享给更多的朋友一起阅读。
分享给需要的人,Ta购买本课程,你将得18元
生成海报并分享
赞 3
提建议
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
上一篇
26 | 如何在Dart层兼容Android/iOS平台特定实现?(一)
下一篇
28 | 如何在原生应用中混编Flutter工程?
精选留言(21)
- 许童童2019-08-29妙啊,通过平台视图,Flutter就可以使用原生视图了,这样,基本所有需求都可以实现了。如果社区再繁荣一点,许多组件都可以拿来即用,那开发需求的速度就是相当快了。共 1 条评论4
- 宇2019-09-02思考题实现思路 dart层设置颜色参数的方法: changeBackgroundColor(int r, int g, int b) async { _channel.invokeMethod('changeBackgroundColor', {"r":r, "g":g, "b":b}); } dart层调用: controller.changeBackgroundColor(0, 255, 255) android native层实现: if (methodCall.method.equals("changeBackgroundColor")) { int r = methodCall.argument("r"); int g = methodCall.argument("g"); int b = methodCall.argument("b"); view.setBackgroundColor(Color.rgb(r, g, b)); result.success(0); }else { result.notImplemented(); }展开
作者回复: 赞👍
4 - Verios2020-03-19每次 widget 重建,都会create一个原生 view 吗?共 1 条评论2
- jianwei2020-01-08floatingActionButton: FloatingActionButton( onPressed: () => controller.changeBackground('#FFFFFF'), ), Future<void> changeBackground(String colorString) async { try { return _channel.invokeMethod('changeBackgroundColor', <String, dynamic> { 'color': colorString }); } catch (e) { } } - (void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { if ([call.method isEqualToString:@"changeBackgroundColor"]) { NSString *colorString = call.arguments[@"color"]; if (colorString.length != 0) { _templcateView.backgroundColor = [UIColor colorWithHexString:colorString]; result(@0); return; } } result(FlutterMethodNotImplemented); }展开2
- 舒大飞2019-10-10反过来,可以在原生页面中嵌入一小块Flutter视图吗
作者回复: 可以,Activity内嵌flutterView其实就是这种方式,只是这个视图大小是整个屏幕
共 2 条评论2 - 微尘2022-11-14 来自北京这一节含金量高啊,不知道windows 是否也可以提供插件给dart端使用呢?
- 五十度灰黑2021-11-29Flutter 页面嵌入原生视图,有滚动条的情况,原生视图定位问题怎么解决?
- 刘洪林2021-05-04对前端来说是不是还要学安卓和iOS开发啊共 1 条评论
- Geek_cc0a3b2021-04-27内嵌原生空间,事件响应比如onClick是直接在原生层响应了吧,flutter不需要负责处理原生事件的响应吧?
- 小强2021-04-25//原生视图销毁回调 @Override public void dispose() { } 安卓上有这个回调,怎么iOS没有对应的回调,刚好项目遇到这个问题了,期待老师解惑
- 无名2020-08-14iOS 平台视图中:-(NSObject<FlutterPlatformView> *)createWithFrame:(CGRect)frame viewIdentifier:(int64_t)viewId arguments:(id)args{} 为何frame的值都是(0.0, 0.0, 0.0, 0.0),为何没有宽高?这里不应该是显示的视图大小吗?
- lala2020-08-12_onPlatformViewCreated(int id) { if (widget.controller == null) { return; } widget.controller.onCreate(id); } 示例中id传值是多少呢?请老师解答下������������������展开
- Geek_fdd35e2020-07-24registrarFor("com.hangchen/NativeViews"); registrarFor这个方法找不到了,说没有声明, flutter 1.0.0共 1 条评论
- zeqin2020-06-04那怎么实现原生和futter间互相传值?
- 无名2020-05-13iOS 中FlutterPlatformViewFactory的func create(withFrame frame: CGRect, viewIdentifier viewId: Int64, arguments args: Any?)方法中的frame值都为0,那么这个参数是干什么用的呢?什么情况下会有值?
- 星星2020-03-16这个在io.flutter.embedding.android.FlutterActivity中如何绑定呢,没有响应的方法啊共 1 条评论
- JW2020-02-03在flutter工程下 .android打开的java文件无法编译和提示,编写本地代码不友好,有什么办法吗?共 2 条评论
- mqh2020-01-06现在注册插件的话需要用FlutterPlugin,用新接口的话如何进行注册?共 1 条评论
- kennen2019-11-26iOS中的frame参数并没有用到,flutter是怎么把宽高传给iOS来展示的呢?
作者回复: 你回忆一下iOS中autolayout和autorelease中父容器的布局变化对于组件布局的影响
- Miracle_2019-10-23请问下,嵌入了原生视图后,如果嵌入的是较为复杂的视图,视图带走点击等交互事件,应该在哪边设置监听或者处理呢?
作者回复: 看具体场景。我的建议是,如果点击交互的事件处理是能够在原生视图内搞定的,原生视图自己监听就好。否则就需要传出来让flutter处理