随着 Google I/O 大会的结束,Flutter 的新消息不断,如:谷歌宣布 Flutter 1.9 版本、Dart 2.5 版本正式发布。另外还宣布了 Flutter Web 支持项目的一项重大里程碑:Flutter 的 Web 支持集成到了 Flutter 的主库中,让开发人员可以使用同一套代码库为移动、桌面和 Web 平台开发应用。Flutter 如此强大,是不是需要早知道一些知识?
一、Flutter 怎么没有标记语言 (markup language) 和语法?
Flutter 的 UI 由指令式的面向对象语言语言构建,也就是 Dart。它也是 Flutter 框架的编写语言。Flutter 本身并不包含声明式的标记语言。
Google 认为将 UI 交给代码来动态构建会带来更多的灵活性。比如,开发者发现固化的标记语言系统很难表达一个从视觉到行为都完全定制的 widget。另外,“代码优先” 的开发也使得热重载以及动态环境适配等特性能更好地得以实现。
从根本上来讲,创造出一种能动态转化成 widget 的语言是可能的,毕竟构建方法说到底也还是代码,它们能做的事情很多,自然也包括将标记语言转化成 widget。
二、为什么 build() 方法被放在 State 上,而不是 StatefulWidget 上?
将 widget build(BuildContext context) 方法放在 State 上,而不是将 widget build(BuildContext context, State state) 方法放在 StatefulWidget 上,能让开发者在继承 StatefulWidget 时拥有更多的灵活性。
三、应用运行时在右上角有一个 Debug 的标识,为什么?
默认情况下,flutter run 指令会使用 debug 编译配置。
Debug 编译配置会在一个 VM 里运行您的 Dart 代码,从而提供更快速的开发操作周期,如热重载。如果是编译发布版本的话,则会使用 Android 和 iOS 标准的工具链。
Debug 编译配置也会检查所有的断言 (assert),这会帮助您在开发时更早地发现错误,但这也会加大运行时的开销。您看到的 Debug 标识是告诉您这些检查目前是打开的状态。您可以通过在运行 flutter run 时附加 --profile 或者 --release 来跳过这些检查。
如果您在使用 IntelliJ 的 Flutter 插件,您可以在 profile 或者 release 模式下启动应用,只需要在菜单里选择 Run > Flutter run in Profile Mode 或者 Release Mode。
四、Flutter 框架采用了哪些编程范式?
Flutter 是一个多范式的编程环境。过去几十年中许多编程技术都有在 Flutter 中使用。我们在选择范式时会考虑其适用性进行综合性的决策。以下列出的范式不分先后:
-
组合 (composition): 这也是 Flutter 的主要开发范式,使用简单有限行为的小对象进行组合,从而实现更复杂的效果。绝大多数 Flutter widget 都是用这种方法构建的。比如 Material FlatButton 类是基于 MaterialButton 类构建的,而这个类则是由 IconTheme、InkWell、Padding、Center、Material、AnimatedDefaultTextStyle 以及 ConstrainedBox 组合而成的。而 InkWell 则是由 GestureDetector 组成,Material 则是由 AnimatedDefaultTextStyle、NotificationListener 和 AnimatedPhysicalModel 组成。如此等等。
-
函数式编程 (functional programming): 整个应用都可以只用 StatelessWidget 来构建,它本质上就是一些方法,用来描述如何将参数传送给其他方法,以及在布局区域内计算布局以及绘制图像。当然这样的应用一般也不会包含状态,所以通常也无法进行交互。比如,Icon widget 就只是一个将其元素 (颜色、图标、尺寸) 罗列在布局区域内的方法。另外,当这个范式被重度使用时,则会使用不可变的数据结构,如整个 Widget 类及其派生,以及一些辅助类,如 Rect 和 TextStyle。另外,从一个较小的尺度来看的话,Dart 的 Iterable API 也重度使用了这个范式 (如 map, reduce, where 等方法),它在框架中经常被用来处理一序列的值。
-
事件驱动编程 (event-driven programming): 用户的交互操作被包装成事件对象,这些对象发送给被各个 event handler 注册的回调方法。屏幕内容的更新使用的也是类似的回调机制。比如,被做为动画系统构建基础的 Listenable 类,就采用了包含多个事件监听者的订阅模型。
-
面向类编程 (class-based programming,是面向对象编程的一种方式): 框架内绝大多数的 API 是由包含各种继承关系的类来组成的。我们在基本类中定义较高级别的 API,然后在其子类中对这些 API 进行特化处理。比如,我们的渲染对象就有一个基本类 RenderObject,它对坐标系的细节并不关心,但它的子类 RenderBox 就引入了笛卡尔坐标系的概念 (x/y 坐标值,以及宽度高度的概念)。
-
原型编程 (prototype-based programming,同样是面向对象编程的一种方式): SrollPhysics 类在运行时动态链接那些会组成滚动逻辑的实例。这就使得系统无需在编译时提前选择平台的情况下,也能组合出符合平台特性的翻页滚动效果。
-
指令式编程 (imperative programming): 简单直白的指令式编程,通常和对象内封装的状态 (state) 搭配使用,这种范式能提供最符合直觉的解法。比如,测试就是使用指令式编程实现的,首先描述出测试的环境,然后给出测试需要满足的定量,最后开始步进,或者根据测试需要插入事件。
-
响应式编程 (reactive programming): Widget 和元素树有时候被描述为响应式的,因为随 widget 构造方法引入的新输入会随着其 build 方法传播给更低等级的 widget;而底层 widget 中出现的修改 (如响应用户的输入) 也会沿着结构树通过 event handler 向上传播。在整个框架中,函数-响应式以及指令-响应式的实现都有出现,具体取决于 widget 的功能需求。Widget 的 build 方法如果只是包含其针对变化如何响应的表达式的话,就是函数-响应式 widget (如 Material Divider 类)。如果 widget 的 build 方法包含一系列的表达式,用于描述该 widget 如何响应变化的话,那它就是指令响应式 widget (如 Chip 类)。
-
声明式编程 (declarative programming): Widget 的 build 方法通常都是一个单一表达式,它包含多级嵌套的构造函数,且使用 Dart 严格的声明式子集编写。这些嵌套的表达式可以被机械地与合适的标记语言互相转换。比如,UserAccountsDrawerHeader 这个 widget 就有一个很长的 build 方法 (20 多行),由一个嵌套的表达式构成。这种范式也可以和指令式混合使用,以实现某些很难用纯声明式的方法实现的 UI。
-
泛型程序设计 (generic programming): 类型可以帮助开发者更早地抓到错误,基于这一点,Flutter 框架也采用了范型开发。比如,State 类就是如此,其关联的 widget 就是类型参数,如此一来 Dart 分析器就能捕获到 state 和 widget 不匹配的情况。类似的,GlobalKey 类就接受一个类型参数,从而类型安全地访问一个 widget 的 state (会使用运行时检查)。Route 接口也在被 pop 时接受类型参数,另外 List, Map, Set 这些集合也都如此,这样就可以在分析或者运行时尽早发现类型不匹配的错误。
-
并发 (concurrent programming): Flutter 大量使用诸如 Future 等异步 API。比如,动画系统就会在动画执行完 future 时进行事件告知。同样的,图片加载系统也会使用 future 告知读取完毕。
-
约束编程 (constraint programming): Flutter 的布局系统使用了约束编程的简化形态来描述一个场景的几何性质。约束值 (比如一个笛卡尔矩形允许的最大 / 最小宽高值) 会从父元素传递给子元素,子元素最终选择一个能满足上面所有约束条件的最终尺寸。这种做法也使得 Flutter 能不依赖太多输入的情况下快速完成一个全新的布局。