用前端最舒服的躺姿 "搞定" Flutter
一股股浓浓 : 不服来 “掰” 啊 !!!的味道。
是的,错过了react Native, weex 这些 “炸” 翻前端的技术,不能在错过 Flutter 了,这年头,你不会一门跨端技术,怎么好意思说自己是【前端】。
2020年5月,Flutter 团队带来了的第一个稳定版本,Flutter 1.17.x 版。
按照官方的说法,新版本关闭了自Flutter 1.12版本的6,339个问题,从231位贡献者那里合并了3164个PR,并修复了许多错误。除了质量改进之外,新版本还在增加了一些新功能:
移动性能和应用程序包大小的改进
运行速度将提高20%-37%,iOS动画减少40%的CPU / GPU使用率;Flutter Gallery示例,包大小减少了18.5%,快速滚动浏览大图像时,减少了70%的内存。
Metal对iOS性能提升50%
开发者可以使用 Metal API直接访问底层 GPU , iOS 设备上,Flutter 将默认使用 Metal,使得应用在绝大多数情况下都运行得更快,渲染速度平均提升约 50%。
新的Material Widget
这是一个新的 widget NavigationRail组件,它由 Google Material Design 团队设计并实现。 NavigationRail 非常适合在移动和桌面设备之间切换的应用。还增加了新的 Animations 库,提供了实现新的 Material motion规范的预构建动画。
Dart DevTools for Flutter
这是一个神器,用过chrome DevTools 的一定不陌生,Dart DevTools 用于 Dart 和 Flutter 代码调试和性能分析的工具。这套工具集功能非常全,包含性能、UI、热更新、热重载、log日志等很多功能。(这个工具以后会专门一期介绍)。
新的Flutter文本主题和Google字体
在新版本中,Flutter 团队在不破坏现有 Flutter 应用的同时,完成了对 2018 年 Material Design 规范文字排版缩放 (Type Scale) 的实现。也就是说TextTheme API是向前兼容的,对于旧的废弃名称只做警告。同时新的 主题也可以搭配 新的Google Fonts for Flutter v1.0 字体。
无障碍功能和国际化
更加全面的工作,对滚动、文本框以及其他输入 widget 的无障碍功能进行了修复。GitHub 上有在这个版本中完成修复的无障碍功能完整列表。国际化方面, 完成了三星键盘输入法的bug。
其他重要改动
在使用 pushReplacement 时,保留之前的路径,继续执行动画
弃用 UpdateLiveRegionEvent
在高速滚动时延迟图像解码
文本选取溢出bug (Android)
空绘图对象的断言缓存提示设置
实时图像缓存
可视范围内,确保构造块高度计算选区准确
在gen_l10n中生成消息查询
从RouteSettings中移除isinitialroute
将mouse_tracking.dart移动到rendering
具体细节可以参考官方地址:https://medium.com/flutter/flutter-spring-2020-update-f723d898d7af
每次迭代都有令人小激动的改进,但是对于刚接触 Flutter 的 web 前端同学来说还是......
好了,回到标题,笔者作为一名传统 web前端,想从前端最熟悉的视角 “躺” 着把 Flutter 了解一遍,不要敬仰,平视它!!!先从兴趣开始。
正文
Flutter 环境的搭建,其实有很多资源可以参考。这里就不赘述了(知道有很多坑,在后续文章中,有机会把个人遇到的坑汇总一下 )。
有兴趣可以参考 「flutter安装环境的搭建」(https://flutter.cn/docs/get-started/install/macos) , 在这里建议各位,一定要自己亲自搭一下环境,跑一下官方demo, 小马过河,焉知深浅,自己定的位才是最准确的。
有了环境,「配置编辑器」 (https://flutter.cn/docs/get-started/editor)。
然后「创建一个Flutter项目」(https://flutter.cn/docs/get-started/codelab)。
项目创建完成,可以先用 flutter run 跑一下。
flutter doctor // 让 flutter 医生检查一下环境和配置,得到四个 ☑ 后,恭喜你通过考核。
flutter run // 奔跑吧...
好了,跑起来了吧,你会看到一个计数的官方示例,点击加号图片可以做加运算。
这时候我们看项目的project 目录里 有一个入口文件叫 main.dart。然后打开 main.dart 就像下面这样:
( 为什么你们看到代码比我的长,因为我折叠了!!! ) 这不是重点,重点是每个类继承的都是一个尾号为Widget 的字符。
聪明的你一定会觉得 Widget 和 Flutter 有着某种神秘的联系。
“Binggo!",是的,Flutter 有两个重型武器,一个叫 Dart ,另一个就是 Widget 了。
Dart 一切皆来自 Object, Flutter 的组件皆来自 Widget。
关于强大的Dart,今天暂且不表,后面有时间可以独立篇幅来聊聊Dart。
先祭出一张 Flutter 的架构老图。
在 Flutter 的世界里,包括views,view controllers,layouts等在内的概念都建立在Widget之上。
Widget 是 Flutter 组件的抽象描述。所以掌握Flutter的基础就是学会使用 Widget开始。
在Flutter界面渲染过程分为三个阶段:布局、绘制、合成,布局和绘制在Flutter框架中完成,合成则交由引擎负责:
Flutter 通过组合、嵌套不同类型的控件,就可以构建出任意功能、任意复杂度的界面。
它包含的最主要的几个类有:
class WidgetsFlutterBinding extends BindingBase with GestureBinding, ServicesBinding, SchedulerBinding,PaintingBinding, RendererBinding, WidgetsBinding { ... }
abstract class Widget extends DiagnosticableTree { ... }
abstract class StatelessWidget extends Widget { ... }
abstract class StatefulWidget extends Widget { ... }
abstract class RenderObjectWidget extends Widget { ... }
abstract class Element extends DiagnosticableTree implements BuildContext { ... }
abstract class RenderObjectElement extends Element { ... }
class StatelessElement extends ComponentElement { ... }
class StatefulElement extends ComponentElement { ... }
上面这些类的主要作用如下:
基于Flutter控件系统开发的程序都需要使用WidgetsFlutterBinding,它是Flutter的控件框架和Flutter引擎的胶水层。
Widget就是所有控件的基类,它本身所有的属性都是只读的。
RenderObjectWidget所有的实现类则负责提供配置信息并创建具体的RenderObjectElement。
Element是Flutter用来分离控件树和真正的渲染对象的中间层,控件用来描述对应的element属性,控件重建后可能会复用同一个element。
RenderObjectElement持有真正负责布局、绘制和碰撞测试(hit test)的RenderObject对象。
StatelessWidget和StatefulWidget并不会直接影响RenderObject创建,只负责创建对应的RenderObjectWidget
StatelessElement和StatefulElement也是类似的功能。
很复杂是吧,先不用管,简单表述Widget是这样的:
Widget = 样式(css) + 标记语义(标签) + 组件化(官方) + 数据绑定(props)
“什么?这不就是 react 吗?"
对, React 的概念和 Flutter 的 Widget 是有相通性的。
“既然有react的概念,难道还有state,setState吗?“
又对, Flutter 还真有 类似 state 状态机制的概念,而且也确实有 setState 的方法。
“dome里有两个基类,StatelessWidget 和 StatefulWidget 是做什么用的?”
StatelessWidget 和 StatefulWidget,这里两个类特别重要,几乎所有的组件都是基于他们创建的。
StatelessWidget 是状态不可变的widget,称为 无状态widget。初始状态设置以后就不可再变化。如果需要变化需要重新创建。
StatefulWidget 可以保存自己的状态,称为 有状态widget。Flutter 首先保存了初始化时创建的State,状态是通过改变State,来重新构建 Widget 树来进行UI变化。改变状态的方法,就是我们用的最多的神器"setState",而单纯改变数据是不会引发UI改变的,这个概念和我们的 React 一样一样的。
如果你是初次接触 Flutter 可以不用记忆这么多组件基类,只用记住以下式子就可以, 不夸张的说,熟悉这个式子就可以开发 Flutter 项目了:
拆解
围绕着widget的构成,我们来拆解分析一下,标记语义,样式,组件化。
▐ 标记语义
为什么不称为 “模版”,“标签”,“element" ,而叫"标记语义",是因为flutter的 widget 结构并不只是 “模版”,“标签”,“element"。widget 描述结构更像是 React 的虚拟dom阶段,浓缩了相关上下文,以对象化的结构展示。
我们先来看看,React 创建出来的虚拟dom结构( 伪代码 ):
var newTree = el('div', {'id': 'container'}, [
el('h1', {style: 'color: red'}, ['simple virtal dom']),
el('p', ['Hello, virtual-dom']),
el('ul', [el('li'), el('li')])
])
再来看看,flutter 用Dart 创建的 widget 代码结构:
Widget build(BuildContext context) {
return new Column(
children: <Widget>[
new Container(
padding: new EdgeInsets.only(top:100.0),
child: new Text('这是一个组件')
),
new Container(
decoration: new BoxDecoration(border: new Border.all(width:1.0,color: Colors.blue)),
padding: new EdgeInsets.all(20.0),
child: new Text('来自输入框:'+active)
)
],
);
}
是不是这样看就熟悉很多了。
注:很多前端er 会不习惯这中书写方式,目前有开发者在社区推动,在编译前,使用jsx标签,编译后再解析成标记树,比如这个DSX设计的提案。
虽然jsx->标记树 还只是提案,但其实可以帮助我们更容易理解,此提案想表达的样式像这样:
class MyScaffold extends StatelessWidget {
build(context) {
return <Material>
<Column>
<MyAppBar
title={<Text
text='Example title'
style={Theme.of(context).primaryTextTheme.title},
/>}
/>
<Expanded>
<Center>
<Text text='Hello, world!'/>
</Center>
</Expanded>
</Column>
</Material>;
}
}
上面这段 jsx 要是用 Dart 来写是什么样的?如下:
class MyScaffold extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Material(
child: Column(
children: <Widget>[
MyAppBar(
title: Text(
'Example title',
style: Theme.of(context).primaryTextTheme.title,
), // Text
), // MyAppBar
Expanded(
child: Center(
child: Text('Hello, world!'),
), // Center
), // Expanded
], // <Widget>[]
), // Column
); // Material
}
}
虽然 Flutter 与 react 这么类比多少有些牵强,但可以个人总结一些方法,方便理解:
开头大写的类名 相当于 jsx 的标签名
child 相当于 jsx 的 子标签+布局
children 相当于 jsx 的 群组子标签(和child还是有区别的)
其他属性相当于 jsx 的 props
大家有没有注意, 第二段dart 语法结尾都会带上 “ // ” 注释符号,这个是编辑器IDE在识别是 Flutter 项目后,自动追加上去的,像 jsx 语言的标签封闭,方便发现标注的起始节点。
▐ 样式
对于 Flutter 样式的理解,可以查看官方的这篇文档,也是同样用类比的方式,很直观的了解,HTML、css样式和 flutter 之间的联系。
https://flutterchina.club/web-analogs/
笔者摘选其中样例的重点部分,对比展示来说明( 由于篇幅问题, 父子关系css 结构,用tab方式来表示 ):
文本样式:
.demo1 {
background-color: #e0e0e0;
width: 320px;
height: 240px;
font: 900 24px Georgia;
letter-spacing: 4px;
text-transform: uppercase;
}
var demo1 = new Container(
child: new Text(
"Lorem ipsum".toUpperCase(), // 对应 左边的文本转换大小写
style: new TextStyle( // 对应 左边的 font
fontSize: 24.0
fontWeight: FontWeight.w900,
fontFamily: "Georgia",
letterSpacing: 4.0,
),
),
width: 320.0, // 对应 左边的 width
height: 240.0, // 对应 左边的 height
color: Colors.grey[300], // 对应 左边的 background-color
);
样式居中:
.demo2 {
display: flex;
align-items: center;
justify-content: center;
}
var demo2 = new Container(
child: new Center( // 对应左边的整个 flex 属性
child: new Text("Lorem ipsum")
)
);
);
设置最大(小)宽度:
.container{
width:300px
.demo3 {
width: 100%;
max-width: 240px;
}
}
// 对于嵌套容器,如果父级的宽度小于子级宽度,则子级容器将自行调整大小以匹配父级。
var container = new Container(
child: new Center(
child: new Text("Lorem ipsum"),
decoration: new BoxDecoration( ... ), // constraints属性,创建一个新的BoxConstraints来设置minWidth或maxWidth
width: 240.0, // 对应左边的 max-width
),
width:300.0
),
旋转组件:
.redbox {
transform: rotate(15deg);
}
var container = new Container( // gray box
child: new Transform( // 对应左边的 transform
child: new Container( ... ),
alignment: Alignment.center, // 对应左边的 transform
transform: new Matrix4.identity() // 对应左边的 transform
..rotateZ(15 * 3.1415927 / 180),
),
)
);
缩放组件:
.redbox {
transform: scale(1.5);
}
var container = new Container( // gray box
child: new Transform( // 对应左边的 transform
child: new Container( ... ),
alignment: Alignment.center, // 对应左边的 transform的中心
transform: new Matrix4.identity() // 对应左边的 transform
..scale(1.5),
),
)
);
设置绝对位置和相对位置:
.greybox {
position: relative;
.redbox {
position: absolute;
top: 24px;
left: 24px;
}
}
var container = new Container( // grey box
child: new Stack( // 相对跟容器位置 relative
children: [
new Positioned( // 相对父容器位置 absolute
child: new Container( ... ),
left: 24.0,
top: 24.0,
)],
)
);
颜色渐变:
.redbox {
background: linear-gradient(180deg, #ef5350, rgba(0, 0, 0, 0) 80%);
}
var container = new Container( // grey box
child: new Center(
child: new Container( // red box
child: new Text( ... ),
decoration: new BoxDecoration(
gradient: new LinearGradient( // 对应左边的 background: linear-gradient
begin: const Alignment(0.0, -1.0),
end: const Alignment(0.0, 0.6),
colors: [
const Color(0xffef5350),
const Color(0x00ef5350)
],
),
)
),
)
);
圆角:
.redbox {
border-radius: 8px;
}
var container = new Container( // grey box
child: new Center(
child: new Container( // red circle
child: new Text( ... ),
decoration: new BoxDecoration(
borderRadius: new BorderRadius.all(
const Radius.circular(8.0),
),
)
),
)
);
阴影:
.redbox {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.8),
0 6px 20px rgba(0, 0, 0, 0.5);
}
var container = new Container( // grey box
child: new Center(
child: new Container( // red box
child: new Text( ... ),
decoration: new BoxDecoration(
boxShadow: [ // 对应左边的 box-shadow
new BoxShadow (
color: const Color(0xcc000000),
offset: new Offset(0.0, 2.0),
blurRadius: 4.0,
),
new BoxShadow (
color: const Color(0x80000000),
offset: new Offset(0.0, 6.0),
blurRadius: 20.0,
),
],
)
),
)
);
画圆:
.redcircle {
text-align: center;
width: 160px;
height: 160px;
border-radius: 50%;
}
var container = new Container( // grey box
child: new Center(
child: new Container( // red circle
child: new Text( ... ),
decoration: new BoxDecoration(
color: Colors.red[400],
shape: BoxShape.circle, // 画圆和圆角不太一样,用的是BoxShape绘制图像能力
),
width: 160.0,
height: 160.0,
),
)
);
内联样式:
// css 的内联结构
.greybox {
font: 900 24px Roboto;
.redbox {
em {
font: 300 48px Roboto;
font-style: italic;
}
}
}
var container = new Container( // grey box
child: new Center(
child: new Container( // red box
child: new RichText(
text: new TextSpan(
style: bold24Roboto,
children: [
new TextSpan(text: "Lorem "), // 继承内联样式
new TextSpan(
text: "ipsum",
style: new TextStyle( // 具有自定义样式的单独样式
fontWeight: FontWeight.w300,
fontStyle: FontStyle.italic,
fontSize: 48.0,
),
),
],
),
),
),
)
);
组件化
官方的widgets目录
点击每一个card后,里面还有子card, 以一个展开的纬度来看,是这样的:
这真是一个庞大的组件系统,这不是社区提供的,而是官方的。
flutter 团队事无巨细的实现了目前市面上基本上能见到的组件方式和类型。
个人认为这样做优缺点并存的。
先说缺点:
1.学习成本增加,曲线也还是比较陡的。
2.组件直接的继承关系路径比较零乱,比如
继承自 Widget 是这些大类道还清晰:
PreferredSizeWidget ProxyWidget RenderObjectWidget StatefulWidget StatelessWidget。
但是,StatelessWidget和StatefulWidget的子类就过于平行化了,名称上晦涩,没有抽象架构化,分层或者塔型级别。StatelessWidget:
StatefulWidget
3.对自定义组件定义模糊,有这么庞大的组件库,到底以后是有个一个更系统的第三方组件库去替换它,还是说 Flutter 官方就不建议使用第三方组件,这个也未有定论。
再说优点:
1.可以阻止以后轮子泛滥,在团队僵持不下使用哪个轮子库时,最好的理由就是“官方”二字,因为使用官方,可以弱化和规避一些问题,比如: 版本迭代不同步,性能瓶颈,规范不统一等问题,也能快速支持官方的辅助工具。
2.正是由于官方的标准划一,为自动化编译,自动化搭建,测试调优,可视化带来便利,甚至为Ai前端模型化业务场景,提供支撑,都说前端的组件像搭乐高,Flutter就是颗粒标准统一的乐高。
3.天下之势,分久必合,合久必分。
前端在经历了 flash 一统pc页面的富媒体时代,后被乔布斯和H5瓦解;
之后又有H5 的繁盛和框架、语法、构建模式的乱战;
再到有“别再更新,老子学不动"的呼声下,希望有个一统江湖的跨平台系统出现;
让开发者更专注于,提高业务内容,创造 “新, 酷,炫” 的展现形势上下功夫。
也许,真有一个前端江湖的王者的诞生。
最后
实际的 Flutter Widget 要复杂的更多的多,在眼花缭乱的 Widget 组件中,笔者想用自己的一些理解,去粗取精,来逐步理解 Flutter 这个新家伙 ,文中有理解不到位的地方,欢迎大家指正。笔者团队也正在开发一套《Flutter 菜鸟手册》的APP,帮助大家熟悉复杂的 Flutter Widget。
原文链接(https://github.com/ryan730/PersonalBlog/issues/1)
更多学习 Flutter的小伙伴,欢迎入QQ群 Flutter Go :679476515
《Flutter GO》项目地址 [alibaba/flutter-go](https://github.com/alibaba/flutter-go) 或点击阅读原文