关于跨平台开发:钉钉协同引擎与应用场景技术探索

作者:孙然(煮虾) 协同引擎是钉钉面向跨平台终端并解决分布式数据一致性的 Runtime 与开发框架。 它次要解决三类场景问题: 多人实时协同跨端同步与利用状态接力离线可用 Local First同时,它也是面向凋谢的。一方产品能够用,三方开发者(小程序、H5 利用)也能够用。 协同引擎Demo Show多人实时协同:一起标注从钉钉客户端 6.0.0 开始,音讯会话里的图片新增了“一起标注”性能。群成员能够对一张图片进行同时勾画,并且所有人都能看到其它人的实时笔迹: 在这里,协同引擎 SDK 提供了一套 CoCanvas(协同画板)组件,图片音讯通过援用 CoCanvas 组件疾速组装出多人协同能力。除了解决多人实时数据(笔迹数据)的协同问题,协同引擎还提供了以后正在协同的参与者信息,用于业务实现展现诸如“多少人正在标注”信息的性能。 跨端同步:聊天草稿你是否有遇到过这样的场景:手机上编辑到一半的货色,回到电脑前想持续编辑?或者,手机上截了个图,要在电脑上持续编辑。当初看来,你得在手机上先把信息或图片发给本人,而后再在电脑里下载下来持续操作。整个流程会被跨端传输过程打断。 如果用协同引擎,整个流程或者能够变成这样——你能够实现端到端的输出同步: 或者在一端复制,间接在另一端粘贴: 实现手机端和电脑端一系列连贯操作的无缝连接! 在下面的例子中,协同引擎提供了 CoString、CoMap、CoList 等根底数据结构,CoImage、CoClipBoard 等组件,让下层业务共享数据就像读写本地变量一样简略。业务只须要关怀业务自身须要解决哪些数据,而无需关怀跨端须要额定做些什么。将来协同引擎也能够持续提供诸如 CoCamera、CoAlbum 等高阶组件,让桌面端利用能够实现拉起手机相册选图、拉起手机相机拍照等更高级的性能。 跨端同步,是让同一用户的多台物理设施,变成一台对立的逻辑设备,共享数据、共享零碎设施、共享利用状态。因为协同引擎自身曾经具备了最根本的数据同步性能,所以也可能轻松反对这些需要场景。 面向凋谢:三方利用如何实现跨端接力协同引擎作为跨平台底层引擎同时也赋能与三方利用,助力实现: 一次开发多端运行:作为三方利用跨端协同的载体,开发一套代码多端运行(挪动/桌面)的小程序桌面端大屏生产力:桌面端小程序主动适配为大屏模式,充分发挥桌面端差异化大屏生产力劣势多人协同:基于协同引擎,实现多人工作合作跨端接力:基于协同引擎,实现跨端利用接力:手机上利用操作状态,主动接力到桌面端大屏关上离线可用:Local First。基于协同引擎,数据都在本地,工作亦可离线提交,协同引擎会确保工作最终胜利Backend As A Service:所有数据(同步、协同、接力)基于协同引擎,开发者只需写前端代码,毋庸开发服务端这外面协同引擎做了什么? 作为小程序数据层,Backend As A Service多人编辑操作的数据实时协同跨端数据同步,利用状态迁徙接力协同引擎简介协同引擎是【面向跨平台终端的】【解决分布式数据一致性的】【Runtime 与开发框架】。 解决的问题协同引擎次要解决三类场景问题: 多人实时协同跨端同步与利用状态接力离线可用 Local First 以上三类问题,即使不应用协同引擎,也有其余实现形式。但这些实现形式对于业务方而言,要解决很多“协同”畛域的底层工程问题乃至算法问题,例如网络解决、本地数据存储、长时间离线解决、多人实时操作合并、回滚与抵触解决等。 协同引擎作为开发框架,提供了通用的协同能力,并以协同变量/组件的接入形式解决上述问题: 申明式应用数据。业务方间接应用协同变量,就像应用本地变量一样,能够毋庸关注底层的操作合并、回滚、抵触解决等问题。跨平台实现,全平台可用。让各端(Android/iOS/Windows/Mac/端外web)、各种状态(Native/H5/小程序)的利用都能够应用协同能力。API与基本概念协同引擎在下层的开发框架中提供了一套协同数据结构和 API。它们和一般的容器变量类似,然而却自带了协同能力。例如一个云协同的计数器就能够这样通过 CoCounter 协同变量解决: Container container = Loader.getContainer(url);CoCounter counter = container.getCoCounter("my_counter");// 点击+1按钮时counter.add();// 监听数据变动counter.addEventListener(new CoCounterValueChangedListener() { @Override public void onValueChanged(int newValue) { // 更新UI updateView(newValue); }});这里,咱们提出了几个根本的概念 ...

March 28, 2022 · 1 min · jiezi

flutter调用系统的打电话发短信发邮件功能

类似于安卓的通过设置intent-action,点击按钮就能跳转到系统的拨号,发短信,发邮件等界面引入类库url_launcher: ^5.0.3 get_it: ^1.0.3+2 创建一个服务 import 'package:url_launcher/url_launcher.dart'; class CallsAndMessagesService { void call(String number) => launch("tel:$number"); void sendSms(String number) => launch("sms:$number"); void sendEmail(String email) => launch("mailto:$email");} 初始化,注册服务,在app中任何地方都能调用GetIt locator = GetIt(); void setupLocator() { locator.registerSingleton(CallsAndMessagesService());} 在主方法中设置void main() { setupLocator(); runApp(MyApp());} 在需要的用到的地方创建 class _MyHomePageState extends State<MyHomePage> { final CallsAndMessagesService _service = locator<CallsAndMessagesService>(); final String number = "123456789"; final String email = "dancamdev@example.com"; ...} 调用代码及界面 @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('dancamdev'), ), body: Container( width: double.infinity, child: Column( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.max, children: <Widget>[ RaisedButton( child: Text( "call $number", ), onPressed: () => _service.call(number), ), SizedBox(height: 20), RaisedButton( child: Text( "message $number", ), onPressed: () => _service.sendSms(number), ), SizedBox(height: 20), RaisedButton( child: Text( "email $email", ), onPressed: () => _service.sendEmail(email), ), ], ), ), ); ...

July 8, 2019 · 1 min · jiezi

Flutter移动端实战手册

该文章属于<简书 — 刘小壮>原创,转载请注明:<简书 — 刘小壮> https://www.jianshu.com/p/d27c1f5ee3ff iOS接入Flutter在进行iOS和Flutter的混编时,iOS比Android的接入方式略复杂,但也还好。现在市面上有不少接入Flutter的方案,但大多数都是千篇一律相互抄的,没什么意义。 进行Flutter混编之前,有一些必要的文件。 xcode_backend.sh文件,在配置flutter环境的时候由Flutter工具包提供。xcconfig环境变量文件,在Flutter工程中自动生成,每个工程都不一样。xcconfig文件xcconfig是Xcode的配置文件,Flutter在里面配置了一些基本信息和路径,接入Flutter前需要先将xcconfig接入进来,否则一些路径等信息将会出错或找不到。 Flutter的xcconfig包含三个文件,Debug.xcconfig、Release.xcconfig、Generated.xcconfig,需要将这些文件配置在下面的位置,并且按照不同环境配置不同的文件。 Project -> Info -> Development Target -> Configurations 有些比较大的工程中已经在Configurations中设置了xcconfig文件,由于每个Target的一种环境只能配置一个xcconfig文件,所以可以在已有的xcconfig文件中import引入Generated.xcconfig文件,并且不需要区分环境。 脚本文件xcode_backend.sh脚本文件用来构建和导出Flutter产物,这是Flutter开发包为我们默认提供的。需要在工程Target的Build Phases加入一个Run Script文件,并将下面的脚本代码粘贴进去。需要注意的是,不要忘记前面的/bin/sh操作,否则会导致权限错误。 /bin/sh "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build/bin/sh "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" embed在xcode_backend.sh中有三个参数类型,build、thin、embed,thin没有太大意义,其他两个则负责构建和导出。 混合开发随后可以对Xcode工程进行编译,这时候肯定会报错的。但是不要慌张,报错后我们在工程主目录下会发现一个名为Flutter的文件夹,其中会包含两个framework,这个文件夹就是Flutter的编译产物,我们将这个文件夹整体拖入项目中即可。 这时候就可以在iOS工程中添加Flutter代码了,下面是详细步骤。 将AppDelegate的集成改为FlutterAppDelegate,并且需要遵循FlutterAppLifeCycleProvider代理。#import <Flutter/Flutter.h>#import <UIKit/UIKit.h>@interface AppDelegate : FlutterAppDelegate <FlutterAppLifeCycleProvider>@end创建一个FlutterPluginAppLifeCycleDelegate的实例对象,这个对象负责管理Flutter的生命周期,并从Platform侧接收AppDelegate的事件。我直接将其声明为一个属性,在AppDelegate中的各个方法中,调用其方法进行中转操作。- (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [self.lifeCycleDelegate application:application willFinishLaunchingWithOptions:launchOptions]; return YES;}- (void)applicationWillResignActive:(UIApplication *)application { [self.lifeCycleDelegate applicationWillResignActive:application];} - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation { [self.lifeCycleDelegate application:application openURL:url sourceApplication:sourceApplication annotation:annotation]; return YES;}随后即可加入Flutter代码,加入的方式也很简单,直接实例化一个FlutterViewController控制器即可,也不需要传其他参数进去(这里先不考虑多实例的问题)。FlutterViewController *flutterViewController = [[FlutterViewController alloc] init];Flutter将其看做是一个画布,实例化一个画布上去之后,任何操作其实都是在当前页面完成的。 ...

June 20, 2019 · 2 min · jiezi

移动端开发新趋势Flutter

该文章属于<简书 — 刘小壮>原创,转载请注明:<简书 — 刘小壮> https://www.jianshu.com/p/1a90adc09e99介绍Flutter是Google开发的新一代跨平台方案,Flutter可以实现写一份代码同时运行在iOS和Android设备上,并且提供很好的性能体验。Flutter使用Dart作为开发语言,这是一门简洁、强类型的编程语言。Flutter对于iOS和Android设备,提供了两套视觉库,可以针对不同的平台有不同的展示效果。Flutter原本是为了解决Web开发中的一些问题,而开发的一套精简版Web框架,拥有独立的渲染引擎和开发语言,但后来逐渐演变为移动端开发框架。正是由于Dart当初的定位是为了替代JS成为Web框架,所以Dart的语法更接近于JS语法。例如定义对象构建方法,以及实例化对象的方式等。在Google刚推出Flutter时,其发展很缓慢,终于在18年发布第一个Bate版之后迎来了爆发性增长,发布第一个Release版时增长速度更快。可以从Github上Star数据看出来这个增长的过程。在19年最新的Flutter 1.2版本中,已经开放Web支持的Beta版。目前已经有不少大型项目接入Flutter,阿里的咸鱼、头条的抖音、腾讯的NOW直播,都将Flutter当做应用程序的开发语言。除此之外,还有一些其他中小型公司也在做。整体架构Flutter可以理解为开发SDK或者工具包,其通过Dart作为开发语言,并且提供Material和Cupertino两套视觉控件,视图或其他和视图相关的类,都以Widget的形式表现。Flutter有自己的渲染引擎,并不依赖原生平台的渲染。Flutter还包含一个用C++实现的Engine,渲染也是包含在其中的。EngineFlutter是一套全新的跨平台方案,Flutter并不像React Native那样,依赖原生应用的渲染,而是自己有自己的渲染引擎,并使用Dart当做Flutter的开发语言。Flutter整体框架分为两层,底层是通过C++实现的引擎部分,Skia是Flutter的渲染引擎,负责跨平台的图形渲染。Dart作为Flutter的开发语言,在C++引擎上层是Dart的Framework。Flutter不仅仅提供了一套视觉库,在Flutter整体框架中包含各个层级阶段的库。例如实现一个游戏功能,上面一些游戏控件可以用上层视觉库,底层游戏可以直接基于Flutter的底层库进行开发,而不需要调用原生应用的底层库。Flutter的底层库是基于Open GL实现的,所以Open GL可以做的Flutter都可以。视觉库在上层Framework中包含两套视觉库,符合Android风格的Material,和符合iOS风格的Cupertino。也可以在此基础上,封装自己风格的系统组件。Cupertino是一套iOS风格的视觉库,包含iOS的导航栏、button、alertView等。Flutter对不同硬件平台有不同的兼容,例如同样的Material代码运行在iOS和Android不同平台上,有一些平台特有的显示和交互,Flutter依然对其进行了区分适配。例如滑动ScrollView时,iOS平台是有回弹效果的,而Android平台则是阻尼效果。例如iOS的导航栏标题是居中的,Android导航栏标题是向左的,等等。这些Flutter都做了区分兼容。除了Flutter为我们做的一些适配外,有一些控件是需要我们自己做适配的,例如AlertView,在Android和iOS两个平台下的表现就是不同的。这些iOS特性的控件都定义在Cupertino中,所以建议在进行App开发时,对一些控件进行上层封装。例如AlertView则对其进行一个二次封装,控件内部进行设备判断并选择不同的视觉库,这样可以保证各个平台的效果。虽然Flutter对于iOS和Android两个平台,开发有cupertino和material两个视觉库,但实际开发过程中的选择,应该使用material当做视觉库。因为Flutter对iOS的支持并不是很好,主要对Android平台支持比较好,material中的UI控件要比cupertino多好几倍。DartDart是Google在2011年推出的一款应用于Web开发的编程语言,Dart刚推出的时候,定位是替代JS做前端开发,后来逐步扩展到移动端和服务端。Dart是Flutter的开发语言,Flutter必须遵循Dart的语言特性。在此基础上,也会有自己的东西,例如Flutter的上层Framework,自己的渲染引擎等。可以说,Dart只是Flutter的一部分。Dart是强类型的,对定义的变量不需要声明其类型,Flutter会对其进行类型推导。如果不想使用类型推导,也可以自己声明指定的类型。Hot ReloadFlutter支持亚秒级热重载,Android Studio和VSCode都支持Hot Reload的特性。但需要区分的是,热重载和热更新是不同的两个概念,热重载是在运行调试状态下,将新代码直接更新到执行中的二进制。而热更新是在上线后,通过Runtime或其他方式,改变现有执行逻辑。AOT、JITFlutter支持AOT(Ahead of time)和JIT(Just in time)两种编译模式,JIT模式支持在运行过程中进行Hot Reload。刷新过程是一个增量的过程,由系统对本次和上次的代码做一次snapshot,将新的代码注入到DartVM中进行刷新。但有时会不能进行Hot Reload,此时进行一次全量的Hot Reload即可。而AOT模式则是在运行前预先编译好,这样在每次运行过程中就不需要进行分析、编译,此模式的运行速度是最快的。Flutter同时采用了两种方案,在开发阶段采用JIT模式进行开发,在release阶段采用AOT模式,将代码打包为二进制进行发布。在开发原生应用时,每次修改代码后都需要重新编译,并且运行到硬件设备上。由于Flutter支持Hot Reload,可以进行热重载,对项目的开发效率有很大的提升。由于Flutter实现机制支持JIT的原因,理论上来说是支持热更新以及服务器下发代码的。可以从服务器。但是由于这样会使性能变差,而且还有审核的问题,所以Flutter并没有采用这种方案。实现原理Flutter的热重载是基于State的,也就是我们在代码中经常出现的setState方法,通过这个来修改后,会执行相应的build方法,这就是热重载的基本过程。Flutter的hot reload的实现源码在下面路径中,在此路径中包含run_cold.dart和run_hot.dart两个文件,前者负责冷启动,后者负责热重载。~/flutter/packages/flutter_tools/lib/src/run_hot.dart热重载的代码实现在run_hot.dart文件中,有HotRunner来负责具体代码执行。当Flutter进行热重载时,会调用restart函数,函数内部会传入一个fullRestart的bool类型变量。热重载分为全量和非全量,fullRestart参数就是表示是否全量。以非全量热重载为例,函数的fullRestart会传入false,根据传入false参数,下面是部分核心代码。Future<OperationResult> restart({ bool fullRestart = false, bool pauseAfterRestart = false, String reason }) async { if (fullRestart) { // ….. } else { final bool reloadOnTopOfSnapshot = _runningFromSnapshot; final String progressPrefix = reloadOnTopOfSnapshot ? ‘Initializing’ : ‘Performing’; final Status status = logger.startProgress( ‘$progressPrefix hot reload…’, progressId: ‘hot.reload’ ); OperationResult result; try { result = await _reloadSources(pause: pauseAfterRestart, reason: reason); } finally { status.cancel(); } }}调用restart函数后,内部会调用_reloadSources函数,去执行内部逻辑。下面是大概逻辑执行流程。在_reloadSources函数内部,会调用_updateDevFS函数,函数内部会扫描修改的文件,并将文件修改前后进行对比,随后会将被改动的代码生成一个kernel files文件。随后会通过HTTP Server将生成的kernel files文件发送给Dart VM虚拟机,虚拟机拿到kernel文件后会调用_reloadSources函数进行资源重载,将kernel文件注入正在运行的Dart VM中。当资源重载完成后,会调用RPC接口触发Widgets的重绘。跨平台方案对比现在市面上RN、Weex的技术方案基本一样,所以这里就以RN来代表类似的跨平台方案。Flutter是基于GPU进行渲染的,而RN则将渲染交给原生平台,而自己只是负责通过JSCore将视图组织起来,并处理业务逻辑。所以在渲染效果和性能这块,Flutter的性能比RN要强很多。跨平台方案一般都需要对各个平台进行平台适配,也就是创建各自平台的适配层,RN的平台适配层要比Flutter要大很多。因为从技术实现来说,RN是通过JSCore引擎进行原生代码调用的,和原生代码交互很多,所以需要更多的适配。而Flutter则只需要对各自平台独有的特性进行适配即可,例如调用系统相册、粘贴板等。Flutter技术实现是基于更底层实现的,对平台依赖度不是很高,相对来说,RN对平台的依赖度是很高的。所以RN未来的技术升级,包括扩展之类的,都会受到很大的限制。而Flutter未来的潜力将会很大,可以做很多技术改进。Widget在Flutter中将显示以及和显示相关的部分,都统一定义为widget,下面列举一些widget包含的类型:用于显示的视图,例如ListView、Text、Container等。用来操作视图,例如Transform等动画相关。视图布局相关,例如Center、Expanded、Column等。在Flutter中,所有的视图都是由Widget组成,Label、AppBar、ViewController等。在Flutter的设计中,组合的优先级要大于继承,整体视图类结构继承层级很浅但单层很多类。如果想定制或封装一些控件,也应该以组合为主,而不是继承。在iOS开发中,我也经常采用这种设计方案,组合大于继承。因为如果继承层级过多的话,一个是不便于阅读代码,还有就是不好维护代码。例如底层需要改一个通用的样式,但这个类的继承层级比较复杂,这样改动的话影响范围就比较大,会将一些不需要改的也改掉,这时候就会发现继承很鸡肋。但在iOS中有Category的概念,这也是一种组合的方式,可以通过将一些公共的东西放在Category中,使继承的方便性和组合的灵活性达到一个平衡。Flutter中并没有单独的布局文件,例如iOS的XIB这种,代码都在Widget中定义。和UIView的区别在于,Widget只是负责描述视图,并不参与视图的渲染。UIView也是负责描述视图,而UIView的layer则负责渲染操作,这是二者的区别。了解Widget在应用程序启动时,main方法接收一个Widget当做主页面,所以任何一个Widget都可以当做根视图。一般都是传一个MaterialApp,也可以传一个Container当做根视图,这都是被允许的。在Flutter应用中,和界面显示及用户交互的对象都是由Widget构成的,例如视图、动画、手势等。Widget分为StatelessWidget和StatefulWidget两种,分别是无状态和有状态的Widget。StatefulWidget本质上也是无状态的,其通过State来处理Widget的状态,以达到有状态,State出现在整个StatefulWidget的生命周期中。当构建一个Widget时,可以通过其build获得构建流程,在构建流程中可以加入自己的定制操作,例如对其设置title或视图等。return Scaffold( appBar: AppBar( title: Text(‘ListView Demo’), ), body: ListView.builder( itemCount: dataList.length, itemBuilder: (BuildContext context, int index) { return Text(dataList[index]); }, ),);有些Widget在构建时,也提供一些参数来帮助构建,例如构建一个ListView时,会将index返回给build方法,来区别构建的Cell,以及构建的上下文context。itemBuilder: (BuildContext context, int index) { return Text(dataList[index]);}StatelessWidgetStatelessWidget是一种静态Widget,即创建后自身就不能再进行改变。在创建一个StatelessWidget后,需要重写build函数。每个静态Widget都会有一个build函数,在创建视图对象时会调用此方法。同样的,此函数也接收一个Widget类型的返回值。class RectangleWidget extends StatelessWidget { @override Widget build(BuildContext context) { return Center ( // UI Code ); }}StatefulWidgetWidget本质上是不可被改变的,但StatefulWidget将状态拆分到State中去管理,当数据发生改变时由State去处理视图的改变。下面是创建一个动态Widget,当创建一个动态Widget需要配合一个State,并且需要重写createState方法。重写此函数后,指定一个Widget对应的State并初始化。下面例子中,在StatefulWidget的父类中包含一个Key类型的key变量,这是无论静态Widget还是动态Widget都具备的参数。在动态Widget中定义了自己的成员变量title,并在自定义的初始化方法中传入,通过下面DynamicWidget类的构造方法,并不需要在内部手动进行title的赋值,title即为传入的值,是由系统完成的。class DynamicWidget extends StatefulWidget { DynamicWidget({Key key, this.title}) : super (key : key); final String title; @override DynamicWidgetState createState() => new DynamicWidgetState();}由于上面动态Widget定义了初始化方法,在调用动态Widget时可以直接用自定义初始化方法即可。DynamicWidget(key: ‘key’, title: ’title’);StateStatefulWidget的改变是由State来完成的,State中需要重写build方法,在build中进行视图组织。StatefulWidget是一种响应式视图改变的方式,数据源和视图产生绑定关系,由数据源驱动视图的改变。改变StatefulWidget的数据源时,需要调用setState方法,并将数据源改变的操作写在里面。使用动态Widget后,是不需要我们手动去刷新视图的。系统在setState方法调用后,会重新调用对应Widget的build方法,重新绘制某个Widget。下面的代码示例中添加了一个float按钮,并给按钮设置了一个回调函数_onPressAction,这样在每次触发按钮事件时都会调用此函数。counter是一个整型变量并和Text相关联,当counter的值在setState方法中改变时,Text Widget也会跟着变化。class DynamicWidgetState extends State<DynamicWidget> { int counter = 0; void _onPressAction() { setState(() { counter++; }); } @override Widget build(BuildContext context) { return new Scaffold( body: Center( child: Text(‘Button tapped $_counter.’) ), floatingActionButton: FloatingActionButton( onPressed: _onPressAction, tooltip: ‘Increment’, child: Icon(Icons.add) ) ); } }主要Widget在iOS中有UINavigationController的概念,其并不负责显示,而是负责控制各个页面的跳转操作。在Flutter中可以将MaterialApp理解为iOS的导航控制器,其包含一个navigationBar以及导航栈,这和iOS是一样的。在iOS中除了用来显示的视图外,视图还有对应的UIViewController。在Flutter中并没有专门用来管理视图并且和View一对一的类,但从显示的角度来说,有类似的类Scaffold,其包含控制器的appBar,也可以通过body设置一个widget当做其视图。themetheme是Flutter提供的界面风格API,MaterialApp提供有theme属性,可以在MaterialApp中设置全局样式,这样可以统一整个应用的风格。new MaterialApp( title: title, theme: new ThemeData( brightness: Brightness.dark, primaryColor: Colors.lightBlue[800], accentColor: Colors.cyan[600], ));如果不想使用系统默认主题,可以将对应的控件或试图用Theme包起来,并将Theme当做Widget赋值给其他Widget。new Theme( data: new ThemeData( accentColor: Colors.yellow, ), child: new FloatingActionButton( onPressed: () {}, child: new Icon(Icons.add), ),);有时MaterialApp设定的统一风格,并不能满足某个Widget的要求,可能还需要有其他的外观变化,可以通过Theme.of传入当前的BuildContext,来对theme进行扩展。Flutter会根据传入的context,顺着Widget树查找最近的Theme,并对Theme复制一份防止影响原有的Theme,并对其进行扩展。new Theme( data: Theme.of(context).copyWith(accentColor: Colors.yellow), child: new FloatingActionButton( onPressed: null, child: new Icon(Icons.add), ),);网络请求Flutter中可以通过async、await组合使用,进行网络请求。Flutter中的网络请求大体有三种:系统自带的HttpClient网络请求,缺点是代码量相对而言比较多,而且对post请求支持不是很好。三方库http.dart,请求简单。三方库dio,请求简单。http网络库http网络库定义在http.dart中,内部代码定义很全,包括HttpStatus、HttpHeaders、Cookie等很多基础信息,有助于我们了解http请求协议。因为是三方库,所以需要在pubspec.yaml中加入下面的引用。http: ‘>=0.11.3+12’下面是http.dart的请求示例代码,可以看到请求很简单,真正的请求代码其实就两行。生成一个Client请求对象,调用client实例的get方法(如果是post则调用post方法),并用Response对象去接收请求结果即可。通过async修饰发起请求的方法,表示这是一个异步操作,并在请求代码的前面加入await,修饰这里的代码需要等待数据返回,需要过一段时间后再处理。请求回来的数据默认是json字符串,需要对其进行decode并解析为数据对象才可以使用,这里使用系统自带的convert库进行解析,并解析为数组。import ‘package:http/http.dart’ as http;class RequestDemoState extends State<MyHomePage> { List dataList = []; @override void initState() { super.initState(); loadData(); } // 发起网络请求 loadData() async{ String requestURL = ‘https://jsonplaceholder.typicode.com/posts'; Client client = Client(); Response response = await client.get(requestURL); String jsonString = response.body; setState(() { // 数据解析 dataList = json.decode(jsonString); }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title) ), body: ListView.builder( itemCount: dataList.length, itemBuilder: (BuildContext context, int index) { return Text(dataList[index][’title’]); }, ), ); }}在调用Client进行post数据请求时,需要传入一个字典进去,Client会通过将字典当做post的from表单。 void requestData() async { var params = Map<String, String>(); params[“username”] = “lxz”; params[“password”] = “123456”; var client = http.Client(); var response = await client.post(url_post, body: params); _content = response.body;}dio网络库dio库的调用方式和http库类似,这里不过多介绍。dio库相对于http库强大的在于,dio库提供了更好的Cookie管理、文件的上传下载、fromData表单等处理。所以,如果对网络库需求比较复杂的话,还是建议使用dio。// 引入外部依赖dio: ^1.0.9数据解析convert系统自带有convert解析库,在使用时直接import即可。convert类似于iOS自带的JSON解析类NSJSONSerialization,可以直接将json字符串解析为字典或数组。import ‘dart:convert’;// 解析代码dataList = json.decode(jsonString);但是,我们在项目中使用时,一般都不会直接使用字典取值,这是一种很不好的做法。一般都会将字典或数组转换为模型对象,在项目中使用模型对象。可以定义类似Model.dart这样的模型类,并在模型类中进行数据解析,对外直接暴露公共变量来让外界获取值。自动序列化但如果定义模型类的话,一个是要在代码内部写取值和赋值代码,这些都需要手动完成。另外如果当服务端字段发生改变后,客户端也需要跟着进行改变,所以这种方式并不是很灵活。可以采用json序列化的三方库json_serializable,此库可以将一个类标示为自动JSON序列化的类,并对类提供JSON和对象相互转换的能力。也可以通过命令行开启一个watch,当类中的变量定义发生改变时,相关代码自动发生改变。首先引入下面的三个库,其中包括依赖库一个,以及调试库两个。dependencies: json_annotation: ^2.0.0dev_dependencies: build_runner: ^1.0.0 json_serializable: ^2.0.0定义一个模型文件,例如这里叫做User.dart文件,并在内部定义一个User的模型类。随后引入json_annotation的依赖,通过@JsonSerializable()标示此类需要被json_serializable进行合成。定义的User类包含两部分,实例变量和两个转换函数。在下面定义json转换函数时,需要注意函数命名一定要按照下面格式命名,否则不能正常生成user.g.dart文件。import ‘package:json_annotation/json_annotation.dart’;// 定义合成后的新文件为user.g.dartpart ‘user.g.dart’;@JsonSerializable()class User { String name; int age; String email; factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json); Map<String, dynamic> toJson() => _$UserToJson(this);}下面就是user.dart指定生成的user.g.dart文件,其中包含JSON和对象相互转换的代码。part of ‘user.dart’;User _$UserFromJson(Map<String, dynamic> json) { return User( json[’name’] as String, json[‘age’] as int, json[’email’] as String);}Map<String, dynamic> _$UserToJson(User instance) => <String, dynamic>{ ’name’: instance.name, ‘age’: instance.age, ’email’: instance.email };有的时候服务端返回的参数名和本地的关键字冲突,或者命名不规范,导致本地定义和服务器字段不同的情况。这种情况可以通过@JsonKey关键字,来修饰json字段匹配新的本地变量。除此之外,也可以做其他修饰,例如变量不能为空等。@JsonKey(name: ‘id’)final int user_id;现在项目中依然是报错的,随后我们在flutter工程的根目录文件夹下,运行下面命令。flutter packages pub run build_runner watch此命令的好处在于,其会在后台监听模型类的定义,当模型类定义发生改变后,会自动修改本地源码以适配新的定义。以文中User类为例,当User.dart文件发生改变后,使用Cmd+s保存文件,随后VSCode会将自定改变user.g.dart文件的定义,以适配新的变量定义。系统文件主要文件iOS文件:iOS工程文件Android:Android工程文件lib:Flutter的dart代码assets:资源文件夹,例如font、image等都可以放在里面.gitignore:git忽略文件packages这是一个系统文件,Flutter通过.packages文件来管理一些系统依赖库,例如material、cupertino、widgets、animation、gesture等系统库就在里面,这些主要的系统库由.packages下的flutter统一管理,源码都在flutter/lib/scr目录下。除此之外,还有一些其他的系统库或系统资源都在.packages中。yaml文件在Flutter中通过pubspec.yaml文件来管理外部引用,包含本地资源文件、字体文件、依赖库等依赖,以及应用的一些配置信息。这些配置在项目中时,需要注意代码对其的问题,否则会导致加载失败。当修改yaml文件的依赖信息后,需要执行flutter get packages命令更新本地文件。但并不需要这么麻烦,可以直接Cmd+s保存文件,VSCode编译器会自动更新依赖。// 项目配置信息name: WeChatdescription: Tencent WeChat App.version: 1.0.0+1// 常规依赖dependencies: flutter:125864 sdk: flutter cupertino_icons: ^0.1.2 english_words: ^3.1.0// 开发依赖dev_dependencies: flutter_test: sdk: flutter flutter: uses-material-design: true // 图片依赖 assets: - assets/images/ic_file_transfer.png - assets/images/ic_fengchao.png // 字体依赖 fonts: - family: appIconFont fonts: - asset: assets/fonts/iconfont.ttfFlutter开发启动函数和大多数编程语言一样,dart也包含一个main方法,是Flutter程序执行的主入口,在main方法中写的代码就是在程序启动时执行的代码。main方法中会执行runApp方法,runApp方法类似于iOS的UIApplicationMain方法,runApp函数接收一个Widget用来做应用程序的显示。void main() { runApp() // code}生命周期在iOS中通过AppDelegate可以获取应用程序的生命周期回调,在Flutter中也可以获取到。可以通过向Binding添加一个Observer,并实现didChangeAppLifecycleState方法,来监听指定事件的到来。但是由于Flutter提供的状态有限,在iOS平台只能监听三种状态,下面是示例代码。class LifeCycleDemoState extends State<MyHomePage> with WidgetsBindingObserver { @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); } @override void didChangeAppLifecycleState(AppLifecycleState state) { super.didChangeAppLifecycleState(state); switch (state) { case AppLifecycleState.inactive: print(‘Application Lifecycle inactive’); break; case AppLifecycleState.paused: print(‘Application Lifecycle paused’); break; case AppLifecycleState.resumed: print(‘Application Lifecycle resumed’); break; default: print(‘Application Lifecycle other’); } }}矩阵变换在Flutter中是支持矩阵变化的,例如rotate、scale等方式。Flutter的矩阵变换由Widget完成,需要进行矩阵变换的视图,在外面包一层Transform Widget即可,内部可以设置其变换方式。child: Container( child: Transform( child: Container( child: Text( “Lorem ipsum”, style: TextStyle(color: Colors.orange[300], fontSize: 12.0), textAlign: TextAlign.center, ), decoration: BoxDecoration( color: Colors.red[400], ), padding: EdgeInsets.all(16.0), ), alignment: Alignment.center, transform: Matrix4.identity() ..rotateZ(15 * 3.1415927 / 180), ), width: 320.0, height: 240.0, color: Colors.grey[300],)在Transform中可以通过transform指定其矩阵变换方式,通过alignment指定变换的锚点。页面导航在iOS中可以通过UINavigationController对页面进行管理,控制页面间的push、pop跳转。Flutter中使用Navigator和Routers来实现类似UINavigationController的功能,Navigator负责管理导航栈,包含push、pop的操作,可以把UIViewController看做一个Routers,Routers被Navigator管理着。Navigator的跳转方式分为两种,一种是直接跳转到某个Widget页面,另一种是为MaterialApp构建一个map,通过key来跳转对应的Widget页面。map的格式是key : context的形式。void main() { runApp(MaterialApp( home: MyAppHome(), // becomes the route named ‘/’ routes: <String, WidgetBuilder> { ‘/a’: (BuildContext context) => MyPage(title: ‘page A’), ‘/b’: (BuildContext context) => MyPage(title: ‘page B’), ‘/c’: (BuildContext context) => MyPage(title: ‘page C’), }, ));}跳转时通过pushNamed指定map中的key,即可跳转到对应的Widget。如果需要从push出来的页面获取参数,可以通过await修饰push操作,这样即可在新页面pop的时候将参数返回到当前页面。Navigator.of(context).pushNamed(’/b’);Map coordinates = await Navigator.of(context).pushNamed(’/location’);Navigator.of(context).pop({“lat”:43.821757,“long”:-79.226392});编码规范VSCode有很好的语法检查,如果有命名不规范等问题,都会以警告的形式表现出来。驼峰命名法,方法名、变量名等,都以首字母小写的驼峰命名法。类名也是驼峰命名法,但类名首字母大写。文件名,文件命名以下划线进行区分,不使用驼峰命名法。Flutter中创建Widget对象,可以用new修饰,也可以不用。child: new Container( child: Text( ‘Hello World’, style: TextStyle(color: Colors.orange, fontSize: 15.0) ))函数中可以定义可选参数,以及必要参数。下面是一个函数定义,这里定义了一个必要参数url,以及一个Map类型的可选参数headers。Future<Response> get(url, {Map<String, String> headers});Dart中在函数定义前加下划线,则表示是私有方法或变量。Dart通过import引入外部引用,除此之外也可以通过下面的语法单独引入文件中的某部分。import “dart:collection” show HashMap, IterableBase;=>调用在Dart中经常可以看到=>的调用方式,这种调用方式类似于一种语法糖,下面是一些常用的调用方式。当进行函数调用时,可以将普通函数调用转换为=>的调用方式,例如下面第一个示例。在此基础上,如果调用函数只有一个参数,可以将其改为第二个示例的方式,也就是可以省略调用的括号,直接写参数名。(单一参数) => {函数声明}elements.map((element) => { return element.length;});单一参数 => {函数声明}elements.map(element => { return element.length;});当只有一个返回值,并且没有逻辑处理时,可以直接省略return,返回数值。(参数1, 参数2, …, 参数N) => 表达式elements.map(element => element.length);当调用的函数中没有参数时,可以直接省略参数,写一对空括号即可。() => {函数实现}小技巧代码重构VSCode支持对Dart语言进行重构,一般作用范围都是在函数内小范围的。例如在创建Widget对象的地方,将鼠标焦点放在这里,当前行的最前面会有提示。点击提示后会有下面两个选项:Extract Local Variable 将当前Widget及其子Widget创建的代码,剥离到一个变量中,并在当前位置使用这个变量。Extract Method 将当前Widget及其子Widget创建的代码,封装到一个函数中,并在当前位置调用此函数。除此之外,将鼠标焦点放在方法的一行,点击最前面的提示,会出现下面两个选项:Convert to expression body 将当前函数转换为一个表达式。Convert to async function body 将当前函数转换为一个异步线程中执行的代码。附加效果在Dart中添加任何附加效果,例如动画效果或矩阵转换,除了直接给Widget子类的属性赋值外,就是在被当前Widget外面包一层,就可以使当前Widget拥有对应的效果。// 动画效果floatingActionButton: FloatingActionButton( tooltip: ‘Fade’, child: Icon(Icons.brush), onPressed: () { controller.forward(); },),// 矩阵转换Transform( child: Container( child: Text( “Lorem ipsum”, style: TextStyle(color: Colors.orange[300], fontSize: 12.0), textAlign: TextAlign.center, ) ), alignment: Alignment.center, transform: Matrix4.identity() ..rotateZ(15 * 3.1415927 / 180),),快捷键(VSCode)Cmd + Shift + p:可以进行快速搜索。需要注意的是,默认是带有一个>的,这样搜索结果主要是dart代码。如果想搜索其他配置文件,或者安装插件等操作,需要把>去掉。Cmd + Shift + o:可以在某个文件中搜索某个类,但前提是需要提前进入这个文件。例如进入framework.dart,搜索StatefulWidget类。注意点使用Flutter要注意代码缩进,如果缩进有问题可能会影响最后的结果,尤其是在.yaml中写配置文件的时候。因为Flutter是开源的,所以遇到问题后可以进入源码中,找解决方案。在代码中要注意标点符号的使用,例如第二个创建Stack的代码,如果上面是以逗号结尾,则后面的创建会失败,如果上面是以分号结尾则没问题。Widget unreadMsgText = Container( width: Constants.UnreadMsgNotifyDotSize, height: Constants.UnreadMsgNotifyDotSize, child: Text( conversation.unreadMsgCount.toString(), style: TextStyle( color: Color(AppColors.UnreadMsgNotifyTextColor), fontSize: 12.0 ), ), ); avatarContainer = Stack( overflow: Overflow.visible, children: <Widget>[ avatar ], ); ...

April 5, 2019 · 4 min · jiezi

Flutter 实现原理及在马蜂窝的跨平台开发实践

一直以来,跨平台开发都是困扰移动客户端开发的难题。在马蜂窝旅游 App 很多业务场景里,我们尝试过一些主流的跨平台开发解决方案,比如 WebView 和 React Native,来提升开发效率和用户体验。但这两种方式也带来了新的问题。比如使用 WebView 跨平台方式,优点确实非常明显。基于 WebView 的框架集成了当下 Web 开发的诸多优势:丰富的控件库、动态化、良好的技术社区、测试自动化等等。但是缺点也同样明显:渲染效率和 JavaScript 的执行能力都比较差,使页面的加载速度和用户体验都不尽如人意。而使用以 React Native(简称 RN)为代表的框架时,维护又成了大难题。RN 使用类 HTML+JS 的 UI 创建逻辑,生成对应的原生页面,将页面的渲染工作交给了系统,所以渲染效率有很大的优势。但由于 RN 代码是通过 JS 桥接的方式转换为原生的控件,所以受各个系统间的差异影响非常大,虽然可以开发一套代码,但对各个平台的适配却非常的繁琐和麻烦。为什么是 Flutter2018 年 12 月初,Google 正式发布了开源跨平台 UI 框架 Flutter 1.0 Release 版本,马蜂窝电商客户端团队进行了调研与实践,发现 Flutter 能很好的帮助我们解决开发中遇到的问题。跨平台开发,针对 Android 与 iOS 的风格设计了两套设计语言的控件实现(Material & Cupertino)。这样不但能够节约人力成本,而且在用户体验上更好的适配 App 运行的平台。重写了一套跨平台的 UI 框架,渲染引擎是依靠 Skia 图形库实现。Flutter 中的控件树直接由渲染引擎和高性能本地 ARM 代码直接绘制,不需要通过中间对象(Web 应用中的虚拟 DOM 和真实 DOM,原生 App 中的虚拟控件和平台控件)来绘制,使它有接近原生页面的性能,帮助我们提供更好的用户体验。同时支持 JIT 和 AOT 编译。JIT 编译方式使其在开发阶段有个备受欢迎的功能——热重载(HotReload),这样在开发时可以省去构建的过程,提高开发效率。而在 Release 运行阶段采用 AOT 的编译方式,使执行效率非常高,让 Release 版本发挥更好的性能。于是,电商客户端团队决定探索 Flutter 在跨平台开发中的新可能,并率先应用于商家端 App 中。在本文中,我们将结合 Flutter 在马蜂窝商家端 App 中的应用实践,探讨 Flutter 架构的实现原理,有何优势,以及如何帮助我们解决问题。Flutter 架构和实现原理Flutter 使用 Dart 语言开发,主要有以下几点原因:Dart 一般情况下是运行 DartVM 上,但是也可以编译为 ARM 代码直接运行在硬件上。Dart 同时支持 AOT 和 JIT 两种编译方式,可以更好的提高开发以及 App 的执行效率。Dart 可以利用独特的隔离区(Isolate)实现多线程。而且不共享内存,可以实现无锁快速分配。分代垃圾回收,非常适合 UI 框架中常见的大量 Widgets 对象创建和销毁的优化。在为创建的对象分配内存时,Dart 是在现有的堆上移动指针,保证内存的增长是程线性的,于是就省了查找可用内存的过程。Dart 主要由 Google 负责开发和维护。目前 Dart 最新版本已经是 2.2,针对 App 和 Web 开发做了很多优化。并且对于大多数的开发者而言,Dart 的学习成本非常低。Flutter 架构也是采用的分层设计。从下到上依次为:Embedder(嵌入器)、Engine、Framework。<center>图 1: Flutter 分层架构图</center>Embedder是嵌入层,做好这一层的适配 Flutter 基本可以嵌入到任何平台上去; Engine层主要包含 Skia、Dart 和 Text。Skia 是开源的二位图形库;Dart 部分主要包括 runtime、Garbage Collection、编译模式支持等;Text 是文本渲染。Framework在最上层。我们的应用围绕 Framework 层来构建,因此也是本文要介绍的重点。Framework1.【Foundation】在最底层,主要定义底层工具类和方法,以提供给其他层使用。2.【Animation】是动画相关的类,可以基于此创建补间动画(Tween Animation)和物理原理动画(Physics-based Animation),类似 Android 的 ValueAnimator 和 iOS 的 Core Animation。3.【Painting】封装了 Flutter Engine 提供的绘制接口,例如绘制缩放图像、插值生成阴影、绘制盒模型边框等。4.【Gesture】提供处理手势识别和交互的功能。5.【Rendering】是框架中的渲染库。控件的渲染主要包括三个阶段:布局(Layout)、绘制(Paint)、合成(Composite)。从下图可以看到,Flutter 流水线包括 7 个步骤。<center>图 2: Flutter 流水线</center>首先是获取到用户的操作,然后你的应用会因此显示一些动画,接着 Flutter 开始构建 Widget 对象。Widget 对象构建完成后进入渲染阶段,这个阶段主要包括三步:布局元素:决定页面元素在屏幕上的位置和大小;绘制阶段:将页面元素绘制成它们应有的样式;合成阶段:按照绘制规则将之前两个步骤的产物组合在一起。最后的光栅化由 Engine 层来完成。在渲染阶段,控件树(widget)会转换成对应的渲染对象(RenderObject)树,在 Rendering 层进行布局和绘制。在布局时 Flutter 深度优先遍历渲染对象树。数据流的传递方式是从上到下传递约束,从下到上传递大小。也就是说,父节点会将自己的约束传递给子节点,子节点根据接收到的约束来计算自己的大小,然后将自己的尺寸返回给父节点。整个过程中,位置信息由父节点来控制,子节点并不关心自己所在的位置,而父节点也不关心子节点具体长什么样子。<center>图 3: 数据流传递方式</center>为了防止因子节点发生变化而导致的整个控件树重绘,Flutter 加入了一个机制——Relayout Boundary,在一些特定的情形下 Relayout Boundary 会被自动创建,不需要开发者手动添加。例如,控件被设置了固定大小(tight constraint)、控件忽略所有子视图尺寸对自己的影响、控件自动占满父控件所提供的空间等等。很好理解,就是控件大小不会影响其他控件时,就没必要重新布局整个控件树。有了这个机制后,无论子树发生什么样的变化,处理范围都只在子树上。<center>图 4: Relayout Boundary 机制</center>在确定每个空间的位置和大小之后,就进入绘制阶段。绘制节点的时候也是深度遍历绘制节点树,然后把不同的 RenderObject 绘制到不同的图层上。这时有可能出现一种特殊情况,如下图所示节点 2 在绘制子节点 4 时,由于其节点 4 需要单独绘制到一个图层上(如 video),因此绿色图层上面多了个黄色的图层。之后再需要绘制其他内容(标记 5)就需要再增加一个图层(红色)。再接下来要绘制节点 1 的右子树(标记 6),也会被绘制到红色图层上。所以如果 2 号节点发生改变就会改变红色图层上的内容,因此也影响到了毫不相干的 6 号节点。<center>图 5: 绘制节点与图层的关系</center>为了避免这种情况,Flutter 的设计者这里基于 Relayout Boundary 的思想增加了Repaint Boundary。在绘制页面时候如果遇见 Repaint Boundary 就会强制切换图层。如下图所示,在从上到下遍历控件树遇到 Repaint Boundary 会重新绘制到新的图层(深蓝色),在从下到上返回的时候又遇到 Repaint Boundary,于是又增加一个新的图层(浅蓝色)。<center>图 6: Repaint Boundary 机制</center>这样,即使发生重绘也不会对其他子树产生影响。比如在 Scrollview 上,当滚动的时候发生内容重绘,如果在 Scrollview 以外的地方不需要重绘就可以使用 Repaint Boundary。Repaint Boundary 并不会像 Relayout Boundary 一样自动生成,而是需要我们自己来加入到控件树中。6.【Widget】控件层。所有控件的基类都是 Widget,Widget 的数据都是只读的, 不能改变。所以每次需要更新页面时都需要重新创建一个新的控件树。每一个 Widget 会通过一个 RenderObjectElement 对应到一个渲染节点(RenderObject),可以简单理解为 Widget 中只存储了页面元素的信息,而真正负责布局、渲染的是 RenderObject。在页面更新重新生成控件树时,RenderObjectElement 树会尽量保持重用。由于 RenderObjectElement 持有对应的 RenderObject,所有 RenderObject 树也会尽可能的被重用。如图所示就是三棵树之间的关系。在这张图里我们把形状当做渲染节点的类型,颜色是它的属性,即形状不同就是不同的渲染节点,而颜色不同只是同一对象的属性的不同。<center>图 7:Widget、Element 和 Render 之间的关系</center>如果想把方形的颜色换成黄色,将圆形的颜色变成红色,由于控件是不能被修改的,需要重新生成两个新的控件 Rectangle yellow 和 Circle red。由于只是修改了颜色属性,所以 Element 和 RenderObject 都被重用,而之前的控件树会被释放回收。<center>图 8: 示例</center>那么如果把红色圆形变成三角形又会怎样呢?由于这里发生变化的是类型,所以对应的 Element 节点和 RenderObject 节点都需要重新创建。但是由于黄色方形没有发生改变,所以其对应的 Element 节点和 RenderObject 节点没有发生变化。<center>图 9: 示例</center>7. 最后是【Material】 & 【Cupertino】,这是在 Widget 层之上框架为开发者提供的基于两套设计语言实现的 UI 控件,可以帮助我们的 App 在不同平台上提供接近原生的用户体验。Flutter 在马蜂窝商家端App 中的应用实践<center>图 10: 马蜂窝商家端使用 Flutter 开发的页面</center>开发方式:Flutter + Native由于商家端已经是一款成熟的 App,不可能创建一个新的 Flutter 工程全部重新开发,因此我们选择 Native 与 Flutter 混编的方案来实现。 在了解 Native 与 Flutter 混编方案前,首先我们需要了解在 Flutter 工程中,通常有以下 4 种工程类型:1. Flutter Application标准的 Flutter App 工程,包含标准的 Dart 层与 Native 平台层。2. Flutter ModuleFlutter 组件工程,仅包含 Dart 层实现,Native 平台层子工程为通过 Flutter 自动生成的隐藏工程(.ios /.android)。3. Flutter PluginFlutter 平台插件工程,包含 Dart 层与 Native 平台层的实现。4. Flutter PackageFlutter 纯 Dart 插件工程,仅包含 Dart 层的实现,往往定义一些公共 Widget。了解了 Flutter 工程类型后,我们来看下官方提供的一种混编方案(https://github.com/flutter/flutter/wiki/Add-Flutter-to-existing-apps),即在现有工程下创建Flutter Module 工程,以本地依赖的方式集成到现有的 Native 工程中。官方集成方案(以 iOS 为例)a. 在工程目录创建 FlutterModule,创建后,工程目录大致如下:b. 在 Podfile 文件中添加以下代码:flutter_application_path = ‘../flutter_Moudule/‘该脚本主要负责:pod 引入 Flutter.Framework 以及 FlutterPluginRegistrant 注册入口pod 引入 Flutter 第三方 plugin在每一个 pod 库的配置文件中写入对 Generated.xcconfig 文件的导入修改 pod 库的 ENABLE_BITCODE = NO(因为 Flutter 现在不支持 bitcode)c. 在 iOS 构建阶段 Build Phases 中注入构建时需要执行的 xcode_backend.sh (位于 FlutterSDK/packages/flutter_tools/bin) 脚本:"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build该脚本主要负责:构建 App.framework 以及 Flutter.framework 产物根据编译模式(debug/profile/release)导入对应的产物编译 flutter_asset 资源把以上产物 copy 到对应的构建产物中d. 与 Native 通信方案一:改造 AppDelegate 继承自 FlutterAppDelegate方案二:AppDelegate 实现 FlutterAppLifeCycleProvider 协议,生命周期由 FlutterPluginAppLifeCycleDelegate 传递给 Flutter以上就是官方提供的集成方案。我们最终没有选择此方案的原因,是它直接依赖于 FlutterModule 工程以及 Flutter 环境,使 Native 开发同学无法脱离 Flutter 环境开发,影响正常的开发流程,团队合作成本较大;而且会影响正常的打包流程。(目前 Flutter 团队正在重构嵌入 Native 工程的方式)最终我们选择另一种方案来解决以上的问题:远端依赖产物。<center>图 11 :远端依赖产物</center>iOS 集成方案通过对官方混编方案的研究,我们了解到 iOS 工程最终依赖的其实是 FlutterModule 工程构建出的产物(Framework,Asset,Plugin),只需将产物导出并 push 到远端仓库,iOS 工程通过远端依赖产物即可。依赖产物目录结构如下:App.framework: Flutter 工程产物(包含 Flutter 工程的代码,Debug 模式下它是个空壳,代码在 flutter_assets 中)。Flutter.framework:Flutter 引擎库。与编译模式(debug/profile/release)以及 CPU 架构(arm*, i386, x86_64)相匹配。lib.a & .h 头文件: FlutterPlugin 静态库(包含在 iOS 端的实现)。flutter_assets: 包含 Flutter 工程字体,图片等资源。在 Flutter1.2 版本中,被打包到 App.framework 中。Android 集成方案Android Nativite 集成是通过 Gradle 远程依赖 Flutter 工程产物的方式完成的,以下是具体的集成流程。a.创建 Flutter 标准工程$ flutter create flutter_demo默认使用 Java 代码,如果增加 Kotlin 支持,使用如下命令:$ flutter create -a kotlin flutter_demob.修改工程的默认配置修改 app module 工程的 build.gradle 配置 apply plugin: ‘com.android.application’ => apply plugin: ‘com.android.library’,并移除 applicationId 配置修改 root 工程的 build.gradle 配置在集成过程中 Flutter 依赖了三方 Plugins 后,遇到 Plugins 的代码没有被打进 Library 中的问题。通过以下配置解决(这种方式略显粗暴,后续的优化方案正在调研)。subprojects { project.buildDir = “${rootProject.buildDir}/app”}app module 增加 maven 打包配置c. 生成 Android Flutter 产物$ cd android$ ./gradlew uploadArchives官方默认的构建脚本在 Flutter 1.0.0 版本存在 Bug——最终的产物中会缺少 flutter_shared/icudtl.dat 文件,导致 App Crash。目前的解决方式是将这个文件复制到工程的 assets 下(在 Flutter 最新 1.2.1 版本中这个 Bug 已被修复,但是 1.2.1 版本又出现了一个 UI 渲染的问题,所以只能继续使用 1.0.0 版本)。d.Android Native 平台工程集成,增加下面依赖配置即可,不会影响 Native 平台开发的同学implementation ‘com.mfw.app:MerchantFlutter:0.0.5-beta’Flutter 和 iOS、Android 的交互使用平台通道(Platform Channels)在 Flutter 工程和宿主(Native 工程)之间传递消息,主要是通过 MethodChannel 进行方法的调用,如下图所示:<center>图 12 :Flutter 与 iOS、Android 交互</center>为了确保用户界面不会挂起,消息和响应是异步传递的,需要用 async 修饰方法,await 修饰调用语句。Flutter 工程和宿主工程通过在 Channel 构造函数中传递 Channel 名称进行关联。单个应用中使用的所有 Channel 名称必须是唯一的; 可以在 Channel 名称前加一个唯一的「域名前缀」。Flutter 与 Native 性能对比我们分别使用 Native 和 Flutter 开发了两个列表页,以下是页面效果和性能对比:iOS 对比(机型 6P 系统 10.3.3):Flutter 页面:iOS Native 页面:可以看到,从使用和直观感受都没有太大的差别。于是我们采集了一些其他方面的数据。Flutter 页面:iOS Native 页面:另外我们还对比了商家端接入 Flutter 前后包体积的大小:39Mb → 44MB在 iOS 机型上,流畅度上没有什么差异。从数值上来看,Flutter 在 内存跟 GPU/CPU 使用率上比原生略高。Demo 中并没有对 Flutter 做更多的优化,可以看出 Flutter 整体来说还是可以做出接近于原生的页面。下面是 Flutter 与 Android 的性能对比。Flutter 页面:Android Native 页面:从以上两张对比图可以看出,不考虑其他因素,单纯从性能角度来说,原生要优于 Flutter,但是差距并不大,而且 Flutter 具有的跨平台开发和热重载等特点极大地节省了开发效率。并且,未来的热修复特性更是值得期待。混合栈管理首先先介绍下 Flutter 路由的管理:Flutter 管理页面有两个概念:Route 和 Navigator。Navigator 是一个路由管理的 Widget(Flutter 中万物皆 Widget),它通过一个栈来管理一个路由 Widget 集合。通常当前屏幕显示的页面就是栈顶的路由。路由 (Route) 在移动开发中通常指页面(Page),这跟 web 开发中单页应用的 Route 概念意义是相同的,Route 在 Android 中通常指一个 Activity,在 iOS 中指一个 ViewController。所谓路由管理,就是管理页面之间如何跳转,通常也可被称为导航管理。这和原生开发类似,无论是 Android 还是 iOS,导航管理都会维护一个路由栈,路由入栈 (push) 操作对应打开一个新页面,路由出栈 (pop) 操作对应页面关闭操作,而路由管理主要是指如何来管理路由栈。<center>图 14 :Flutter 路由管理</center>如果是纯 Flutter 工程,页面栈无需我们进行管理,但是引入到 Native 工程内,就需要考虑如何管理混合栈。并且需要解决以下几个问题:1. 保证 Flutter 页面与 Native 页面之间的跳转从用户体验上没有任何差异2. 页面资源化(马蜂窝特有的业务逻辑)3. 保证生命周期完整性,处理相关打点事件上报4. 资源性能问题参考了业界内的解决方法,以及项目自身的实际场景,我们选择类似于 H5 在 Navite 中嵌入的方式,统一通过 openURL 跳转到一个 Native 页面(FlutterContainerVC),Native 页面通过 addChildViewController 方式添加 FlutterViewController(负责 Flutter 页面渲染),同时通过 channel 同步 Native 页面与 Flutter 页面。每一次的 push/pop 由 Native 发起,同时通过 channel 保持 Native 与 Flutter 页面同步——在 Native 中跳转 Flutter 页面与跳转原生无差异一个 Flutter 页面对应一个 Native 页面(FlutterContainerVC)——解决页面资源化FlutterContainerVC 通过 addChildViewController 对单例 FlutterViewController 进行复用——保证生命周期完整性,处理相关打点事件上报由于每一个 FlutterViewController(提供 Flutter 视图的实现)会启动三个线程,分别是 UI 线程、GPU 线程和 IO 线程,使用单例 FlutterViewController 可以减少对资源的占用——解决资源性能问题Flutter 应用总结Flutter 一经发布就很受关注,除了 iOS 和 Android 的开发者,很多前端工程师也都非常看好 Flutter 未来的发展前景。相信也有很多公司的团队已经投入到研究和实践中了。不过 Flutter 也有很多不足的地方,值得我们注意:虽然 1.2 版本已经发布,但是目前没有达到完全稳定状态,1.2 发布完了就出现了控件渲染的问题。加上 Dart 语言生态小,学习资料可能不够丰富。关于动态化的支持,目前 Flutter 还不支持线上动态性。如果要在 Android 上实现动态性相对容易些,iOS 由于审核原因要实现动态性可能成本很高。Flutter 中目前拿来就用的能力只有 UI 控件和 Dart 本身提供能力,对于平台级别的能力还需要通过 channel 的方式来扩展。已有工程迁移比较复杂,以前沉淀的 UI 控件,需要重新再实现一套。最后一点比较有争议,Flutter 不会从程序中拆分出额外的模板或布局语言,如 JSX 或 XM L,也不需要单独的可视布局工具。有的人认为配合 HotReload 功能使用非常方便,但我们发现这样代码会有非常多的嵌套,阅读起来有些吃力。目前阿里的闲鱼开发团队已经将 Flutter 用于大型实践,并应用在了比较重要的场景(如产品详情页),为后来者提供了良好的借鉴。马蜂窝的移动客户端团队关于 Flutter 的探索才刚刚起步,前面还有很多的问题需要我们一点一点去解决。不过无论从 Google 对其的重视程度,还是我们从实践中看到的这些优点,都让我们对 Flutter 充满信心,也希望在未来我们可以利用它创造更多的价值和奇迹。路途虽远,犹可期许。本文作者:马蜂窝电商研发客户端团队。(马蜂窝技术原创内容,转载务必注明出处保存文末二维码图片,禁止商业用途,谢谢配合。)参考文献:Flutter’s Layered Designhttps://www.youtube.com/watch?v=dkyY9WCGMi0 Flutter’s Rendering Pipelinehttps://www.youtube.com/watch?v=UUfXWzp0-DU&t=1955s Flutter 原理与美团的实践https://juejin.im/post/5b6d59476fb9a04fe91aa778#comment 关注马蜂窝技术,找到更多你想要的内容 ...

March 26, 2019 · 4 min · jiezi

uni-app 开发vue和nvue时,注意事项

vue注意事项相比Web平台, Vue.js 在 uni-app 中使用差异主要集中在两个方面: 新增:uni-app除了支持Vue实例的生命周期,还支持应用启动、页面显示等生命周期 受限:相比web平台,部分功能受限,比如 v-html 指令,具体见下。(受限部分仅在App和小程序端受限,H5版不受限)注意: 不要在选项属性或回调上使用箭头函数,比如 created: () => console.log(this.a) 或 vm.$watch(‘a’, newValue => this.myMethod())。因为箭头函数是和父级上下文绑定在一起的,this 不会是如你做预期的 Vue 实例,且 this.a 或 this.myMethod 也会是未定义的。 建议使用 uni-app 的 onReady代替 vue 的 mounted。 建议使用 uni-app 的 onLoad 代替 vue 的 created。注意事项 .vue 和 .nvue 并不是一个规范,因此一些在 .vue 中适用的方案并不适用于 .nvue。 Vue 上挂载属性,不能在 .nvue 中使用。 .nvue 不支持 vuex 如果希望 .vue 和 .nvue 共享一些数据的话,需要采用公用模块的方案,分别在 .vue 和 .nvue 文件中引入。非H5端不支持 Vue官方文档:Class 与 Style 绑定 中的 classObject 和 styleObject 语法。绑定的数据对象不必内联定义在模板里:<div v-bind:class=“classObject”></div>data: { classObject: { active: true, ’text-danger’: false }}注意:以:style=““这样的方式设置px像素值,其值为实际像素,不会被编译器转换。此外还可以用 computed 方法生成 class 或者 style 字符串,插入到页面中,举例说明:<template> <!– 支持 –> <view class=“container” :class=“computedClassStr”></view> <view class=“container” :class="{active: isActive}"></view> <!– 不支持 –> <view class=“container” :class=“computedClassObject”></view></template><script> export default { data () { return { isActive: true } }, computed: { computedClassStr () { return this.isActive ? ‘active’ : ’’ }, computedClassObject () { return { active: this.isActive } } } }</script>用在组件上非H5端暂不支持在组件上使用 Class 与 Style 绑定注意事项 nvue 页面均采用 flex 布局,不支持其他布局方式,需要注意的是 flex 默认为竖向排列,即 flex-direction: column。 在 App.vue 中定义的全局样式不会在 nvue 页面生效。 目前不支持在 nvue 页面使用 scss、less 等预编译语言。 不能在 style 中引入字体文件,nvue 中字体图标的使用参考:weex 加载自定义字体。 class 进行绑定时只支持数组语法(weex 限制)。 nvue 页面跳转 vue 页面时,只能调用 uni-app 的 路由 API 进行跳转。 nvue 暂不支持运行在模拟器上。 在 created 里调用 uni-app 的 api 时,可能出现 launch webview id is not ready 错误,延时几百毫秒再执行就不会报错了。 nvue 页面 titleNview 设为 false时,想要模拟状态栏,可以参考:https://ask.dcloud.net.cn/article/35111。 nvue 中不支持使用 import 的方式引入外部 css,需使用如下方式进行引入,注意:引入外部 css 的 style 节点下写样式是不会生效的,需要额外添加新的 style 节点。、 <style src=”@/common/test.css”></style> <style> .test { color: #E96900; } </style> 开发 nvue 时,若遇到如下错误,是因为一个 uni-app 里必须有一个 vue 页面,在项目里新建一个空白的 vue 页面即可解决此问题。 Uncaught Error: module “common/vendor.js” is not defined 20:31:58.664 Wed Jan 23 2019 20:33:31 GMT+0800 (CST) Page route 错误 20:31:58.687 Page[pages/index/index] not found. May be caused by: 1. Forgot to add page route in pages.json. 2. Invoking Page() in async task. 20:31:58.706 console.groupEndnvue注意事项nvue 页面均采用 flex 布局,不支持其他布局方式,需要注意的是 flex 默认为竖向排列,即 flex-direction: column。nvue 的各组件在安卓端默认是透明的,如果不设置background-color,可能会导致出现重影的问题。在 App.vue 中定义的全局样式不会在 nvue 页面生效。nvue 切换横竖屏时可能重新计算 px,导致样式出现问题,建议有 nvue 的页面锁定手机方向。目前不支持在 nvue 页面使用 scss、less 等预编译语言。不能在 style 中引入字体文件,nvue 中字体图标的使用参考:weex 加载自定义字体。class 进行绑定时只支持数组语法(weex 限制)。nvue 页面跳转 vue 页面时,只能调用 uni-app 的 路由 API 进行跳转。nvue 暂不支持运行在模拟器上。在 created 里调用 uni-app 的 api 时,可能出现 launch webview id is not ready 错误,延时几百毫秒再执行就不会报错了。nvue 页面 titleNview 设为 false时,想要模拟状态栏,可以参考:https://ask.dcloud.net.cn/article/35111。nvue 中不支持使用 import 的方式引入外部 css,需使用如下方式进行引入,注意:引入外部 css 的 style 节点下写样式是不会生效的,需要额外添加新的 style 节点。 <style src="@/common/test.css"></style> <style> .test { color: #E96900; } </style>开发 nvue 时,若遇到如下错误,是因为一个 uni-app 里必须有一个 vue 页面,在项目里新建一个空白的 vue 页面即可解决此问题。Uncaught Error: module “common/vendor.js” is not defined20:31:58.664 Wed Jan 23 2019 20:33:31 GMT+0800 (CST) Page route 错误20:31:58.687 Page[pages/index/index] not found. May be caused by: 1. Forgot to add page route in pages.json. 2. Invoking Page() in async task.20:31:58.706 console.groupEnd ...

March 15, 2019 · 2 min · jiezi

关于ReactNative0.56版本Flatlist列表内容跳动的问题

Reactnative的版本升级一直是一个工作量比较的大的事情,每次升级都可能伴随着很多的坑。前段时间在升级到0.56版本的时候发现一个问题,在flatlist使用中,加载多页后,列表项内容开始进行上下抖动的乱跳,疯了一样。于是开始上react-native的issues上寻找答案,有通过查看官方的版本升级日志找到了答案:react-native升级日志0.57在其中看到如下bugFix描述:因为Flatlist继承自VirtualizedList,所以就豁朗开朗了。解决方案:将ReactNative Version升级到0.57以上版本就好了官方升级方案,推荐第一种“基于 Git 的自动合并更新”的升级方案。我的网站:https://wayne214.github.io

January 4, 2019 · 1 min · jiezi