你们好,我是郭树煜,Github GSY 系列开源项目的做者,系列包括有 GSYVideoPlayer 、GSYGitGithubApp(Flutter\ReactNative\Kotlin\Weex)四大版本,目前总 star 在 17 k+ 左右,主要活跃在掘金社区,id 是恋猫的小郭,主要专栏有《Flutter完整开发实战详解》系列等,平时工做负责移动端项目的开发,工做经历从 Android 到 React Native 、Weex 再到现在的 Flutter ,期间也参与过 React 、 Vue 、小程序等相关的开发,算是一个大前端的选手吧。前端
此次主要是给你们分享 Flutter 相关的内容,主要涉及作一些实战和科普性质的内容。java
恰逢最近谷歌 IO 大会结束,大会后也在线上线下和你们有过交流,总结了下你们最关系的问题有:node
这个问题算是被问得最多的一个,先说观点:我我的认为其实这并不冲突,由于有个 误区就是认为跨平台开发就能够抛弃原生开发!react
若是从事过跨平台开发的同窗应该知道,平台提供的功能向来是有限的,而面对产品经理的各类 “点歪技能树” 的需求,不少时候你是须要基于框架外提供支持,常见的就是 混合开发或者原生插件支持 。git
因此这里我表达的是,目前 Kotlin
和 Dart
更可能是相辅相成 ,而一旦业务复杂度到必定程度,跨平台框架还可能存在下降工做效率的问题,好比针对新需求,须要重复开发 Android/IOS
的原生插件作支持,这也是 Aribnb 曾经选择放弃 React Native
的缘由之一。github
与我而言,跨平台的意义在于解决的是端逻辑的统一 ,至少避免了逻辑重复实现,或者 IOS
和 Android
之间争论 谁对谁错 的问题,甚至能够统一到 web 端等等。web
Flutter
做为后来者,不免会被用来和 React Native
进行对比,在这个万物皆是 JS
的时代,Dart
和 Flutter
的出现显得尤其扎眼。redux
在设计上它们有着许多类似之处,响应式设计/async支持/setState更新 等等,同时也有着各类的差别,而你们最为关心的,无非 性能、支持、上手难易、稳定性程度 这四方面:小程序
React Native
也在进行下一代的优化, 而对此最直观的数据就是:GSY系列 在18年用于闲鱼测试下的对比数据了 。同时注意不要用模拟器测试性能,特别是IOS模拟器作性能测试,由于 Flutter 在 IOS模拟器中纯 CPU ,而实际设备会是 GPU 硬件加速,同时只在 Release 下对比性能。react-native
支持上 Flutter 和 React Native , 都存在第三方包质量良莠不齐的问题,而目前在这一块 Flutter 是弱于 React Native 的 ,毕竟 React Native
发展已久,虽然版本号一直不到 1.0,可是在 JS
的加持下生态丰富,同时也是由于平台特性的缘由,诸如 WebView 、地图等控件的支持上如今依旧不够好,这个后面也会说道。
上手难易度上,Flutter
配置环境和运行的“成功率”比 React Native 高很多 ,这里面有 node_module
黑洞这个坑,也有 React Native
自己依赖平台控件致使的,至少我曾经试过接手一个 React Native
跑了一天都没跑起来的经历,同时 Flutter
在运行和SDK版本升级的阵痛也会少不少。
稳定性:Flutter
中大部分异常是不会引发应用崩溃 ,更多会在 Debug 上体现为红色错误堆栈,Release 上 UI 异常等等。
若是你是前端,我会推荐你先学
React Native
,若是你是原生开发,我推荐你学Flutter
。在 React Native 0.59.x 版本开始,React 已经将许多内置控件和库移出主项目,但愿模糊 React 和 React Native 的界线,统一开发,这里的理念和 Flutter 很像。
Flutter 暂时不支持热更新!!!!!!!!
var
的语法糖是在赋值时才自推导出类型的 ,而 dynamic
是动态声明,在运行时检测,它们的使用有时候容易出现错误。
以下图因此说,
var
初始化时被指定为 dynamic
类型的。String
类型,这时候进行 ++ 操做就会出现运行时报错,以下图所示,Dart
支持不少有意思的操做符,以下图:
AA
若是为空,就返回 999
;AA
为空,就为 AA
赋值 999
;AA
进行整除 999
,输出结果 10
。以下图所示,Dart
中是支持操做符重载的,这样能够比较直观咱们的代码逻辑,而且简化代码时的调用。
以下图所示,在 Dart
中方法时能够做为参数传递的,这样的形式可让咱们更灵活的组织代码的逻辑。
在 Dart
中 async await / async* yield
等语法糖,表明 Dart
中的 Future
和 Stream
操做,它们对应 Dart
中的异步逻辑支持。
sync* / yield 对应
Stream
的同步操做。
在 Dart
中支持混入的模式,以下图所示,混入时的基础顺序是从右到左依次执行的,并且和 super
有关,同时 Dart
还支持 mixin
关键字的定义。
Flutter 的启动类用的就是 mixins 方式
Dart
中单线程模式中增长了 isolate
提供跨线程的真异步操做,而由于 Dart
中线程不会共享内存,因此也不存在死锁,从而也致使了 isolate
之间数据只能经过 port
的端口方式发送接口,相似于 Scoket
的方式,同时提供了 compute
的封装接口方便调用。
Dart 为了让类能够像函数同样调用,默认均可以实现 call()
方法,一样 typedef
定义的方法也是具有 call()
条件。
好比我定义了一个 CallObject
class CallObject {
List<Widget> footerButton = [];
call(int i, double e) => "$i xxxx $e";
}
复制代码
就能够经过如下执行
CallObject callObject = CallObject();
print(callObject(11, 11.0));
print(callObject?.call(11, 11.0));
复制代码
而后我定义了
typedef void ValueFunction(int i);
ValueFunction vt = (int i){
print("zzz $i");
};
复制代码
就能够经过直接执行和判空执行处理
vt(666);
vt?.call(777);
复制代码
以下图所示,ChangeNotifier
模式在 Flutter
中是十分常见的,好比 TextField
控件中,经过 TextEditingController
能够快速设置值的显示,这是为何呢?
以下图所示,这是由于 TextEditingController
它是 ChangeNotifier
的子类,而 TextField
的内部对其进行了 addListener
,同时咱们改变值的时候调用了notifyListener
,触发内部 setState
。
在 Flutter
中全部的状态共享都是经过它实现的,如自带的 Theme
,Localizations
,或者状态管理的 scoope_model
、 flutter_redux
等等,都是基于它实现的。
以下图是 SliderTheme
的自定义实现逻辑,默认 Theme
中是包含了 SliderTheme
,可是咱们能够经过覆盖一个新的 SliderTheme
嵌套去实现自定义,而后经过 SliderTheme theme = SliderTheme(context);
获取,其中而 context
的实现就是 Element
。
在 Element
的 inheritFromWidgetOfExactType
方法实现里,有一个 Map<Type, InheritedElement> _inheritedWidgets
的对象。
_inheritedWidgets
通常状况下是空的,只有当父控件是 InheritedWidget
或者自己是 InheritedWidgets
时才会有被初始化,而当父控件是 InheritedWidget
时,这个 Map
会被一级一级往下传递与合并 。 因此当咱们经过 context
调用 inheritFromWidgetOfExactType
时,就能够往上查找到父控件的 Widget
。
StreamBuilder
通常用于经过 Stream
异步构建页面的,以下图所示,经过点击以后,绿色方框的文字会变成 addNewxxx
,由于 Stream
进行了 map
变化,同时通常实现 bloc
模式的时候,常常会用到它们。
相似的还有 FutureBuilder
通常 Widget
都是一帧的,而 State
实现了 Widget
的跨帧绘制,通常定义的时候,咱们能够以下图同样实现,而以下图尖头所示,这时候咱们点击 setState
改变的时候,是不会出现效果的,为何呢?
其实 State 对象的建立和更新时机致使的:
一、createState 只在 StatefulElement 建立时才会被建立的。
二、StatefulElement 的 createElement 通常只在 inflateWidget 调用。
三、updateChild 执行 inflateWidget 时, 若是 child 存在能够更新的话,不会执行 inflateWidget。
Flutter 中主要有 Widget
、Element
、RenderObject
、Layer
四棵树,它们的做用是:
Widget
:就是咱们日常写的控件,Flutter
宇宙中万物皆 Widget
,它们都是不可变一帧,同时也是被人吐槽不少的嵌套模式,固然换个角度,事实上你把他看成 Widget
配置文件来写或者就好理解了。
Element
:它是 BuildContext
的实现类,Widget
实现跨帧保存的 state
就是存放在这里,同时它也充当了 Widget
和 RenderObject
之间的桥梁。
RenderObject
:它才是真正干活(layout、paint)等,同时它才是真实的 “dom” 。
Layer
:一整块的重绘区域(isRepaintBoundary),决定重绘的影响区域。
skia
在绘制的时候,saveLayer
是比较消耗性能的,好比透明合成、clipRRect
等等都会可能须要saveLayer
的调用, 而saveLayer
会清空GPU绘制的缓存,致使性能上的损耗,因此开发过程当中若是掉帧严重,能够针对这一块进行优化。
Flutter
在手势中引入了竞技的概念, Down
事件在 Flutter
中尤其重要。
PointerDownEvent
是一切的起源,在 Down
事件中通常不会决出胜利者。
在 MOVE
和 UP
的时候才竞争获得响应。
以点击为例子:Down
时添加进去参与竞争,UP
的时候才决定谁胜利,胜利条件是:
I、UP
的时候若是只有一个,那么就是它了。
II、UP
的时候若是有多个,那么强制队列里第一个直接胜利。
UP
的时候才响应,那么 Down 事件怎么先传递出去了?FLutter
在这里作了一个 didExceedDeadline
机制 ,事实上在上面的 addPointer
的时候,会启动了一个定时器,默认 100 ms,若是超过指定时间没 UP
,那就先执行这个 didExceedDeadline
响应 Down
事件。
它们的 onTapDown
都会被触发,可是 onTap
只有一个得到。
ScrollView
嵌套呢?举个简单的例子,两个 SingleChildScrollView
的嵌套时,在布局会经历:
performLayout
->applyContentDimensions
->applyNewDimensions
->context.setCanDrag(physics.shouldAcceptUserOffset(this));
只有 shouldAcceptUserOffset
为 ture
时,才会添加 VerticalDragGestureRecognizer
去处理手势。
而判断条件主要是 return math.max(0.0, child.size.height - size.height);
,也就是若是 child Scroll 的 height 小于父控件 Scroll 的时候,就会出现 child 不添加 VerticalDragGestureRecognizer 的状况,这时候根本就没有竞争了。
Flutter
中的动画是怎么执行的呢?
咱们先看一段代码,而后这段代码执行的效果以下图2所示。
那既然 Widget
都是一帧,那么动画确定有 setState
的地方了。
首先这里有个地方能够看下,这时候 200 这个数值执行后是会报错的,由于白框内可见 Tween
中的 T
在这时候会出现既有 int
又有 double
,没法判断的问题,因此真实应该是 200.0 。
同时你发现没有,代码中 parent
的 Container
在 只有100的状况下,它的 child
能够正常的画 200,这是由于咱们的 paint
没有跟着 RenerObjcet
的大小走, 因此通常状况下,整个屏幕都是咱们的画版,Canvas 绘制与父控件大小能够不要紧。
同时动画是经过 vsync
同步信号去触发的,就是咱们 mixin 的 SingleTickerProviderStateMixin
,它内部的 Ticker
会经过 SchedulerBinding
的 scheduleFrameCallback
同步信号触发重绘 。
动画后的控件的点击区域,和你的动画数据改变的是 paint 仍是 layout 有关 。
scope_model
、flutter_redux
、fish_redux
、甚至还有有 dva_flutter
等等,能够看出状态管理在 flutter
中和前端十分相近。
这里简单说说 scope_model
,它只有一个文件,可是很巧妙,它利用的就是 AnimationBuilder
的特性。
以下图是使用代码,在前面咱们知道,状态管理使用的是 InheritedWidget
实现共享的,而当咱们对 Model
进行数据改变时,经过调用 notifyListeners
通知页面更新了。
这里的原理是什么呢?
其实 scope_model
内部利用了 AnimationBuilder
,而 Model
实现了 Listenable
接口。
当 Model
设置给了 AnimationBuilder
时, AnimationBuilder
会执行 addListener
添加监听,而监听方法里会执行 setState
。
因此咱们改变 set
方法时调用 notifyListeners
就触发了 setState
去更新了,这样体现出了前面说的 FLutter
常见的开发模式。
以 Android
的角度来讲,从方便调试和解耦集成上,咱们通常会以 aar
的形式集成混合开发,这里就会涉及到 gradle
打包的一个概念。
一、以下代码所示,在项目中进行 gradle
脚本修改,组件化开发模式,用 apk
开发,用 aar
提供集成,正常修改 gradle
代码便可快速打包。
那若是 Flutter
的项目插件带有本地代码呢?
若是开发过
React Native
的应该知道,在原生插件安装时会须要执行react-native link
,而这时候会修改项目的gradle 和java代码。
二、 和 React Native
颇有侵入性相比, Flutter
就很巧妙了。
以下图所示,安装过的插件会出如今 .flutter_plugins
文件中,而后经过读取文件,动态在 setting.gradle
和 flutter.gradle
中引入和依赖:
因此这时候咱们能够参考打包,修改咱们的gradle脚本,利用 fat-aar 插件将本地 projcet 也打包的 aar 里。
官方将来将有
Flutter build aar
的方法可提供使用。
三、混合开发的最大痛点是什么?
确定是堆栈管理!!! 因此项目开发了 flutter_boost
来解决这个问题。
engine
,切换 Surface
渲染显示。Activity
就是一个 Surface
,不渲染的页面经过截图缓存画面。
flutter_boost
截止到我测试的时间 2019-05-16, 只支持 1.2以前的版本
混合开发除了集成到原生工程,也有将原生控件集成到 Flutter 渲染树里里的需求。
首先咱们看看没有 PlatformView
以前是如何实现 WebView
的,这样会有什么问题?
以下图所示,事实上 dart 中仅仅是用了一个 SingleChildRenderObjectWidget
用于占位,将大小传递给原生代码,而后在原生代码里显示出来而已。
这样的时候一定会代码画面堆栈问题,由于这个显示脱离了 Flutter 的渲染树,经过出现动画确定会不一致。
AndroidView -> TextureLayer,利用Android 上的副屏显示与虚拟内存显示原理。
共享内存,实时截图渲染技术。
存在问题,耗费内存,页面复杂时慢。
这部分由于以前之前聊过,就不赘述了
RN由于是原生控件,因此在react 和react native 整合这件事上存在难度。
flutter 做为一个UI 框架,与平台无关,在web上利用的是dart2js的能力。 好比Image
一、某些功能页面,能够一套代码实现,利用插件安装引入,在web、移动app、甚至 pc 上,均可以编译出对应平台的高性能代码,而不会像 Weex 等同样存在各类兼容问题。
二、在应用上能够快速实现“降级策略”,好比某种状况下应用产生奔溃了,能够替换为同等 UI 的 h5 显示,而这些代码只须要维护一份。
Github : github.com/CarGuo
RTC社区 : rtcdeveloper.com
开源 Flutter 完整项目:github.com/CarGuo/GSYG…
开源 Flutter 多案例学习型项目: github.com/CarGuo/GSYF…
开源 Fluttre 实战电子书项目:github.com/CarGuo/GSYF…