第三章 基础组件

本章目录

3.1 Widget简介

3.1.1 概念

Flutter中几乎所有的对象都是一个Widget。与原生开发中“控件”不同的是,Flutter中的Widget的概念更广泛,它不仅可以表示UI元素,也可以表示一些功能性的组件如:用于手势检测的 GestureDetector widget、用于APP主题数据传递的Theme等等,而原生开发中的控件通常只是指UI元素

Flutter主要就是用于构建用户界面的,所以,在大多数时候,读者可以认为widget就是一个控件,不必纠结于概念

3.1.2 Widget与Element

在Flutter中,Widget的功能是“描述一个UI元素的配置数据”,它就是说,Widget其实并不是表示最终绘制在设备屏幕上的显示元素,而它只是描述显示元素的一个配置数据。

实际上,Flutter中真正代表屏幕上显示元素的类是Element,也就是说Widget只是描述Element的配置数据!现在,读者只需要知道:Widget只是UI元素的一个配置数据,并且一个Widget可以对应多个Element

总结一下:

  • Widget实际上就是Element的配置数据,Widget树实际上是一个配置树,而真正的UI渲染树是由Element构成;不过,由于Element是通过Widget生成的,所以它们之间有对应关系,在大多数场景,我们可以宽泛地认为Widget树就是指UI控件树或UI渲染树。
  • 一个Widget对象可以对应多个Element对象。这很好理解,根据同一份配置(Widget),可以创建多个实例(Element)。

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

3.1.3 Widget主要接口

  • 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类来实现一个新组件,相反,我们通常会通过继承StatelessWidgetStatefulWidget来间接继承Widget类来实现。StatelessWidgetStatefulWidget都是直接继承自Widget类,而这两个类也正是Flutter中非常重要的两个抽象类,它们引入了两种Widget模型,接下来我们将重点介绍一下这两个类。

3.1.4 StatelessWidget

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

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

class Echo extends class Echo extends StatelessWidget {
  const Echo({
    Key key,  
    @required this.text,
    this.backgroundColor:Colors.grey,
  }):super(key:key);

  final String text;
  final Color backgroundColor;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        color: backgroundColor,
        child: Text(text),
      ),
    );
  }
}
//上面的代码,实现了一个回显字符串的Echo widget。

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

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

Context

build方法有一个context参数,它是BuildContext类的一个实例,表示当前widget在widget树中的上下文,每一个widget都会对应一个context对象(因为每一个widget都是widget树上的一个节点)。实际上,context是当前widget在widget树中位置中执行”相关操作“的一个句柄,比如它提供了从当前widget开始向上遍历widget树以及按照widget类型查找父级widget的方法。下面是在子树中获取父级widget的一个示例:

3.1.5 StatefulWidget

StatelessWidget不同的是createElement()方法返回的Element 对象不同;另外StatefulWidget类中添加了一个新的接口createState()

下面我们看看StatefulWidget的类定义:

abstract class StatefulWidget extends abstract class StatefulWidget extends Widget {
  const StatefulWidget({ Key key }) : super(key: key);

  @override
  StatefulElement createElement() => new StatefulElement(this);

  @protected
  State createState();
}
  • 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生命周期

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

class CounterWidget extends class CounterWidget extends StatefulWidget {
  const CounterWidget({
    Key key,
    this.initValue: 0
  });

  final int initValue;

  @override
  _CounterWidgetState createState() => new _CounterWidgetState();
}

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

class _CounterWidgetState extends State<class _CounterWidgetState extends State<CounterWidget> {  
  int _counter;

  @override
  void initState() {
    super.initState();
    //初始化状态  
    _counter=widget.initValue;
    print("initState");
  }

  @override
  Widget build(BuildContext context) {
    print("build");
    return Scaffold(
      body: Center(
        child: FlatButton(
          child: Text('$_counter'),
          //点击后计数器自增
          onPressed:()=>setState(()=> ++_counter,
          ),
        ),
      ),
    );
  }

  @override
  void didUpdateWidget(CounterWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    print("didUpdateWidget");
  }

  @override
  void deactivate() {
    super.deactivate();
    print("deactive");
  }

  @override
  void dispose() {
    super.dispose();
    print("dispose");
  }

  @override
  void reassemble() {
    super.reassemble();
    print("reassemble");
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    print("didChangeDependencies");
  }

}

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

Widget build(BuildContext context) {
  return CounterWidget();
}

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

I/flutter ( I/flutter ( 5436): initState
I/flutter ( 5436): didChangeDependencies
I/flutter ( 5436): build

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

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

I/flutter ( I/flutter ( 5436): reassemble
I/flutter ( 5436): didUpdateWidget
I/flutter ( 5436): build

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

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

Widget build(BuildContext context) {
  //移除计数器 
  //return CounterWidget();
  //随便返回一个Text()
  return Text("xxx");
}

然后热重载,日志如下:

I/flutter ( I/flutter ( 5436): reassemble
I/flutter ( 5436): deactive
I/flutter ( 5436): dispose

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

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

  • initState:当Widget第一次插入到Widget树时会被调用,对于每一个State对象,Flutter framework只会调用一次该回调,所以,通常在该回调中做一些一次性的操作,如状态初始化、订阅子树的事件通知等。
  • 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则会调用此回调。
  • deactivate():当State对象从树中被移除时,会调用此回调。在一些场景下,Flutter framework会将State对象重新插到树中,如包含此State对象的子树在树的一个位置移动到另一个位置时(可以通过GlobalKey来实现)。如果移除后没有重新插入到树中则紧接着会调用dispose()方法。
  • dispose():当State对象从树中被永久移除时调用;通常在此回调中释放资源。

为什么要将build方法放在State中,而不是放在StatefulWidget中?

3.4 按钮

3.4.1 Material组件库中的按钮

RaisedButton

RaisedButton 即”漂浮”按钮,它默认带有阴影和灰色背景。按下后,阴影会变大,如图3-10所示:

图3-10

FlatButton

FlatButton即扁平按钮,默认背景透明并不带阴影。按下后,会有背景色,如图3-11所示:

图3-11

OutlineButton

OutlineButton默认有一个边框,不带阴影且背景透明。按下后,边框颜色会变亮、同时出现背景和阴影(较弱),如图3-12所示:

图3-12

IconButton

IconButton是一个可点击的Icon,不包括文字,默认没有背景,点击后会出现背景,如图3-13所示:

图3-13

带图标的按钮

RaisedButtonFlatButtonOutlineButton都有一个icon 构造函数,通过它可以轻松创建带图标的按钮,如图3-14所示:

图3-14

3.4.2 自定义按钮外观

按钮外观可以通过其属性来定义,不同按钮属性大同小异,我们以FlatButton为例,介绍一下常见的按钮属性,详细的信息可以查看API文档。

const FlatButton({
  ...  
  @required this.onPressed, //按钮点击回调
  this.textColor, //按钮文字颜色
  this.disabledTextColor, //按钮禁用时的文字颜色
  this.color, //按钮背景颜色
  this.disabledColor,//按钮禁用时的背景颜色
  this.highlightColor, //按钮按下时的背景颜色
  this.splashColor, //点击时,水波动画中水波的颜色
  this.colorBrightness,//按钮主题,默认是浅色主题 
  this.padding, //按钮的填充
  this.shape, //外形
  @required this.child, //按钮的内容
})

其中大多数属性名都是自解释的,我们不赘述。下面我们通过一个示例来看看如何自定义按钮。

示例

定义一个背景蓝色,两边圆角的按钮。效果如图3-15所示:

图3-15

代码如下:

FlatButton(
  color: Colors.blue,
  highlightColor: Colors.blue[700],
  colorBrightness: Brightness.dark,
  splashColor: Colors.grey,
  child: Text("Submit"),
  shape:RoundedRectangleBorder(borderRadius: BorderRadius.circular(20.0)),
  onPressed: () {},
)

3.5 图片及ICON

3.5.1 图片

Flutter中,我们可以通过Image组件来加载并显示图片,Image的数据源可以是asset、文件、内存以及网络。

ImageProvider

ImageProvider 是一个抽象类,主要定义了图片数据获取的接口load(),从不同的数据源获取图片需要实现不同的ImageProvider ,如AssetImage是实现了从Asset中加载图片的ImageProvider,而NetworkImage实现了从网络加载图片的ImageProvider。

Image

Image widget有一个必选的image参数,它对应一个ImageProvider。下面我们分别演示一下如何从asset和网络加载图片。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!