Widget简介

概念

在前面的介绍中,我们知道,在Flutter中,几乎所有的对象都是一个Widget,与原生开发中的“控件”不同的是,Flutter中的widget的概念更广泛,它不仅可以表示UI元素,也可以表示一些功能性的组件如:用于手势检测的 GestureDetector widget、用于应用主题数据传递的Theme等等。而原生开发中的控件通常只是指UI元素。在后面的内容中,我们在描述UI元素时,我们可能会用到“控件”、“组件”这样的概念,读者心里需要知道他们就是widget,只是在不同场景的不同表述而已。由于Flutter主要就是用于构建用户界面的,所以,在大多数时候,读者可以认为widget就是一个控件,不必纠结于概念。

Widget与Element

在Flutter中,Widget的功能是“描述一个UI元素的配置数据”,它就是说,Widget其实并不是表示最终绘制在设备屏幕上的显示元素,而只是显示元素的一个配置数据。实际上,Flutter中真正代表屏幕上显示元素的类是Element,也就是说Widget只是描述Element的一个配置,有关Element的详细介绍我们将在本书后面的高级部分深入介绍,读者现在只需要知道,Widget只是UI元素的一个配置数据,并且一个Widget可以对应多个Element,这是因为同一个Widget对象可以被添加到UI树的不同部分,而真正渲染时,UI树的每一个节点都会对应一个Element对象。总结一下:

  • Widget实际上就是Element的配置数据,Widget树实际上是一个配置树,而真正的UI渲染树是由Element构成;不过,由于Element是通过Widget生成,所以它们之间有对应关系,所以在表述上,我们可以宽泛的认为Widget树就是指UI控件树或UI渲染树。
  • 一个Widget对象可以对应多个Element对象。

读者应该将这两点刻在心中。

主要接口

我们先来看一下Widget类的声明:

  1. @immutable
  2. abstract class Widget extends DiagnosticableTree {
  3. const Widget({ this.key });
  4. final Key key;
  5. @protected
  6. Element createElement();
  7. @override
  8. String toStringShort() {
  9. return key == null ? '$runtimeType' : '$runtimeType-$key';
  10. }
  11. @override
  12. void debugFillProperties(DiagnosticPropertiesBuilder properties) {
  13. super.debugFillProperties(properties);
  14. properties.defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.dense;
  15. }
  16. static bool canUpdate(Widget oldWidget, Widget newWidget) {
  17. return oldWidget.runtimeType == newWidget.runtimeType
  18. && oldWidget.key == newWidget.key;
  19. }
  20. }
  • Widget类继承自DiagnosticableTreeDiagnosticableTree即“诊断树”,主要作用是提供调试信息。
  • Key: 这个key属性类似于React/Vue中的key,主要的作用是决定是否在下一次build时复用旧的widget,决定的条件在canUpdate()方法中。
  • createElement():正如前文所述“一个Widget可以对应多个Element”;Flutter Framework在构建UI树时,会先调用此方法生成对应节点的Element对象。此方法是Flutter Framework隐式调用的,在我们开发过程中基本不会调用到。
  • debugFillProperties(...) 复写父类的方法,主要是设置诊断树的一些特性。
  • canUpdate(...)是一个静态方法,它主要用于在Widget树重新build时复用旧的widget,其实具体来说,应该是:是否用新的Widget对象去更新旧UI树上所对应的Element对象的配置;通过其源码我们可以看到,只要newWidgetoldWidgetruntimeTypekey同时相等时就会用newWidget去更新Element对象的配置,否则就会创建新的Element

有关Key和Widget复用的细节将会在本书后面高级部分深入讨论,读者现在只需知道,为Widget显式添加key的话可能(但不一定)会使UI在重新构建时变的高效,读者目前可以先忽略此参数。本书后面的示例中,我们只在构建列表项UI时会显示指定Key。

另外Widget类本身是一个抽象类,其中最核心的就是定义了createElement()接口,在Flutter开发中,我们一般都不用直接继承Widget类来实现Widget,相反,我们通常会通过继承StatelessWidgetStatefulWidget来间接继承Widget类来实现,而StatelessWidgetStatefulWidget都是直接继承自Widget类,而这两个类也正是Flutter中非常重要的两个抽象类,它们引入了两种Widget模型,接下来我们将重点介绍一下这两个类。

Stateless Widget

在之前的章节中,我们已经简单介绍过StatelessWidget,StatelessWidget相对比较简单,它继承自Widget,重写了createElement()方法:

  1. @override
  2. StatelessElement createElement() => new StatelessElement(this);

StatelessElement 间接继承自Element类,与StatelessWidget相对应(作为其配置数据)。

StatelessWidget用于不需要维护状态的场景,它通常在build方法中通过嵌套其它Widget来构建UI,在构建过程中会递归的构建其嵌套的Widget。我们看一个简单的例子:

  1. class Echo extends StatelessWidget {
  2. const Echo({
  3. Key key,
  4. @required this.text,
  5. this.backgroundColor:Colors.grey,
  6. }):super(key:key);
  7. final String text;
  8. final Color backgroundColor;
  9. @override
  10. Widget build(BuildContext context) {
  11. return Center(
  12. child: Container(
  13. color: backgroundColor,
  14. child: Text(text),
  15. ),
  16. );
  17. }
  18. }

上面的代码,实现了一个回显字符串的Echo widget。

按照惯例,widget的构造函数应使用命名参数,命名参数中的必要参数要添加@required标注,这样有利于静态代码分析器进行检查,另外,在继承widget时,第一个参数通常应该是Key,如果接受子widget的child参数,那么通常应该将它放在参数列表的最后。同样是按照惯例,widget的属性应被声明为final,防止被意外改变。

然后我们可以通过如下方式使用它:

  1. Widget build(BuildContext context) {
  2. return Echo(text: "hello world");
  3. }

Screenshot_1535019164

Stateful Widget

和StatelessWidget一样,StatefulWidget也是继承自widget类,并重写了createElement()方法,不同的是返回的Element 对象并不相同;另外StatefulWidget类中添加了一个新的接口createState(),下面我们看看StatelessWidget的类定义:

  1. abstract class StatefulWidget extends Widget {
  2. const StatefulWidget({ Key key }) : super(key: key);
  3. @override
  4. StatefulElement createElement() => new StatefulElement(this);
  5. @protected
  6. State createState();
  7. }
  • StatefulElement 间接继承自Element类,与StatefulWidget相对应(作为其配置数据)。StatefulElement中可能会多次调用createState()来创建状态(State)对象。

  • createState() 用于创建和Stateful widget相关的状态,它在Stateful widget的生命周期中可能会被多次调用。例如,当一个Stateful widget同时插入到widget树的多个位置时,Flutter framework就会调用该方法为每一个位置生成一个独立的State实例,其实,本质上就是一个StatefulElement对应一个State实例。

    在本书中经常会出现“树“的概念,在不同的场景可能指不同的意思,在说“widget树”时它可以指widget结构树,但由于widget与Element有对应关系(一可能对多),在有些场景(Flutter的SDK文档中)也代指“UI树”的意思。而在stateful widget中,State对象也和StatefulElement具有对应关系(一对一),所以在Flutter的SDK文档中,可以经常看到“从树中移除State对象”或“插入State对象到树中”这样的描述。其实,无论哪种描述,其意思都是在描述“一棵构成用户界面的节点元素的树”,读者不必纠结于这些概念,还是那句话“得其神,忘其形”,因此,本书中出现的各种“树”,如果没有特别说明,读者都可抽象的认为它是“一棵构成用户界面的节点元素的树”。

State

一个StatefulWidget类会对应一个State类,State表示与其对应的StatefulWidget要维护的状态,State中的保存的状态信息可以:

  1. 在widget build时可以被同步读取。
  2. 在widget生命周期中可以被改变,当State被改变时,可以手动调用其setState()方法通知Flutter framework状态发生改变,Flutter framework在收到消息后,会重新调用其build方法重新构建widget树,从而达到更新UI的目的。

State中有两个常用属性:

  1. widget,它表示与该State实例关联的widget实例,由Flutter framework动态设置。注意,这种关联并非永久的,因为在应用声明周期中,UI树上的某一个节点的widget实例在重新构建时可能会变化,但State实例只会在第一次插入到树中时被创建,当在重新构建时,如果widget,Flutter framework会动态设置State.widget为新的widget实例。

  2. context,它是BuildContext类的一个实例,表示构建widget的上下文,它是操作widget在树中位置的一个句柄,它包含了一些查找、遍历当前Widget树的一些方法。每一个widget都有一个自己的context对象。

    对于BuildContext读者现在可以先作了解,随着本书后面内容的展开,也会用到Context的一些方法,读者可以通过具体的场景对其有个直观的认识。关于BuildContext更多的内容,我们也将在后面高级部分再深入介绍。

State生命周期

理解State的生命周期对flutter开发非常重要,为了加深读者印象,本节我们通过一个实例来演示一下State的生命周期。在接下来的示例中,我们实现一个计数器widget,点击它可以使计数器容加1,由于要保存计数器的数值状态,所以我们应继承StatefulWidget,代码如下:

  1. class CounterWidget extends StatefulWidget {
  2. const CounterWidget({
  3. Key key,
  4. this.initValue: 0
  5. });
  6. final int initValue;
  7. @override
  8. _CounterWidgetState createState() => new _CounterWidgetState();
  9. }

CounterWidget接收一个initValue整形参数,它表示计数器的初始值。下面我们看一下State的代码:

  1. class _CounterWidgetState extends State<CounterWidget> {
  2. int _counter;
  3. @override
  4. void initState() {
  5. super.initState();
  6. //初始化状态
  7. _counter=widget.initValue;
  8. print("initState");
  9. }
  10. @override
  11. Widget build(BuildContext context) {
  12. print("build");
  13. return Center(
  14. child: FlatButton(
  15. child: Text('$_counter'),
  16. //点击后计数器自增
  17. onPressed:()=>setState(()=> ++_counter) ,
  18. ),
  19. );
  20. }
  21. @override
  22. void didUpdateWidget(CounterWidget oldWidget) {
  23. super.didUpdateWidget(oldWidget);
  24. print("didUpdateWidget");
  25. }
  26. @override
  27. void deactivate() {
  28. super.deactivate();
  29. print("deactive");
  30. }
  31. @override
  32. void dispose() {
  33. super.dispose();
  34. print("dispose");
  35. }
  36. @override
  37. void reassemble() {
  38. super.reassemble();
  39. print("reassemble");
  40. }
  41. @override
  42. void didChangeDependencies() {
  43. super.didChangeDependencies();
  44. print("didChangeDependencies");
  45. }
  46. }

接下来,我们创建一个新路由,在新路由中,我们只显示一个CounterWidget

  1. Widget build(BuildContext context) {
  2. return CounterWidget();
  3. }

我们运行应用并打开该路由页面,在新路由页打开后,屏幕中央就会出现一个数字0,然后控制台日志输出:

  1. I/flutter ( 5436): initState
  2. I/flutter ( 5436): didChangeDependencies
  3. I/flutter ( 5436): build

可以看到,在StatefulWidget插入到Widget树时首先initState方法会被调用。

然后我们点击⚡️按钮热重载,控制台输出日志如下:

  1. I/flutter ( 5436): reassemble
  2. I/flutter ( 5436): didUpdateWidget
  3. I/flutter ( 5436): build

可以看到时initStatedidChangeDependencies都没有被调用,而此时didUpdateWidget被调用。

接下来,我们在widget树中移除CounterWidget,将路由build方法改为:

  1. Widget build(BuildContext context) {
  2. //移除计数器
  3. //return CounterWidget();
  4. //随便返回一个Text()
  5. return Text("xxx");
  6. }

然后热重载,日志如下:

  1. I/flutter ( 5436): reassemble
  2. I/flutter ( 5436): deactive
  3. I/flutter ( 5436): dispose

我们可以看到,在CounterWidget从widget树中移除时,deactivedispose会依次被调用。

下面我们来看看各个回调函数:

  • initState:当Widget第一次插入到Widget树时会被调用,对于每一个State对象,Flutter framework只会调用一次该回调,所以,通常在该回调中做一些一次性的操作,如状态初始化、订阅子树的事件通知等。不能在该回调中调用BuildContext.inheritFromWidgetOfExactType(该方法用于在Widget树上获取离当前widget最近的一个父级InheritFromWidget,关于InheritedWidget我们将在后面章节介绍),原因是在初始化完成后,Widget树中的InheritFromWidget也可能会发生变化,所以正确的做法应该在在build()方法或didChangeDependencies()中调用它。
  • didChangeDependencies():当State对象的依赖发生变化时会被调用;例如:在之前build() 中包含了一个InheritedWidget,然后在之后的build()InheritedWidget发生了变化,那么此时InheritedWidget的子widget的didChangeDependencies()回调都会被调用。典型的场景是当系统语言Locale或应用主题改变时,Flutter framework会通知widget调用此回调。
  • build():此回调读者现在应该已经相当熟悉了,它主要是用于构建Widget子树的,会在如下场景被调用:

    1. 在调用initState()之后。
    2. 在调用didUpdateWidget()之后。
    3. 在调用setState()之后。
    4. 在调用didChangeDependencies()之后。
    5. 在State对象从树中一个位置移除后(会调用deactivate)又重新插入到树的其它位置之后。
  • reassemble():此回调是专门为了开发调试而提供的,在热重载(hot reload)时会被调用,此回调在Release模式下永远不会被调用。
  • didUpdateWidget():在widget重新构建时,Flutter framework会调用Widget.canUpdate来检测Widget树中同一位置的新旧节点,然后决定是否需要更新,如果Widget.canUpdate返回true则会调用此回调。正如之前所述,Widget.canUpdate会在新旧widget的key和runtimeType同时相等时会返回true,也就是说在在新旧widget的key和runtimeType同时相等时didUpdateWidget()就会被调用。
  • deactivate():当State对象从树中被移除时,会调用此回调。在一些场景下,Flutter framework会将State对象重新插到树中,如包含此State对象的子树在树的一个位置移动到另一个位置时(可以通过GlobalKey来实现)。如果移除后没有重新插入到树中则紧接着会调用dispose()方法。
  • dispose():当State对象从树中被永久移除时调用;通常在此回调中释放资源。

todo: 这里缺一张生命周期图

注意:在继承StatefulWidget重写其方法时,对于包含@mustCallSuper标注的父类方法,都要在子类方法中先调用父类方法。

状态管理

响应式的编程框架中都会有一个永恒的主题——“状态管理”,无论是在React/Vue(两者都是支持响应式编程的web开发框架)还是Flutter,他们讨论的问题和解决的思想都是一致的。所以,如果你对React/Vue的状态管理有了解,可以跳过本节。言归正传,我们想一个问题,stateful widget的状态应该被谁管理?widget本身?父widget?都会?还是另一个对象?答案是取决于实际情况!以下是管理状态的最常见的方法:

  • Widget管理自己的state。
  • 父widget管理子widget状态。
  • 混合管理(父widget和子widget都管理状态)。

如何决定使用哪种管理方法?以下原则可以帮助你决定:

  • 如果状态是用户数据,如复选框的选中状态、滑块的位置,则该状态最好由父widget管理。
  • 如果状态是有关界面外观效果的,例如颜色、动画,那么状态最好由widget本身来管理。
  • 如果某一个状态是不同widget共享的则最好由它们共同的父widget管理。

在widget内部管理状态封装性会好一些,而在父widget中管理会比较灵活。有些时候,如果不确定到底该怎么管理状态,那么推荐的首选是在父widget中管理(灵活会显得更重要一些)。

接下来,我们将通过创建三个简单示例TapboxA、TapboxB和TapboxC来说明管理状态的不同方式。 这些例子功能是相似的 ——创建一个盒子,当点击它时,盒子背景会在绿色与灰色之间切换。状态 _active确定颜色:绿色为true ,灰色为false

a large green box with the text, 'Active'a large grey box with the text, 'Inactive'

下面的例子将使用GestureDetector来识别点击事件,关于该GestureDetector的详细内容我们将在后面“事件处理”一章中介绍。

Widget管理自身状态

_TapboxAState 类:

  • 管理TapboxA的状态。
  • 定义_active:确定盒子的当前颜色的布尔值。
  • 定义_handleTap()函数,该函数在点击该盒子时更新_active,并调用setState()更新UI。
  • 实现widget的所有交互式行为。
  1. // TapboxA 管理自身状态.
  2. //------------------------- TapboxA ----------------------------------
  3. class TapboxA extends StatefulWidget {
  4. TapboxA({Key key}) : super(key: key);
  5. @override
  6. _TapboxAState createState() => new _TapboxAState();
  7. }
  8. class _TapboxAState extends State<TapboxA> {
  9. bool _active = false;
  10. void _handleTap() {
  11. setState(() {
  12. _active = !_active;
  13. });
  14. }
  15. Widget build(BuildContext context) {
  16. return new GestureDetector(
  17. onTap: _handleTap,
  18. child: new Container(
  19. child: new Center(
  20. child: new Text(
  21. _active ? 'Active' : 'Inactive',
  22. style: new TextStyle(fontSize: 32.0, color: Colors.white),
  23. ),
  24. ),
  25. width: 200.0,
  26. height: 200.0,
  27. decoration: new BoxDecoration(
  28. color: _active ? Colors.lightGreen[700] : Colors.grey[600],
  29. ),
  30. ),
  31. );
  32. }
  33. }

父widget管理子widget的state

对于父widget来说,管理状态并告诉其子widget何时更新通常是比较好的方式。 例如,IconButton是一个图片按钮,但它是一个无状态的widget,因为我们认为父widget需要知道该按钮是否被点击来采取相应的处理。

在以下示例中,TapboxB通过回调将其状态导出到其父项。由于TapboxB不管理任何状态,因此它的父类为StatelessWidget。

ParentWidgetState 类:

  • 为TapboxB 管理_active状态.
  • 实现_handleTapboxChanged(),当盒子被点击时调用的方法.
  • 当状态改变时,调用setState()更新UI.

TapboxB 类:

  • 继承StatelessWidget类,因为所有状态都由其父widget处理。
  • 当检测到点击时,它会通知父widget。
  1. // ParentWidget 为 TapboxB 管理状态.
  2. //------------------------ ParentWidget --------------------------------
  3. class ParentWidget extends StatefulWidget {
  4. @override
  5. _ParentWidgetState createState() => new _ParentWidgetState();
  6. }
  7. class _ParentWidgetState extends State<ParentWidget> {
  8. bool _active = false;
  9. void _handleTapboxChanged(bool newValue) {
  10. setState(() {
  11. _active = newValue;
  12. });
  13. }
  14. @override
  15. Widget build(BuildContext context) {
  16. return new Container(
  17. child: new TapboxB(
  18. active: _active,
  19. onChanged: _handleTapboxChanged,
  20. ),
  21. );
  22. }
  23. }
  24. //------------------------- TapboxB ----------------------------------
  25. class TapboxB extends StatelessWidget {
  26. TapboxB({Key key, this.active: false, @required this.onChanged})
  27. : super(key: key);
  28. final bool active;
  29. final ValueChanged<bool> onChanged;
  30. void _handleTap() {
  31. onChanged(!active);
  32. }
  33. Widget build(BuildContext context) {
  34. return new GestureDetector(
  35. onTap: _handleTap,
  36. child: new Container(
  37. child: new Center(
  38. child: new Text(
  39. active ? 'Active' : 'Inactive',
  40. style: new TextStyle(fontSize: 32.0, color: Colors.white),
  41. ),
  42. ),
  43. width: 200.0,
  44. height: 200.0,
  45. decoration: new BoxDecoration(
  46. color: active ? Colors.lightGreen[700] : Colors.grey[600],
  47. ),
  48. ),
  49. );
  50. }
  51. }

混合管理

对于一些widget来说,混和管理的方式非常有用。在这种情况下,widget自身管理一些内部状态,而父widget管理一些其他外部状态。

在下面TapboxC示例中,点击时,盒子的周围会出现一个深绿色的边框。点击时,边框消失,盒子的颜色改变。 TapboxC将其_active状态导出到其父widget中,但在内部管理其_highlight状态。这个例子有两个状态对象_ParentWidgetState_TapboxCState

_ParentWidgetStateC 对象:

  • 管理_active 状态。
  • 实现 _handleTapboxChanged() ,当盒子被点击时调用。
  • 当点击盒子并且_active状态改变时调用setState()更新UI。

_TapboxCState 对象:

  • 管理_highlight state。
  • GestureDetector监听所有tap事件。当用户点下时,它添加高亮(深绿色边框);当用户释放时,会移除高亮。
  • 当按下、抬起、或者取消点击时更新_highlight状态,调用setState()更新UI。
  • 当点击时,将状态的改变传递给父widget.
  1. //---------------------------- ParentWidget ----------------------------
  2. class ParentWidgetC extends StatefulWidget {
  3. @override
  4. _ParentWidgetCState createState() => new _ParentWidgetCState();
  5. }
  6. class _ParentWidgetCState extends State<ParentWidgetC> {
  7. bool _active = false;
  8. void _handleTapboxChanged(bool newValue) {
  9. setState(() {
  10. _active = newValue;
  11. });
  12. }
  13. @override
  14. Widget build(BuildContext context) {
  15. return new Container(
  16. child: new TapboxC(
  17. active: _active,
  18. onChanged: _handleTapboxChanged,
  19. ),
  20. );
  21. }
  22. }
  23. //----------------------------- TapboxC ------------------------------
  24. class TapboxC extends StatefulWidget {
  25. TapboxC({Key key, this.active: false, @required this.onChanged})
  26. : super(key: key);
  27. final bool active;
  28. final ValueChanged<bool> onChanged;
  29. _TapboxCState createState() => new _TapboxCState();
  30. }
  31. class _TapboxCState extends State<TapboxC> {
  32. bool _highlight = false;
  33. void _handleTapDown(TapDownDetails details) {
  34. setState(() {
  35. _highlight = true;
  36. });
  37. }
  38. void _handleTapUp(TapUpDetails details) {
  39. setState(() {
  40. _highlight = false;
  41. });
  42. }
  43. void _handleTapCancel() {
  44. setState(() {
  45. _highlight = false;
  46. });
  47. }
  48. void _handleTap() {
  49. widget.onChanged(!widget.active);
  50. }
  51. Widget build(BuildContext context) {
  52. // 在按下时添加绿色边框,当抬起时,取消高亮
  53. return new GestureDetector(
  54. onTapDown: _handleTapDown, // 处理按下事件
  55. onTapUp: _handleTapUp, // 处理抬起事件
  56. onTap: _handleTap,
  57. onTapCancel: _handleTapCancel,
  58. child: new Container(
  59. child: new Center(
  60. child: new Text(widget.active ? 'Active' : 'Inactive',
  61. style: new TextStyle(fontSize: 32.0, color: Colors.white)),
  62. ),
  63. width: 200.0,
  64. height: 200.0,
  65. decoration: new BoxDecoration(
  66. color: widget.active ? Colors.lightGreen[700] : Colors.grey[600],
  67. border: _highlight
  68. ? new Border.all(
  69. color: Colors.teal[700],
  70. width: 10.0,
  71. )
  72. : null,
  73. ),
  74. ),
  75. );
  76. }
  77. }

另一种实现可能会将高亮状态导出到父widget,同时保持_active状态为内部,但如果你要将该TapBox给其它人使用,可能没有什么意义。 开发人员只会关心该框是否处于Active状态,而不在乎高亮显示是如何管理的,所以应该让TapBox内部处理这些细节。

全局状态管理

当应用中包括一些跨widget(甚至跨路由)的状态需要同步时,上面介绍的方法很难胜任了。比如,我们有一个设置页,里面可以设置应用语言,但是我们为了让设置实时生效,我们期望在语言状态发生改变时,我们的APP Widget能够重新build一下,但我们的APP Widget和设置页并不在一起。这是正确的做法是通过一个全局状态管理器来处理这种“相距较远”的widget之间的通信。目前主要有两种办法:

  1. 实现一个全局的事件总线,将语言状态改变对应为一个事件,然后再APP Widget所在的父widgetinitState 方法中订阅语言改变的事件,当用户在设置页切换语言后,我们触发语言改变事件,然后APP Widget那边就会收到通知,然后重新build一下即可。
  2. 使用redux这样的全局状态包,读者可以在pub上查看其详细信息。

本书后面事件处理一章中会实现一个全局事件总线。

Flutter widget库介绍

Flutter提供了一套丰富、强大的基础widget,在基础widget库之上Flutter又提供了一套Material风格(Android默认的视觉风格)和一套Cupertino风格(iOS视觉风格)的widget库。要使用基础widget库,需要先导入:

  1. import 'package:flutter/widgets.dart';

下面我们介绍换一下常用的widget。

基础widget

  • Text:该 widget 可让创建一个带格式的文本。
  • RowColumn: 这些具有弹性空间的布局类Widget可让您在水平(Row)和垂直(Column)方向上创建灵活的布局。其设计是基于web开发中的Flexbox布局模型。
  • Stack: 取代线性布局 (译者语:和Android中的LinearLayout相似),Stack允许子 widget 堆叠, 你可以使用 Positioned 来定位他们相对于Stack的上下左右四条边的位置。Stacks是基于Web开发中的绝度定位(absolute positioning )布局模型设计的。
  • ContainerContainer 可让您创建矩形视觉元素。container 可以装饰为一个BoxDecoration, 如 background、一个边框、或者一个阴影。 Container 也可以具有边距(margins)、填充(padding)和应用于其大小的约束(constraints)。另外, Container可以使用矩阵在三维空间中对其进行变换。

Material widget

Flutter提供了一套丰富的Material widget,可帮助您构建遵循Material Design的应用程序。Material应用程序以MaterialApp widget开始, 该widget在应用程序的根部创建了一些有用的widget,比如一个Theme,它配置了应用的主题。 是否使用MaterialApp完全是可选的,但是使用它是一个很好的做法。在之前的示例中,我们已经使用过多个Material widget了,如:ScaffoldAppBarFlatButton等。要使用Material widget,需要先引入它:

  1. import 'package:flutter/material.dart';

Cupertino widget

Flutter也提供了一套丰富的Cupertino风格的widget,尽管目前还没有Material widget那么丰富,但也在不断的完善中。值得一提的是在Material widget库中,有一些widget可以根据实际运行平台来切换表现风格,比如MaterialPageRoute,在路由切换时,如果是Android系统,它将会使用Android系统默认的页面切换动画(从底向上),如果是iOS系统时,它会使用iOS系统默认的页面切换动画(从右向左)。由于在前面的示例中还没有Cupertino widget的示例,我们实现一个简单的Cupertino页面:

  1. //导入cupertino widget库
  2. import 'package:flutter/cupertino.dart';
  3. class CupertinoTestRoute extends StatelessWidget {
  4. @override
  5. Widget build(BuildContext context) {
  6. return CupertinoPageScaffold(
  7. navigationBar: CupertinoNavigationBar(
  8. middle: Text("Cupertino Demo"),
  9. ),
  10. child: Center(
  11. child: CupertinoButton(
  12. color: CupertinoColors.activeBlue,
  13. child: Text("Press"),
  14. onPressed: () {}
  15. ),
  16. ),
  17. );
  18. }
  19. }

下面是在iPhoneX上页面效果截图:

image-20180824181958838

总结

Flutter提供了丰富的widget,在实际的开发中你可以随意使用它们,不要怕引入过多widget库会让你的应用安装包变大,这不是web开发,dart在编译时只会编译你使用了的代码。由于Material和Cupertino都是在基础widget库之上的,所以如果你的应用中引入了这两者之一,则不需要再引入flutter/widgets.dart了,因为它们内部已经引入过了。