百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

Flutter混合开发探索与实践(flutter开发技巧)

cac55 2024-09-20 12:51 23 浏览 0 评论

背景

Flutter是Google推出的跨平台、高性能开发框架,使用Skia作为渲染引擎,不使用平台控件,保证Android和iOS上UI一致性。使用Flutter开发,Android、iOS使用一套Dart代码,可以节省开发成本。

通常具有一定规模的App都有一套成熟通用的基础库,而且依赖公司体系内的很多基础库。使用Flutter重新开发时间和实现成本都很高。所以在Native App中嵌入Flutter功能的混合开发模式是应用Flutter技术的稳健型改造方式。

公寓PMS是一款给公寓管家提供房源管理的APP,前期功能已使用Native开发上线。我们在该项目中使用了Flutter开发,需要实现以下功能:将Flutter集成到已有Native项目中;实现Flutter与Native页面混合管理;实现Flutter与Native通信,复用已有Native资源;实现Dart侧代码开发框架。


Flutter引擎介绍

1. Flutter架构

首先看下Flutter架构图:

Flutter的架构主要分为3层:Framework,Engine,Embedder。

Framework使用dart实现,包括Material Design和Cupertino风格的Widgets,文本/图片/按钮等基础Widgets、渲染、动画、手势等。

Engine使用C++实现,主要包括:Skia,Dart和Text。Skia是开源的二维图形库,提供了适用于多种软硬件平台的通用API。在代码调用 dart:ui库时,调用最终会走到Engine层,然后实现真正的绘制逻辑。

Embedder是一个嵌入层,是将Flutter引擎移植到各个平台的中间层代码,主要包括渲染Surface设置,线程设置,以及插件等。


2.Flutter线程模型

Flutter Engine自己不创建管理线程。Flutter Engine线程的创建和管理是由Embedder负责的。Flutter Engine要求Embedder提供四个Task Runner。尽管Flutter Engine不在乎Runner具体跑在哪个线程,但是它需要线程配置在整一个生命周期里面保持稳定。也就是说一个Runner最好始终保持在同一线程运行。这四个主要 的Task Runner包括:

Platform Task Runner

Flutter Engine的主Task Runner,运行Platform Task Runner的线程可以理解为是主线程。类似于Android Main Thread或者iOS的Main Thread。

UI Task Runner Thread(Dart Runner)

UI Task Runner被Flutter Engine用于执行Dart root isolate代码。

GPU Task Runner

GPU Task Runner被用于执行设备GPU的相关调用。

IO Task Runner

IO Runner的主要功能是从图片存储(比如磁盘)中读取压缩的图片格式,将图片数据进行处理为GPU Runner 的渲染做好准备。

前面我们提到Engine Runner的线程可以按照实际情况进行配置,各个平台目前有自己的实现策略。Android和iOS平台上面每一个Engine实例启动的时候会为UI,GPU,IO Runner各自创建一个新的线程。所有Engine实例共享同一个Platform Runner和线程。


Flutter官方默认混合方案

多引擎模式

在混合方案中解决的主要问题是如何去处理交替出现的Flutter和Native页面。Flutter官方给出了一个Keep It Simple的方案:对于连续的Flutter页面(Widget)只需要在当前FlutterActivity打开即可,对于间隔的Flutter页面初始化新的引擎。页面示意如下图所示:

这个方案的好处就是简单易懂,容易使用,但是存在比较严重的问题。如果Native页面与Flutter页面交替出现,Flutter Engine的数量会线性增加,多引擎模式会造成以下问题:

  • 内存问题。多引擎模式下每个引擎之间的Isolate是相互独立的,所以每一个引擎底层都维护了图片缓存等比较消耗内存的对象。
  • 冗余资源问题。通过前文可以知道,引擎在Android和iOS的实现中,每一个Flutter实例会新启动三个线程(IO,GPU和UI),从而带来了额外的资源使用。
  • 页面间通信复杂。每一个Flutter页面在一个隔离的isolate中,页面间通信将会变得非常复杂。
  • 插件的注册问题。插件依赖Messenger传递消息,而Messenger由FlutterNativeView实现。多引擎方式使得插件的注册和通信将会变得混乱且难以维护。

综上,由于多引擎混合方案存在比较多的问题,所以项目中没有采用此方案。


Flutter Boost实现方案

通过调研发现,阿里闲鱼推出了Flutter Boost解决方案,该方案采用的是多个Flutter页面共享引擎的实现方式,示意图如下所示:

所有的Flutter页面共享一个Flutter实例(FlutterView),这种方式能够有效避免多引擎方式带来的各种问题,但是单例的实现也使页面的管理变得更加复杂。为此Flutter Boost提供了一套完整的解决方案。

下面看下Flutter Boost的整体架构图:

方案实现分为Native部分与Dart部分:

Native部分概念

  • Container:Native容器,Fragment(Android),ViewController(iOS)
  • Container Manager:Native容器管理器
  • Messaging:基于Message Channel的消息通道

Dart部分概念

  • Container:Flutter Widget的容器,Flutter Navigator
  • Container Manager:Flutter 容器管理器
  • Coordinator: 协调器,接受Messaging消息,负责调用Container Manager的状态管理。

Native容器与Flutter容器(Navigator)是一一对应的,生命周期也是同步的。当一个Native容器被创建的时候,Flutter对应的容器也被创建,它们通过相同的唯一id关联起来。当Native的容器被销毁的时候,Flutter的容器也被销毁。Flutter容器的状态是跟随Native容器,这也就是Native驱动。由Manager统一管理切换当前在屏幕上展示的容器。

性能对比

下图对官方默认多引擎混合方案和Flutter Boost方案进行了性能对比:


从上述对比图可以看出,当连续打开多个Flutter页面时,默认多引擎方式页面的内存呈线性增长,而Flutter Boost页面内存保持在一个比较稳定的范围。所以我们的项目中选用了Flutter Boost方案。


公寓PMS进入Flutter Boost

1.Dart工程部分

在Dart工程的pubspec.yaml中引入Flutter Boost:

flutter_boost: 
    git: 
      url: 'https://github.com/alibaba/flutter_boost.git' 
ref: '0.0.410' 

然后运行flutter packages get获取Flutter Boost代码到本地。


2. Native工程部分(Android)

(1)在setting.gradle中依赖Flutter工程:

setBinding(new Binding([gradle: this, mainModuleName: 'ApartmentClient'])) 
evaluate(new File( 
settingsDir.parentFile, 
'flutter_apartment/.android/include_flutter.groovy' 
))

(2)在build.gradle中引入Flutter Boost的Native工程:

implementation project(':flutter') 
implementation project(':flutter_boost')

至此就把Flutter Boost接入到公寓PMS工程里面了,但是要使用Flutter Boost,还需要以下工作要完成。

  • 设计Flutter跳转协议,接入跳转框架

Flutter Boost框架没有集成ARouter等路由跳转框架。所以我们需要结合自己的业务特点设计跳转协议。仿照WubaRN的设计思想,我们需要在Native端有一个Flutter通用载体页,所有的路由跳转都经由Native侧跳转中心。跳转框架我们用的是58JumpCenterLib,[h1]跳转协议如下所示:

wbapartment://jump/house/flutter?params={"container_name":"personalCenter","show_guide":true}

“flutter”:Native侧载体页页面类型

“params”:跳转协议参数,其中“container_name”是固定参数,标识Dart侧的具体显示页面(Navigator);“params”里面的所有参数都经由MessageChannel透传到Dart侧。

最后需要处理一下Dart侧传过来的跳转协议,代码如下:

private void initFlutterBoost() {
    FlutterBoostPlugin.init(new IPlatform() {
        ......
        /**
         * 当Dart侧打开一个本地页面,将会回调这个方法,页面参数拼接在url中
         * @param context
         * @param url
         * @param requestCode
         * @return
         */
        @Override
        public boolean startActivity(Context context, String url, int requestCode) {
            return PageTransferManager.jump(context, url);
        }
    });
}
  • 完善Native侧Flutter载体页

由于在公寓PMS APP中,我们需要在首页TAB页中嵌入Flutter页面,还需要支持跳转协议的单独展示页面。所以我们的做法是基于Fragment进行封装,单独页面使用FragmentActivity/Fragment的方式。

通过完成以上工作,就可以在公寓PMS项目中使用Flutter Boost框架了。


Flutter Boost的缺点及改进

Flutter Boost是从应用层出发,直接复用FlutterView从而共享Flutter Engine。Native侧实现时,需要共享FlutterView,不同Activity/ViewController切换时,需要将FlutterView从前页面的Activity/ViewController移除,然后添加到当前页面的Activity/ViewController。这个过程在Android上能够明显的感觉到页面的闪动。Flutter 1.12的发布完美的解决了这个问题,Flutter 1.12支持将Flutter Engine通过id缓存起来,然后启动页面时,可以指定使用缓存中的Engine,从而彻底解决了混合开发共享引擎的问题。页面间使用缓存引擎方案,需要将Native侧页面和Dart侧页面一一对应。可以使用Message Channel通信,结合路由跳转中心,由Native页面驱动即可。


混合开发中遇到的问题

1. Dart侧网络请求问题

在公寓PMS项目中,Dart侧网络请求使用的是开源框架dio。但是开发过程中遇到问题,登录信息、设备版本等信息是Native侧实现的,Dart侧的网络请求header没法直接获取这些信息。解决办法是通过Message Channel将Native侧的header信息共享给Dart侧。

Native侧实现:

new MethodChannel(getBoostFlutterView(), METHOD_CHANNEL).setMethodCallHandler(
        new MethodChannel.MethodCallHandler() {
            @Override
            public void onMethodCall(MethodCall call, MethodChannel.Result result) {
                if (call.method.equals("getHeader")) {
                    IHeadersIntegration commonHeaderUtils = CommonHeaderUtils.getInstance(MainTabActivity.sRef.get());
                    Map<String, String> headerMap = commonHeaderUtils.generateParamMap(MainTabActivity.sRef.get());
                    result.success(JsonUtils.hashMapToJson(headerMap));
                } else {
                    result.notImplemented();
                }
            }
        });

Dart侧实现:

Map<String, dynamic> headers;
try {
  final String headerString = await platform.invokeMethod('getHeader');
  headers = jsonDecode(headerString);
} on PlatformException catch (e) {}
Map<String, String> params = new Map();
Response response = await dio.get(
  dataUrl,
  queryParameters: params,
  options: Options(headers: headers),
);


2. 复用Native的资源图片问题

Flutter默认将所有的图片资源文件打包到assets目录下,但是我们并不是用Flutter全新开发的项目,图片资源放在Native侧的drawable目录下,即使是全新的Flutter页面也会有很多图片复用已有的Native侧图片,所以在assets目录下新增图片资源并不合适。但是Flutter官方并没有提供直接调用drawable目录下的图片资源的途径。

通过调研,可以通过以下方式实现Native侧的图片共享:

Message Channel方式

Dart侧通过Message Channel将资源文件名传递到Native侧;Native侧将对应名称的drawable以二进制格式传递到Dart侧;Dart侧接收到二进制格式图片后进行渲染。

Native侧代码:

BasicMessageChannel<Object> messageChannel = new BasicMessageChannel<>(getFlutterView(),
        "getPic", StandardMessageCodec.INSTANCE);
messageChannel.setMessageHandler(new BasicMessageChannel.MessageHandler<Object>() {
    @Override
    public void onMessage(Object o, BasicMessageChannel.Reply<Object> reply) {     reply.reply(drawableToByte(getResources().getDrawable(getResId(o.toString()))));
    }
});

Dart侧代码:

const _messageChannel =
    const BasicMessageChannel<Object>("getPic", StandardMessageCodec());
Future<Uint8List> getNativeImage(String name) async {
  Uint8List result = await _messageChannel.send(name);
  return result;
}

通过以上步骤,就可以将Android Native侧drawable目录下侧资源图片共享给Dart侧控件使用,从而避免了重复引入资源。


Dart侧开发框架使用

在使用Dart开发需求之初,为了快速实现功能,还有对Flutter特性不熟悉,我们没有使用开发框架,功能就是代码的堆砌。但是,随着使用页面的增多,发现项目中业务代码耦合严重,代码可维护性很差。为此,我们进行了相关调研,发现闲鱼开源了一款Flutter应用框架——Fish-Redux。

1. Fish-Redux介绍

Fish-Redux是一个基于Redux数据管理的组装式Flutter应用框架,特别适用于构建中大型的复杂应用。它的最大特点是配置式组装,它会非常干净,易编写、易维护、易协作。

下面看下Fish-Redux架构图:

架构主要分为3层,自下向上依次为:

Redux

Redux是一个用来做[可预测][集中式][易调试][灵活性]的数据管理的框架。所有对数据的增删改查等操作都由Redux来集中负责。

Fish-Redux通过Redux做集中化的可观察的数据状态管理。Fish-Redux在Flutter中对传统的Redux做了改良。一个组件需要定义一个数据(Struct)和一个Reducer。同时组件之间存在着父依赖子的关系。通过这层依赖关系,解决了【集中】和【分治】之间的矛盾,同时对Reducer的手动层层Combine变成由框架自动完成,简化了使用Redux的困难。

Component

Component是对局部的展示和功能的封装。基于Redux的原则,Fish-Redux对功能细分为修改数据的功能(Reducer)和非修改数据的功能(Effect)。组件是对视图的分治,也是对数据的分治。通过逐层分治,将复杂的页面和数据切分为相互独立的小模块,有利于团队内的协作开发。

Adapter

Adapter也是对局部的展示和功能的封装。它是Component实现上的一种变化,优化了Flutter在使用ListView场景下的性能问题。

综上所述,Fish-Redux不仅实现了Flutter页面的状态管理,更是一套完整的Flutter应用开发框架。下面介绍一下公寓PMS是如何使用Fish-Redux进行开发的。


2. Fish-Redux在公寓PMS的应用

Fish-Redux的接入非常简单,只需在Flutter项目中pubspec.yaml的dependencies模块设置fish-redux及依赖版本,然后运行flutter packages get即可。

下面以公寓PMS中个人中心页面介绍:

下图是个人中心页面,

该页面使用Flutter ListView控件实现,主要由6个item,5种item组合而成。

下面是个人中心的Page代码:

class PersonalCenterPage
    extends Page<PersonalCenterPageState, Map<dynamic, dynamic>> {
  PersonalCenterPage(): super(
          initState: initState,
          effect: buildEffect(),
          view: buildView,
          dependencies: Dependencies<PersonalCenterPageState>(
              adapter: NoneConn<PersonalCenterPageState>() +
                  PersonalCenterListAdapter()),
        );
}

PersonalCenterPage由State,Effect,View,Adapter组成。其中,State定义了页面的数据及状态信息;Effect定义了在页面生命周期开始时,调用网络请求api获取页面数据;View定义了页面具体的UI,包括ListView,Loading图,TitleBar等;Adapter里面定义了列表包含的Component等。下面着重看下Adapter实现:

class PersonalCenterListAdapter
    extends DynamicFlowAdapter<PersonalCenterPageState> {
  PersonalCenterListAdapter(): super(
          pool: <String, Component<Object>>{
            NORMAL_ITEM: NormalItemComponent(),
            USER_INFO_ITEM: UserItemComponent(),
            LOGOUT_ITEM: LogoutItemComponent(),
            TODO_ITEM: TodoItemComponent(),
            CONTACT_ITEM: ContactItemComponent(),
          },
          connector: _HouseListConnector(),
          reducer: buildReducer(),
        );
}

在PageCenterListAdapter中,pool中注册了列表中所包含的Component及类型;connector是连接器,负责将网络请求返回的数据转化成Component渲染时需要的数据;reducer里定义了修改页面数据的行为,当网络请求成功后,会调用该action触发页面渲染。

最后看下Component实现,以UserItemComponent为例:

class UserItemComponent extends Component<UserItemState> {
  UserItemComponent() : super(
    view: buildView
  );
}

其中,UserItemState是该模块渲染所需数据,view则是该模块UI逻辑。

下面看下该页面整体的代码结构图:

从上图可以看出,使用Fish-Redux开发会使代码结构非常清晰,尤其是当页面逻辑复杂的时候。Fish-Redux使Flutter开发变得简单,只要按照方法的要求传入对应的参数即可,实现了面向方法编程。

该实现中,将页面分解成Page->Adapter->Component的结构。当列表页中新增样式,只需要开发对应的Component并注册到Adapter中的pool即可。由于模块拆分到粒度比较细的业务单元,该页面中实现的Component也可以复用到别的页面中,避免重复开发。

由于Fish-Redux中包含了Redux的功能,使得开发过程中的状态传递变得非常简单,只需注册Action,在接收Action的地方设置响应逻辑,在触发的地方调用dispatch(Action action)方法即可。

此外,Fish-Redux的好处是将逻辑与视图隔离开,view只负责具体的页面渲染;而逻辑通过Effect和Reducer实现。所以有这样的公式Component = View + Effect(可选) + Reducer(可选) + Dependencies(可选)。这不仅很好的实现了代码的解耦,也为以后实现UI代码自动生成,开发人员只开发业务逻辑代码的开发模式提供了可能。

借鉴Flutter中面向函数编程,可插拔的页面组件化思想,我们目前正在对Native项目58APP租房页面进行重构,以实现代码结构的统一,不同页面组件间的复用,并且页面可以根据Server返回数据灵活组装。


总结

本文介绍了Flutter混合开发中遇到的问题及解决办法,以及开发中应用Fish-Redux的实践。Flutter混合开发,主要的问题是共享Flutter引擎的实现。Flutter-Boost提供了共享FlutterView的实现方式。我们引入了Flutter-Boost,开发了Native侧载体页,设计了通用Flutter跳转协议,结合58跳转中心解决了Flutter混合开发的问题。在业务开发过程中,随着开发的深入和业务逻辑的复杂,通过调研,使用了Fish-Redux进行了代码的重构,对复杂业务进行了细粒度的拆分,对逻辑和试图进行隔离,优化了代码结构。


参考文献

1、Flutter中文网,https://flutterchina.club/

2、深入理解Flutter引擎线程模式,https://mp.weixin.qq.com/s/hZ5PUvPpMlEYBAJggGnJsw

3、已开源|码上用它开始Flutter混合开发——FlutterBoost,https://mp.weixin.qq.com/s/v-wwruadJntX1n-YuMPC7g

4、Fish-Redux介绍文档,https://github.com/alibaba/fish-redux/tree/master/doc


作者简介

万兵 :58同城房产技术部-Android开发工程师。主要负责58和安居客APP租房和商业地产业务的开发和维护工作。

相关推荐

iphone6自定义铃声设置流程(iphone6怎么自定义来电铃声)

苹果iphone6自定义铃声如何设置,iphone6自定义铃声怎么设置,iphone6自定义铃声设置教程,下面小编给大家分享一下。设置自定义铃声首先要自己制作或者找到一个铃声音频。1、在电脑上面把已经...

iphone手机三分钟更换铃声,无需电脑直接手机操作

iPhone怎么在手机上换铃声?无需电脑!1分钟教你给苹果手机换铃声众所周知,苹果手机的ios系统是比较封闭的,封闭系统就会给我们带来一些不便,这里要说的就是苹果手机更换个性化铃声就比较麻烦,因为io...

iPhone手机个性铃声设置详细教程(iphone个性铃声怎么设置)

iPhone现在已成街机了。朋友聚在一起的时候,是不是总有种以为是自己手机响了的赶脚。那么,小编今天跟大家分享一下iPhone怎么换铃声?,让你轻松设置属于你的个性铃声。1:电脑端安装iTools,安...

iPhone也能用自己喜欢的铃声了,2分钟包搞定!

听到超好听的铃声,怎样才能放进iPhone里?这貌似是一道千古难题。90%的iPhone小白:听到这么爽脆带感的iPhone铃声,我要用!我要用!我要用!视频然而打开iTunes发现,脑子一片空白……...

苹果iOS 26隐藏新铃声曝光:强调“玻璃质感”

IT之家6月20日消息,苹果在iOS26中隐藏了一个新的铃声,这是现有默认铃声“Reflection”的改版。“Reflection”自2017年iPhoneX问世以来一直是系...

苹果ios14充电提示音怎么设置 iPhone手机修改充电提示音教程

ios14充电提示音最近很火爆,大家都想要知道苹果设置充电提示音的方法,个性化的设置非常吸引大家,小编也会在这里教大家ios14苹果充电提示音的设置,操作的流程会直接分享在下方,各位玩家们都能一起来看...

苹果iphone手机直接设置铃声教程(ios怎么直接设置铃声)

iPhone苹果16自定义来电铃声攻略(ios自定义铃声怎么设置)

在智能手机的个性化设置中,更改来电铃声是展现自我风格的一种方式。对于使用iPhone苹果16的用户来说,设置一个独特的来电铃声,不仅能提升接听体验,还能让日常生活更加多彩。以下是如何为iPhone苹果...

未越狱iPhone用户自定义来电铃声和短信铃声的教程

其实自定义来电铃声和短信铃声的过程没有什么区别,但要注意,来电铃声的播放时间不能超过40秒,短信铃声的播放时间不能超过30秒,这也就是说长度为30-40秒的仅在iPhone的来电铃声中出现,少于30秒...

苹果手机怎么设置闹钟铃声?更改为歌曲铃声,亲测有效

很不是有很多小伙伴每天早上都被苹果手机刺耳的“雷达”闹钟铃声给吵醒呢?想要更换一个舒缓的闹钟铃声,却发现自己鼓捣半天却无法更换喜欢的歌曲闹钟铃声。苹果手机怎么设置闹钟铃声?下面小编就来分享如何将苹果手...

独家教程:iPhone手机铃声制作与更换,一般人我不告诉他

今天刚好自己作铃声。。想到吧里好多人都会问怎么制作铃声。于是顺便截图发吧里。虽然百度一下铃声制作已经泛滥。但是还是会一直有小白问。所以这里会详细图文说明。先介绍小白式铃声制作。http://www....

苹果用户iTunes自制自定义铃声教程

怎么制作iphone6铃声,如何使用iTunes剪切音乐自制铃声,新版iTunes怎么自制铃声,相信很多苹果用户都想使用一些个性化铃声。那么就学习一下这个方法吧。1、在电脑上面打开iTunes,选择编...

轻松学会!苹果手机怎么设置铃声来电铃声(3个方法)

在日常生活中,手机的铃声扮演着非常重要的角色,不仅是接收来电的提示,更是展示个人品位和风格的方式之一。而对于苹果手机用户来说,定制来电铃声可以让手机更具个性化,并且让您在繁忙的环境中更容易识别重要的来...

iPhone13怎么设置来电铃声?苹果13自定义铃声操作教程

我们知道,苹果iOS系统是相对封闭的,在很多功能设置上与安卓机大有不同。就如来电铃声,很多苹果用户就不喜欢使用默认的那些来电铃声,想要自定义铃声,却不知如何操作。最近,新购入iPhone13系列机型的...

iPhone 免费铃声:iOS 26 终于让设置变得更简单

苹果终于开窍了——省去了最烦人的繁琐步骤。iOS26终结了多年来困扰iPhone用户的麻烦操作:现在无需通过GarageBand应用,仅需轻点几下就能设置自定义铃声。完全免费、直接操作且极...

取消回复欢迎 发表评论: