Flutter到家助手实践

栏目: IT技术 · 发布时间: 4年前

内容简介:从2019年8月起“到家助手App”在团队正式使用Flutter进行App开发,其中Flutter开发页面占比约70%。目前此占比在行业内是比较高的一个值,而Flutter做为一个比较新的“跨端开发框架”,在引入过程中遇到了不少问题,踩了不少坑。所以本文会介绍团队是如何引入Flutter到现有App开发的过程,供读者参考借鉴。在介绍引入Flutter技术前,需要介绍一下Flutter是什么?Flutter是谷歌的跨端移动UI框架 ,可以快速在iOS和Android上构建高质量的原生用户界面。

目的

从2019年8月起“到家助手App”在团队正式使用Flutter进行App开发,其中Flutter开发页面占比约70%。目前此占比在行业内是比较高的一个值,而Flutter做为一个比较新的“跨端开发框架”,在引入过程中遇到了不少问题,踩了不少坑。所以本文会介绍团队是如何引入Flutter到现有App开发的过程,供读者参考借鉴。

Flutter概况

在介绍引入Flutter技术前,需要介绍一下Flutter是什么?Flutter是谷歌的跨端移动UI框架 ,可以快速在iOS和Android上构建高质量的原生用户界面。

Flutter到家助手实践

从2018年12月1.0版本正式发布,就受到移动研发工作者的广泛关注与应用,如上图所示:在GitHub上的Stars 数量是68,000 ,使用了短短一年,在Stars数量上就接近了同行业领头羊React Native(以下简称RN),RN从发布至今已经有 4 年有余,拥有较为完善的开发生态。

跨端方案选择

每个移动前端团队在提升开发效率上都会重点关注跨端方案的选择,目前行业主流方案有两个分别是RN与Flutter,接下来会从四个方面对两个主流跨端方案进行对比:

Flutter到家助手实践

成熟度:RN已经发布多年,行业对其也比较认可,积累了丰富的开发组件。所以在此方面RN目前更有优势,但随着时间推移,而且在Google的大力推动下,会有越来越多开发者的参与,相信不久的将来Flutter开发生态也会越加成熟;

性能:在渲染技术方面Flutter 一开始就定义了一个目标:“以每秒120帧的速度持续渲染”,正是基于此目标,Google选择了自己实现(GDI),避免了RN的那种通过桥接器与Javascript通讯导致效率低下的问题。

如下图所示,在XCode里的Debug View Hierarchy可以看到,Flutter View的布局是一个整体。而RN的渲染上采用的是把<View>标签里的内容转成原生View,虽然比基于Cordova的HTML5好很多。但一旦遇到嵌套层级多view的情况时,就很容易出现页面渲染瓶颈。所以在性能方面比RN更胜一筹。

Flutter到家助手实践

调试效率:Flutter 在Debug时“革命性”的引入JIT(动态编译),在开发过程中Flutter通过JIT 可以实现中Hot Reload (热更新),开发者可以在不用重新运行程序的情况下就可以快速刷新开发页面,提升了开发效率。

而Release时使用的是AOT(静态编译),效率等同于原生的代码的执行效率。所以在调试效率上的表现,Flutter也更优于RN;

UI一致性:“一套代码,多端运行”是跨平台开发方案都在倡导的,但在UI一致性上RN让很多开发者又爱又恨。

因为RN框架本身不负责界面渲染,而是由原生代理实现。所以开发者除了要掌握RN框架外,还必须同时熟悉 iOS 和 Android 系统的前端匹配等相关知识。

而Flutter则闭环了渲染过程,不依赖于底层操作系统提供的任何组件,从根本上保证了视图渲染在 Android 和 iOS 上的高度一致性。近期Flutter还发布了Web方面的支持,未来前景可期;

Flutter到家助手实践

局限

对一项新技术不能全看优势的方面,也要看到一些局限的地方,Flutter局限地方如下:

  • 包大小提升:使用Flutter后应用包大小会明显提升,iOS ipa包会变大10M左右,Android apk包会变大6M左右;

  • 学习成本高:Flutter作为全新技术学习成本较高,是众多开发者犹豫进场的最主要原因;

  • 生态相对弱:Flutter作为新技术在第三方库数量相对较少,很多功能需要自己造“轮子”;

  • 原生功能限制:部分原生功能Push、地图、定位、蓝牙等功能还是需要借助原生实现;

组员学习

Flutter到家助手实践

针对Flutter“学习成本高”这一问题,团队在引入新技术到团队开发之前,前期先整理Flutter的学习框架,再根据团队情况与时间排期,进行了学习项优先级分阶。

上图中的标红项,“基础Widget”、“网络与存储”、“工程管理”、“打包”等四项是需要优先组织团队培训学习。另外,其他项如“测试”、“调试”、“线上运维”等其他项为非业务阻碍项,就暂时调低其学习优先级。

在确定了学习优先级后,分两期对组员进行内部培训,第一期内容是“基础Widget”使用,第二期是“网络与存储”与“工程管理”。经过一个月的培训,使团队的每个成员具备了Flutter的基础开发能力,可以进行业务开发。

另外,笔者也建议大家要学习上图的其他项,像“状态管理”,“跨组件通信”等,都是将来学习或开发第三方Flutter插件的重要基础。

落地项目

确保团队成员有开发能力后,就要去思考如何进行落地项目? 在落地项目的程中,需要面临的问题有三个,分别是“协同开发”,“混合开发”和“容灾降级”。

问题

  • 协同开发:由于是Dart是新的开发语言,而团队的原技术栈分别是Android与iOS,所以需要解决三端代码管理与协同开发;

  • 混合开发:大多数的项目是需要采取混合开发(Android/iOS +Flutter),那么Native页面与Flutter页面的相互跳转,是需要关注并解决的问题;

  • 容灾降级:在Flutter技术引入前期,我们对部分Native页面进行了Flutter改造,所以要考虑极端情况下,若Flutter页不可用可以降级回原来的Native页面;

“协同开发”方案

“协同开发”需要关注的事项有三个,分别是“代码仓库模式”、“原工程支持Flutter开发”、和“混合栈框架”,接下来分别对这三个事项进行详细介绍。

两种仓库模式

谈到协同开发就需要涉及到代码仓库调整,所以需提前介绍一下Flutter开发的两种模式,其中一种是“混合模式”,另外一种是“产物模式”,以下是两种模式的优缺点说明:

Flutter到家助手实践

在经历前期培训后,团队组员已掌握Flutter的开发技术,所以“混合模式”在当前情况下,是比较适合我们团队的代码管理方式。未来会考虑迭代使用“产物模式”来进行代码管理,这样的好处是将来稳定的功能模块可以实现组件化。

原生工程支持Flutter开发

iOS平台配置

可能需要做一个假设,你已有的iOS工程路径在 ~/integratedFlutter 目录中,你需要在这个目录里创建 Flutter 的模块:

$ cd ~/integratedFlutter

$ flutter create -t module my_flutter

在 my_flutter 目录中 flutter 会创建一些 Dart 代码,一些 Ruby 脚本,启动项以及一个隐藏的.ios子目录。 着在你的 Podfile 文件中添加以下脚本:

platform :ios, '8.0'


target 'integratedFlutter' do

flutter_application_path = '../my_flutter/'

eval(File.read(File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')), binding)


end

Flutter工程项目与原生工程是同一级目录,但git地址是单独分开的

Flutter到家助手实践

$ pod install

等待安装完成。

使用 Xcode 打开 integratedFlutter.xcworkspace,在 Build Phases 中创建一个新的 Script Phases ,将如下两行粘贴到 Shell 中:

"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build

"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" embed

最后将 Build Settings -> Build Options -> Enable Bitcode 设置为 No,运行 command + b 编译工程;

Android平台配置

创建一个flutter module

flutter create -t module --org com.example my_flutter

在原生根项目的settings.gradle加入如下配置信息

// MyApp/settings.gradle

include ':app' // assumed existing content

setBinding(new Binding([gradle: this])) // new

evaluate(new File( // new

settingsDir.parentFile, // new

'my_flutter/.android/include_flutter.groovy' // new

))

在原生App模块中加入flutter依赖

dependencies {

implementation project(':flutter')

}

添加依赖后就可以与原生项目一起编译了。 这种方式虽可以满足混编需求,但还不是特别方便,主要是开发完项目后,还需要去Android Studio项目中进行编译,比较麻烦。 所以我们也可以把Flutter项目settings.gradle改造,在Flutter开发环境下直接运行包含原生代码的混合项目,改造方式如下:

// MyApp/settings.gradle

//projectName 原生模块名称

//projectPath 原生项目路径

include ":$projectName"

project(":$projectName").projectDir = new File("$projectPath")


这样改造之后不仅可以在Flutter IDE中直接编译Flutter混合工程,并进行调试。也可以运行futter run来启动Flutter混合工程。不过在配置的时候,需要注意Flutter中 gradle编译环境和原生编译环境的一致性,如果不一致可能会导致编译错误。

混合栈框架

混合导航栈,指的是Flutter页面与原生Native页面相互融合,展现在用户视角的页面中。

Flutter Page1 -> Flutter Page2 -> Native Page1 -> Flutter Page3

混合框架由来

从iOS和Android里每初始化一个Flutter实例就需要初始化一个Flutter Engine,而每个Flutter Engine 会要求Embeder提供四个Task Runner,这些Runner用于渲染相关工作,同时这些实例内存不能共享,这样会带来较大的系统资源消耗。如果不控制Flutter Engine数量,App很容易就出现内存暴涨,进而App进程被系统强杀。

由于Google 推出Flutter SDK时就是希望我们能直接使用Flutter 进行新App的开发,这样所有页面都是Flutter页,只用初始化一个Flutter Engine了,也就不会有内存使用问题。所以 Google官方目前暂没有推出自己主导混合开发框架。

然而在真实的开发情况是,大多数开发团队会从原有Native项目开始,进行功能开发或功能页改造来引入Flutter,目的是为了让团队有Flutter开发能力,同时也能支撑业务开发。

所以针对混合页面栈的内存控制,是大多数团队的真实需求。目前行业在混合栈方面主要有两种解决方案,一个是以“今日头条”为代表的通过修改FlutterEngine源码实现多Flutter实例,在底层共享Isolate。另外一个是以“闲鱼”为代表的FlutterBoost ,它是通过共享一个FlutterView来进行页面切换的导航方案。经过调研之后团队最终选择了第二种方案。

混合栈机制说明

FlutterBoost实现了一个管理多个Navigator的对象。当前最多只会有一个可见的Flutter 容器(Navigator),这个Navigator所包含的页面也就是我们当前可见容器所对应的页面。

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

Flutter到家助手实践

用一个简单的例子描述一个混合栈新页面创建的过程:

  • 创建Native容器(iOS ViewController,Android Activity or    Fragment);

  • Native容器通过消息机制通知Flutter Coordinator新的容器被创建;

  • Flutter Container Manager进而得到通知,负责创建出对应的Flutter容器,并且在其中装载对应的Widget页面;

  • 当Native容器展示到屏幕上时,容器发消息给Flutter Coordinator通知要展示页面的id,如上图“原生Tab”->“flutter_tab”,就是通过获取原生的Tab切换消息,然后把消息传递给Flutter Coordinator;

  • Flutter Container Manager找到对应id的Flutter Container并将其设置为前台可见容器;

上述过程是一个混合栈新页面创建的主要逻辑,销毁和进入后台等操作也类似有Native容器事件去进行驱动;

混合栈使用

  • 注册路由:在Flutter的Main程序初始化时,把相应name id 和name Id对应要创建的Widget进行注册;

@override

void initState() {

super.initState();

FlutterBoost.singleton.registerPageBuilders({

'first': (pageName, params, _) => FirstMethodChannelWidget(),

'second': (pageName, params, _) => SecondRouteWidget(),

'tab': (pageName, params, _) => TabRouteWidget(),

'tab1':(pageName,params,_)=> TabRouteWidget1(),

'platformView': (pageName, params, _) => PlatformRouteWidget(),

'flutterFragment': (pageName, params, _) => FragmentRouteWidget(params),

///可以在native层通过 getContainerParams 来传递参数

'flutterPage': (pageName, params, _) {

print("flutterPage params:$params");

return FlutterRouteWidget(params:params);

},

});

}

@override

Widget build(BuildContext context) {

return MaterialApp(

title: 'Flutter Boost example',

builder: FlutterBoost.init(postPush: _onRoutePushed),

home: Container());

}

void _onRoutePushed(

String pageName, String uniqueId, Map params, Route route, Future _) {

}

  • 原生注册:在创建相应的原生容器,并设置相应name id,确保Flutter 的Coordinator能够正确的找到相应的Widget;

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions

{

PlatformRouterImp *router = [PlatformRouterImp new];

[FlutterBoostPlugin.sharedInstance startFlutterWithPlatform:router

onStart:^(FlutterEngine *engine) {



}];

self.window = [[UIWindow alloc] initWithFrame: [UIScreen mainScreen].bounds];



[self.window makeKeyAndVisible];

UIViewControllerDemo *vc = [[UIViewControllerDemo alloc] initWithNibName:@"UIViewControllerDemo" bundle:[NSBundle mainBundle]];

vc.tabBarItem = [[UITabBarItem alloc] initWithTitle:@"原生Tab" image:nil tag:0];



FLBFlutterViewContainer *fvc = FLBFlutterViewContainer.new;

[fvc setName:@"tab" params:@{}];

fvc.tabBarItem = [[UITabBarItem alloc] initWithTitle:@"flutter_tab" image:nil tag:1];

FLBFlutterViewContainer *fvc1 = FLBFlutterViewContainer.new;

[fvc1 setName:@"tab1" params:@{}];

fvc1.tabBarItem = [[UITabBarItem alloc] initWithTitle:@"flutter_tab2" image:nil tag:2];



UITabBarController *tabVC = [[UITabBarController alloc] init];

UINavigationController *rvc = [[UINavigationController alloc] initWithRootViewController:tabVC];



router.navigationController = rvc;

tabVC.viewControllers = @[vc,fvc,fvc1];

self.window.rootViewController = rvc;

[router setupFlutterCallNative];

return YES;

}

容灾降级方案

容灾必要性:

在刚引入Flutter技术时,团队其实也有业务安全方面担心。主要表现为若App使用了Flutter技术会不会导致线上业务受到影响?所以为了业务安全,开发初期阶段,特别是在对原有Native页面进行Flutter改造时,是有必要使用原生容灾降级。这样做不仅能锻炼团队使用新技术开发信心,也能确保线上业务不受影响;

容灾实现:

容灾的实现是在Native程序初始化时会获取Flutter功能配置信息,然后在决定进入某一个Flutter page前,先使用配置文件进行判断,若被使用Flutter页面能正常运行,则直接使用Flutter页面展现。若被使用的Flutter页面线上运行时因某种原因(如:数据解析出错)不能正常运行,则降级使用原生页面展示;

Flutter到家助手实践

  • 获取Flutter功能配置信息

//获取Flutter配置信息

- (void)requestFlutterConfigInfo{

self.flutters = [NSMutableArray array];

NSString *url;

....

NSURL *downloadURL = [NSURL URLWithString:url];

NSURLRequest *request = [NSURLRequest requestWithURL:downloadURL

cachePolicy:NSURLRequestReloadIgnoringLocalCacheData

timeoutInterval:20.0];

// 创建文件下载

NSURLSessionDataTask *task = [[NSURLSession sharedSession]

dataTaskWithRequest:request

completionHandler:^(NSData *data, NSURLResponse *response, NSError *error)

{

if (!error) {

NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];

NSArray *res = JDUtils.safeGetArrayValue([dict objectForKey:@"flutters"]);

if (res && res.count) {

[self.flutters addObjectsFromArray:res];

}

}

}];

[task resume];

}

  • 判断功能是否可用

/**

是否显示门店列表

@return YES 显示, NO 不显示

*/

- (BOOL)isShowStationList;

....

- (void)shareBgViewTapped:(UITapGestureRecognizer *)recognizer {

if ([FlutterRouter.sharedRouter isShowShareView]) {

//使用Flutter页面

[FlutterRouter.sharedRouter openPage:FlutterPageShare params:@{} animated:YES completion:^(BOOL isFinish){}];

}else{

//使用原生页面

PHShareViewController *vc = [[PHShareViewController alloc] init];

vc.hidesBottomBarWhenPushed = YES;

[self.navigationController pushViewController:vc animated:YES];

}

}

成果展示

Flutter技术引入到家助手App开发后,对业务开发的帮助主要体现在以下两个方面:

1.解决开发人员配比不均:

“到家助手App”前端团队的服务用户主要是线下商家,而线下商家经常会有一些设备定制化开发需求,其中设备主要机型系统是Android操作系统,因此前端团队在Android开发人数上比iOS的人数更高一些,在“到家助手App”的前端团队里Android与iOS的人员比例是 2:1 ,行业内此比例一般是1:1。

在平常业务开发中并没有什么问题,但是在业务需求爆发的情况下,会出现因iOS人员总开发人力少,导致业务响应速度变慢的风险。

在引入Flutter开发技术后,大多数新功能都使用Flutter开发,不仅解决了业务响应速度问题,还使得原来的iOS开发人员可以通过Flutter“反哺”支持商家Android设备开发需求。

2.统一开发规范:

在未引入Flutter之前,Android与iOS的开发人员在完成业务开发后,会各自进行代码Review。而引入Flutter技术后,团队基于Flutter探索出适合于统一的开发规范,原来的Android与iOS可以一起进行代码Review。

界面展示

在“到家助手App”的Flutter页面里,有简单页面,也有复杂业务功能:

Flutter到家助手实践

简单功能

Flutter到家助手实践 复杂业务

由于篇幅所限,截图中没有展示“到家助手App”里所有使用Flutter开发的页面。但这并不影响表达一个信息,那就是Flutter是能真实的用于提升前端团队的开发效率。

小结

本文介绍了团队如何引入Flutter技术到App开发的整体过程,其中“组员学习”介绍了通过“分阶学习”来保证组员“从0到1”快速掌握新技术。在“落地项目”中讲解了如何解决“协同开发”、“混合开发”、“容灾降级” 等共性问题。希望对读者在引入Flutter开发时能有所帮助。

相关阅读:

官方 iOS 混编方案

https://github.com/flutter/flutter/wiki/Add-Flutter-to-existing-apps

Flutter的编译模式

https://stephenwzl.github.io/2018/07/30/flutter-compile-mode/

干货 | 京东技术中台的Flutter实践之路

https://www.jianshu.com/p/70573c55da91

Flutter核心技术与实战

https://time.geekbang.org/column/intro/200

Flutter到家助手实践


以上所述就是小编给大家介绍的《Flutter到家助手实践》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

A Byte of Python

A Byte of Python

Swaroop C H / Lulu Marketplace / 2008-10-1 / USD 27.98

'A Byte of Python' is a book on programming using the Python language. It serves as a tutorial or guide to the Python language for a beginner audience. If all you know about computers is how to save t......一起来看看 《A Byte of Python》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具