现在,让我们来谈谈 Flutter 中的组件。
如果你去看 Flutter 的图书,或者去官网学习,会发现有大量的组件和相关的示例、练习等等。那为何到我们的小册这里,只用两讲的篇幅来介绍呢?
这是因为,无论是哪一个具体的组件,它们的使用方式都有相似之处。我们只需要掌握学习方法,然后应对几乎所有的组件就可以了。另一方面,随着 Flutter 框架的不断更新,新组件的推出和旧组件用法的更新也会随之来临。再加上组件本身数量庞大,变化多样,如果靠记忆的方式学习就会很累,要不断更新大脑中的"知识库"才行。
所以我们就不如把握组件的"核心"领域,也就是牢牢抓住那些不变的部分,从而做到"以不变应万变"。更可以换一种视角,即把千万种组件当作是实现产品的"工具",而非必备知识库。用到的时候,随时查询"工具使用说明书",也就是文档,照着"抄"就行了,根本没那么难。
对于特别基础且常用的组件,因为太常用了,所以会自动形成记忆。对于那些不常用的,就算记住了,受记忆曲线的规律限制,也会被淡忘,更没有刻意去记忆的必要。
说了那么多,其实就只有一个目的:大家可不要死记硬背,特别是对于编程,特别是对于 Flutter 中的组件。
所以,在本讲中,我会为大家介绍那些不变的"核心"知识。另外,随着后续实战项目的展开,大家就更能体会到"现用现查"的高效了。
组件(Widget)基础
在 Flutter 中,有一句话是这么说的:"一切皆组件"。这是因为一个 Flutter 程序几乎都是由不同的组件(Widget)所构成的 。无论某个组件看上去样式多么华丽、能响应多么复杂的手势,它都是由基础组件构成的,无非是不同的排列组合而已 。我们只要理解和掌握 常用的基础组件的用法,就不难使用那些经过组合而成的千万种组件了。
💡 提示:对于基础组件,需要"理解"、"掌握";对于由其构成的组件,直接"使用"。
为了便于学习,我把 Flutter 中所有的基础组件分为界面组件、布局组件和手势组件三部分。
- 界面组件:文本显示(Text)、图片显示(Image)、复选框(Checkbox)等等看得见、摸得着的都算;
- 布局组件:包括垂直布局(Column)、水平布局(Row)、垂直列表布局(ListView)、网格列表布局(GridView)、层叠布局(Stack)等等为界面组件的摆放起"指导"和"约束"作用的一类组件,通常看不见,但又起着重要的作用;
- 手势组件:专门理解处理用户"意图",并调度执行特定代码的组件。这类组件可以给原本不能响应手势的组件赋予"灵魂",比如文本显示组件(Text)与手势组件结合后,可以变身为文本按钮。
看到这,你应该拥有了"文本按钮"的实现思路。下图演示了"文本按钮"的代码和点击效果:

橙色框的代码是一个文本显示组件,对应右边运行图中橙色框。玫红色框的代码就是一个手势组件,文本显示组件作为其子组件结合在一起, onTap() 是手势组件的一部分,表示单点触摸时要执行的具体任务。当用户点击屏幕上的"Press me"时,手势组件捕获到用户的点击,满足单点触摸条件,任务执行,控制台输出"Oh My God!!!"字样。
整个过程是不是特别容易?不知不觉中,完成了一次自定义组件。如果想要别的样式,还可以附加,完全自由灵活。
接下来,我会分别介绍上述三类组件。
界面组件
界面组件可以说是数量最为庞大的一类,同时也是最不值得大书特书的一类。使用它们的总原则就是:看"说明书"即可。
这里举个例子,这个组件还算是常用,且稍微复杂的那种:Scaffold。
新创建的 Flutter App 都是一个点击计数的小程序,其中就用到了 Scaffold。Scaffold 被称为"脚手架",使用它可以快速实现符合 Material Design 设计风格的应用程序。
先不要太深入细节,当我们阅读这个程序的源码时,会发现使用 Scaffold 部分是这样的:

总共传入了 3 个参数,appBar 对应应用程序的顶栏,body 对应内容区域,floatingActionButton 对应右下角的按钮。
如果从未用过 Scaffold,该如何下手呢?我们不妨按键盘上的 Ctrl + 鼠标左键点击 Scaffold 处,跳转到源码。
当我们查看 Scaffold 源码时,会发现它有很多参数。别慌,看注释。英文看不懂怎么办?直接用工具翻译即可。
在众多参数中,我留意到了 drawer。它的解释是:
Drawer, which is a vertical panel that is typically displayed to the left of the body (and often hidden on phones) using the drawer property.*
使用工具翻译结果是:
Drawer,这是一个垂直面板,通常使用Drawer属性显示在机身左侧(通常在手机上隐藏)。
稍微有点 Material Design 常识,或使用 Android 设备较多的同学肯定会猜到,这个 drawer 是不是像这样:

试试就知道。回到代码,传入 drawer 参数,具体如下:
dart
return Scaffold(
...
drawer: Drawer(child: Text("抽屉"),),
...
);
重新运行程序,会发现程序顶栏左侧自动出现了三个横线模样的按钮,在吸引我们点击它。之后我们发现,它果然就是 Android 中的抽屉菜单。

是不是觉得这个抽屉菜单有点不太美观?没关系,继续 Ctrl + 鼠标左键点击,看 Drawer 源码。稍加改动,界面就变成了这样:

代码则是这样:

之所以我不再展开具体代码内容,是因为我这段完全是照抄源码注释的内容。
没错!就是直接照抄,然后改成实际需求就行了。一切以实用为主,界面组件没有什么深奥的东西,基本就是这个流程:看说明书,然后照抄,最后改改就能交差了。
最后,我要说一下,为什么我会想到用 Drawer 组件作为参数呢?这其实也不难想到:仔细观察 Scaffold 的其它三个参数,appBar 参数传入的是 AppBar,floatingActionButton 参数传入的是 FloatingActionButton,只有 body 参数传入的是 Center,那 drawer 参数是不是也有 Drawer 与其对应呢?别猜,试试呗。
布局类组件
使用布局类组件就需要一点思考了(是"使用",而非"学习"),特别是对于复杂的界面。
Flutter 中的布局最常用的有水平(Row)、水平列表(ListView)、垂直(Column)、网格布局(GridView),和堆叠(Stack)布局等等。它们通常需要组件构成的数组作为参数。
还是回到那个计数器程序,展开 Scaffold 中 body 参数部分的代码,它是这样的:

仔细看上图的对应关系。
- 紫色边框部分表示整个内容区域是一个 Center 组件。它是一个布局类的组件,从名字上就可以得知,该组件可以将其子组件居中布局;
- Column 组件作为 Center 组件的子组件,也是布局类组件,表示其中的子组件依次垂直排布,对应图中绿色的部分。mainAxisAlignment 参数用于指定垂直方向上的对齐方式,这里使用居中(
MainAxisAlignment.center),所以我们可以看到两行文字在垂直方向上恰好居于绿色区域的中间; - 橙色部分表示 Column 组件的子组件数组,对应上图右边的橙色区域。
这就是垂直布局(Column)的使用方法了,与其类似的还有水平布局(Row),它们在使用上基本一致,大家可以自行练习使用。
需要特别说明的是垂直列表布局(ListView)。在 Flutter 中,抽象了 ListView 中的组件类型。做过 Android 开发的都知道,ListView 中的子视图通常使用同一布局文件,也就意味着每个子视图都是相似的。
如果要求每个子视图都不一样,使用 ListView 就比较麻烦了。但 Flutter 中"一切皆组件",这就意味着 ListView 中的子组件只要是组件就行了,也就使得每个子组件不一样的目标可以非常简单地达成。
甚至 Column 和 ListView 之间的界限也逐渐模糊。当使用 Column 时,一旦子组件太高,超过屏幕范围,则会溢出报错。但使用 ListView 时,则会自动变成滚动交互,避免视图溢出。这一点在网格布局(GridView)和水平布局(Row)中同样适用。
具体来说,如果将代码修改如下,运行便会出错:

可以看到,我将字号改大,然后复制成 3 份,便会出现箭头所指的溢出错误。
如果将 Column 改成 ListView,便能化解这一问题:

仅修改了红色箭头所指的位置,程序便能正常运行了。用户可以通过上下拖动查看位于屏幕之外的内容。
那么,ListView 就一定比 Column 好吗?其实也不一定。从本例就可以明显看出来:改成 ListView 之后,文字就不再居中了,需要在每个 Text 中重新指定。因为 ListView 没有 mainAxisAlignment 参数,子组件自动占满整行宽度。
最后再来看看 Stack,它是堆叠布局。和前面用过的布局方式都不一样,它像叠罗汉似地,把所有的子组件逐层叠加。乍看上去似乎没什么用,但在实现某些 UI 效果时却有奇效。比如下面这个:

Stack 实现了橙色箭头所指向的圆角矩形图标。
从代码中可以看到,Stack 同样接受由组件构成的数组。遵循"层层覆盖"原则,alignment 参数是子组件的对齐方式。在布局时,白色的 Icon 组件覆盖了绿色的 Container 组件,前者是图标的内容,后者是图标的背景。
以上便是几种常用的布局类组件,几乎所有的应用程序都离不开它们。所以对待它们的原则就是:多练习,然后让大脑自动形成记忆。
手势组件
手势组件虽然看不见,但是"摸得着"。它可以"赋能"本不能发生互动的组件。
比如本讲第一个示例,将 Text 作为 GestureDetector 的子组件。Text 本身无法响应点击,但经过这一步骤后,它就可以对用户的点击做出响应了。
GestureDetector 可以侦测多种互动方式,比如按下、抬起、双指按下、双指抬起、双击、长按、横向/纵向拖动等等,大家可以阅读 gesture_detector.dart 源码来了解所有支持的动作类型。它们的使用大同小异,这里就不再赘述了。
手势组件是最好记的一类,因为它只有一个,即 GestureDetector,而且在很多时候是用不到它的。因此对待它的原则就是:记住名字,根据参数名推测具体动作,实在不行就去查"说明书" 。
小结
🎉 恭喜,您完成了本次课程的学习!
📌 以下是本次课程的重点内容总结:
本讲首先概括地介绍了 Flutter 中"组件"的基础知识,然后具体讲了 Flutter 中三类组件,具体而言包括学法和用法。
-
界面组件:文本显示(Text)、图片显示(Image)、复选框(Checkbox)等等看得见、摸得着的都算。
- 应对策略:看说明书,然后照抄,最后改改就能交差了。
-
布局类组件:包括垂直布局(Column)、水平布局(Row)、垂直列表布局(ListView)、网格列表布局(GridView)、层叠布局(Stack)等等为界面组件的摆放起"指导"和"约束"作用的一类组件,通常看不见,但又起着重要的作用。
- 应对策略:多练习,然后让大脑自动形成记忆。
-
手势组件:专门理解处理用户"意图",并调度执行特定代码的组件。
- 应对策略:记住名字,根据参数名推测具体动作,实在不行就去查"说明书"。
总体来说,Flutter 中的组件看似数量庞大,千变万化,但本质上都是基础组件的各种排列组合,叠加使用的结果。正如本讲的第一个示例,非常轻松地就实现了自定义组件。我们的应对策略绝非死记硬背,而是抓住变化中不变的部分,做到"以不变应万变"。
➡️ 在下次课程中,我们继续聊 Flutter 中的组件,如何让组件与数据发生联动,继续下一讲的学习,你将得到答案。