美文网首页
Flutter基础 - 深入理解Widget

Flutter基础 - 深入理解Widget

作者: 巧巧的二表哥 | 来源:发表于2019-06-25 17:03 被阅读0次

    1.前言

    本文涵盖了WidgetStateBuildContextInheritedWidget等术语的相关概念,并着力解答以下几个问题:

    2.Widget

    在Flutter中,几乎任何事物都是Widget

    可以把Widget想象成一种可视化组件,或者应用中可以与可视化组件进行交互的模块。

    Widget树

    Flutter中所有的WIdget都以树状结构呈现。
    一个包含其他Widget的Widget被称为父Widget或者Widget容器,被包含在父Widget下的Widget被称为子Widget
    下面我们来分析示例代码中的Widget树结构:

    code - 1.dart

    其树状结构如下:

    widget tree

    BuildContext

    BuildContext是Widget树结构中每个Widget的上下文环境,每个BuildContext都只属于一个Widget。
    如果Widget A有多个子Widget,则Widget A的BuildContext是其子Widget的BuildContext的父context。
    为了便于理解每个context的作用范围,我们将前文中的Widget树状图中的BuildContext用颜色进行标注,效果如下:

    widget tree with context

    从BuildContext的继承关系中,我们可以很容易找到Widget的父级(祖先)Widget。例如,在上图Scaffold > Center > Column > Text这一结构中,
    通过代码context.ancestorWidgetOfExactType(Scaffold)就可以获取到当前context下的第一个Scaffold。
    同理,也可以通过父context的关系找到对应的子Widget,但是并不推荐这么使用,我们将在后文解释原因。

    StatelessWidget

    StatelessWidget一旦创建就无法进行修改,这意味着它不会因为外部条件变化而重新绘制。
    一个典型的StatelessWidget示例如下:

    my_stateless_widget.dart

    如代码所见,StatelessWidget的生命周期非常简单明了:

    1. 初始化;
    2. 通过build()方法进行渲染。

    StatefulWidget

    与StatelessWidget相对应的另一种Widget,它可以在其生命周期中操作内部持有数据的变化,这些数据被称为State,这样的Widget也叫做StatefulWidget。
    典型的StatefulWidget有CheckboxRadioSwitch等相关组件,其State发生的变化将直接体现到UI上进行更新。
    简单的StatefulWidget示例:

    my_stateful_widget.dart

    我们将会在State部分详细讲解StatefulWidget的结构与生命周期。

    3.State

    State作为StatefulWidget内部数据,它的作用主要在于两点:

    1. 定义Widget的交互行为;
    2. 调整Widget的布局显示。

    任何State一旦发生调整都会使StatefulWidget进行rebuild操作。

    BuildContext与State关系

    在StatefulWidget中,State与BuildContext唯一相关,State不能修改所属的BuildContext,而且当Widget在树结构中发生位置变化时(该操作也会导致BuildContext的变化),这样的关系依然保持。
    可以这样认为,一旦State与BuildContext建立了关联,这种关系将一直固定存在,意味着我们不能直接通过其他BuildContext获取到当前context下的state

    StatefulWidget生命周期

    正如前文示例代码所示,State作为StatefulWidget的主体,它可以在多个节点(@override所标记的重写方法)对State进行调整。
    当然,还有didUpdateWidget()deactivate()reassemble()等重写方法并不在本文范畴中。
    在下面的时序图中我们将完整地了解StatefulWidget各个方法的调用顺序(已省略部分方法),以及State与BuildContext的关联时机,State的生效时机等:

    lifecycle of stateful widget

    initState()
    initState()是构造方法执行之后第一个调用的方法,它的执行完成标志着state对象初始化完毕,并且在生命周期中只被调用一次。
    该方法重写主要完成一些额外的初始化工作,例如animation和controller的相关初始化等,重写时需要调用super.initState()来完成父类的初始化。
    在该方法中,context对象可以访问但是并不能拿来使用,因为此时state与context并没有建立关联。

    didChangeDependencies()
    didChangeDependencies()是第二个调用的方法,在这一步中context可以直接使用。
    该方法一般在Widget自身和InheritedWidget相关联时或者需要创建基于context的监听时需要重写,且重写时需要调用super.didChangeDependencies()

    注意,如果Widget和InheritedWidget进行了关联,则Widget每一次进行rebuild操作时该方法都会重复调用。

    build()
    build()方法在didChangeDependencies()didUpdateWidget()之后执行,是构建Widget及其树形结构的位置。
    该方法会在state对象发生改变或者InheritedWidget向其注册的Widget发起通知时进行调用,我们可以在setState((){...})方法的闭包中强制Widget进行rebuild(重绘)操作。

    dispose()
    dispose()方法会在Widget销毁时调用。该方法进行一些清理操作(例如,listener,controller等),注意在方法最后调用super.dispose()

    如何选择StatefulWidget与StatelessWidget

    回答这个问题之前,我们先不妨问问自己:在Widget是生命周期中,是否需要一个变量来改变Widget,并且考虑如何对Widget进行rebuild操作。
    如果我们回答yes,那就需要StatefulWidget,否则,就应该选择StatelessWidget。
    举个两个栗子:

    1. 试想需要创建一个包含CheckBox的列表,列表中的每一项都包含了标题和CheckBox的状态。当点击列表中的每一项时,CheckBox的状态也随之切换。在这种场景下,需要使用StatefulWidget来记录每一项的状态,以及通过它才能对CheckBox进行重绘。
    2. 在界面中有一个Form表单,表单允许用户输入数据并发送到服务器。如果不需要在提交前做一些数据验证或者其他操作,StatelessWidget足够可用。

    StatefulWidget结构

    正如前文代码段所示,StatefulWidget包含两个部分:

    1. 定义Widget部分;
    2. 定义State部分。

    定义Widget部分
    该部分属于StatefulWidget的public部分(文件外部可以通过import访问到),在这里可以对Widget做一些初始化自定义,以及通过重写createState()方法与私有的State对象进行关联。

    注意,任何需要调整的变量都不应该定义在这里,因为在整个Widget生命周期中这里的变量都不会被改变,示例代码中parameter前的final关键字也说明了这点。

    定义State部分
    该部分属于StatefulWidget的private部分(dart语言中以下划线开头声明的类名,方法名,变量名等都属于作用域范围下的私有声明),这里定义的变量在Widget生命周期中可以调整,并且这些调整可以应用到Widget的重绘上。
    同时,_MyStatefulWidgetState内部可以通过widget.{变量名}访问到与之关联的Widget中的变量,例如widget.parameter。

    Widget唯一标识 - Key

    在Flutter中,每个Widget都有唯一标识(Key),该标识由系统框架创建,并且传递给Widget构造方法中的可选参数*key*
    如果不显式地传入key,系统会自动创建一个,在一些特殊情况下,必须传入key值,例如需要通过key来直接获取对应Widget的时候。
    Flutter框架提供了下列key方案:

    GlobalKey
    该key保证整个App内部都是唯一的,但是创建GlobalKey的代价非常昂贵,如果不需要保证整个App内部唯一性,可以考虑使用LocalKey。

    LocalKey
    与GlobalKey相对应的一种局部key,需要保证创建LocalKey的Widget都有同一个父Widget,不然就失去其作用。

    UniqueKey
    UniqueKey必须保证关联的Widget只有一个child,属于LocalKey。

    ObjectKey
    对象级别的key,通过Widget的实例对象来创建,与之类似的还有ValueKey,它使用了类型来作为key值,均属于LocalKey。

    访问State

    正如前文所述,State与BuildContext相关联,而BuildContext又与Widget相关联。

    Widget自身
    在理论上是唯一能够访问state的对象,而且state也可以直接访问Widget的变/常量。

    子Widget
    某些时候父类Widget可能会需要访问子类Widget的state中的值来完成一些特殊操作。
    为了满足这一需求,最简单的就是通过key来获取。针对以下代码,我们可以使用myWidgetStateKey.currentState来获取state值:

    ...
       GlobalKey<MyStatefulWidgetState> myWidgetStateKey = GlobalKey<MyStatefulWidgetState>();
        ...
       @override
       Widget build(BuildContext context){
          return MyStatefulWidget(
             key: myWidgetStateKey,
             color: Colors.blue,
          );
       }
    

    注意,MyStatefulWidgetState类名没有下划线前缀,因为我们需要将其暴露出来才可以访问到。

    父Widget
    试想有以下Widget的树形结构,底层的子Widget想访问根节点Widget的state:

    complex sample of widget tree

    想要达成这一目标需要满足3个条件:

    • 根节点Widget需要暴露state变量,不再将state声明为私有类型;
    class MyExposingWidget extends StatefulWidget {
    
       MyExposingWidgetState myState;
        @override
       MyExposingWidgetState createState(){
          myState = MyExposingWidgetState();
          return myState;
       }
    }
    
    • State必须为其中的值创建getter/setter或者声明值为public(不推荐);
    class MyExposingWidgetState extends State<MyExposingWidget>{
        Color _color;
    
        Color get color => _color;
       ...
    }
    
    • 底层Widget获取到state的引用。
    class MyChildWidget extends StatelessWidget {
    
    
       @override
       Widget build(BuildContext context){
          final MyExposingWidget widget = context.ancestorWidgetOfExactType(MyExposingWidget);
          final MyExposingWidgetState state = widget?.myState;
    
          return Container(
             color: state == null ? Colors.blue : state.color,
          );
       }
    }
    

    虽然上述方案可以解决在任何地方访问state的问题,但并不能感知state内部的值何时进行修改,随之而来的Widget何时进行重绘等问题,InheritedWidget就能帮助我们解决这一问题。

    4.InheritedWidget

    简而言之,InheritedWidget可以帮助我们在Widget树形结构中高效地传递数据信息。作为一种特殊的Widget,它可以使Widget树中所有的Widget都能够共享数据。

    基本概念

    为了更加清楚的解释相关概念,我们以下面代码进行说明:

    class MyInheritedWidget extends InheritedWidget {
        MyInheritedWidget({
          Key key,
          @required Widget child,
          this.data,
       }): super(key: key, child: child);
    
       final data;
    
       static MyInheritedWidget of(BuildContext context) {
          return context.inheritFromWidgetOfExactType(MyInheritedWidget);
       }
    
       @override
       bool updateShouldNotify(MyInheritedWidget oldWidget) => data != oldWidget.data;
    }
    

    代码中定义了名为MyInheritedWidget的Widget,它的目的即在其Widget子树中共享其data变量。为了实现这一目的,我们还需要为它传入子Widget作为构造方法的参数,才使得其子树间的共享数据成为可能。换个更简单的说法,如果想要某个Widget的子节点能共享数据,请使用InheritedWidget来"包裹"它。
    再来看看静态方法static MyInheritedWidget of(BuildContext context),则实现了从BuildContext中获取具体类型Widget的功能。
    最后,重写updateShouldNotify方法来告知InheritedWidget的子Widget(订阅/注册过数据的修改通知)是否需要接收更新。
    使用InheritedWidget时,只需编写类似如下代码:

    class MyParentWidget... {
           ...
          @override
          Widget build(BuildContext context){
             return MyInheritedWidget(
                data: counter,
                child: Row(
                children: <Widget>[
                   ...
                ],
             ),
          );
       }
    }
    

    子Widget访问数据

    子Widget可以通过获得InheritedWidget引用来访问数据:

    class MyChildWidget... {
        ...
    
       @override
       Widget build(BuildContext context){
          final MyInheritedWidget inheritedWidget = MyInheritedWidget.of(context);
    
          ///
          /// From this moment, the widget can use the data, exposed by the MyInheritedWidget
          /// by calling: inheritedWidget.data
          ///
          return Container(
             color: inheritedWidget.data.color,
          );
       }
    }
    

    Widget交互

    试想有如下WIdget树结构:

    cart sample of widget tree

    为了举例说明图中结构,我们假设以下场景:

    • Widget A是将商品添加到购物车的按钮;
    • Widget B是展示购物车中商品数量的文本;
    • Widget C是WIdget B同级的其他文本;
    • 我们希望按下按钮(Widget A)时 Widget B能够准确显示购物车中的商品数量,而Widget C并不会发生任何重绘。

    为了模拟这一需求,我们编写以下代码:

    class Item {
       String reference;
    
       Item(this.reference);
    }
    
    class _MyInherited extends InheritedWidget {
       _MyInherited({
          Key key,
          @required Widget child,
          @required this.data,
       }) : super(key: key, child: child);
    
       final MyInheritedWidgetState data;
    
       @override
       bool updateShouldNotify(_MyInherited oldWidget) {
          return true;
       }
    }
    
    class MyInheritedWidget extends StatefulWidget {
       MyInheritedWidget({
          Key key,
          this.child,
       }): super(key: key);
    
       final Widget child;
    
       @override
       MyInheritedWidgetState createState() => MyInheritedWidgetState();
    
       static MyInheritedWidgetState of(BuildContext context){
          return (context.inheritFromWidgetOfExactType(_MyInherited) as _MyInherited).data;
       }
    }
    
    class MyInheritedWidgetState extends State<MyInheritedWidget>{
       /// List of Items
       List<Item> _items = <Item>[];
    
       /// Getter (number of items)
       int get itemsCount => _items.length;
    
       /// Helper method to add an Item
       void addItem(String reference){
          setState((){
             _items.add( Item(reference));
          });
       }
    
       @override
       Widget build(BuildContext context){
          return _MyInherited(
             data: this,
             child: widget.child,
          );
       }
    }
    
    class MyTree extends StatefulWidget {
       @override
       _MyTreeState createState() => _MyTreeState();
    }
    
    class _MyTreeState extends State<MyTree> {
       @override
       Widget build(BuildContext context) {
          return MyInheritedWidget(
             child: Scaffold(
             appBar: AppBar(
             title: Text('Title'),
          ),
             body: Column(
             children: <Widget>[
                WidgetA(),
                Container(
                   child: Row(
                   children: <Widget>[
                            Icon(Icons.shopping_cart),
                            WidgetB(),
                            WidgetC(),
                               ],
                            ),
                        ),
                    ],
                 ),
             ),
          );
       }
    }
    
    class WidgetA extends StatelessWidget {
       @override
       Widget build(BuildContext context) {
          final MyInheritedWidgetState state = MyInheritedWidget.of(context);
          return Container(
             child: RaisedButton(
                child: Text('Add Item'),
                onPressed: () {
                   state.addItem(' item');
                },
             ),
          );
       }
    }
    
    class WidgetB extends StatelessWidget {
       @override
       Widget build(BuildContext context) {
          final MyInheritedWidgetState state = MyInheritedWidget.of(context);
          return Text('${state.itemsCount}');
       }
    }
    
    class WidgetC extends StatelessWidget {
       @override
       Widget build(BuildContext context) {
          return Text('I am Widget C');
       }
    }
    

    简要说明一下每个类的功能:

    • _MyInherited是一个InheritedWidget,在点击Widget A时会重复创建。
    • MyInheritedWidget是一个StatefulWidget,其state管理着一个商品数组,通过静态方法static MyInheritedWidgetState of(BuildContext context)来获取state对象。
    • MyInheritedWidgetState是管理商品数组的state类,同时创建一个itemsCount的getter方法以及addItem(String preference)方便外部调用。State中每加入一个商品,build(BuildContext context)方法会创建一个_MyInherited对象。
    • MyTree创建了结构图中的Widget树结构,并以MyInheritedWidget为根节点。
    • WidgetA是一个RaiseButton类型的Widget,点击之后调用state的addItem(String preference)方法以完成商品的添加操作。
    • WidgetB是一个简单的文本,用于展示购物车中的商品数量。

    那么,代码是如何实现Widget向State进行注册的呢?关键点就在于静态方法static MyInheritedWidgetState of(BuildContext context)的内部实现:

    static MyInheritedWidgetState of(BuildContext context){
       return (context.inheritFromWidgetOfExactType(_MyInherited) as _MyInherited).data;
    }
    

    当子Widget调用该方法时,会传递其BuildContext,并返回对应MyInheritedWidgetState类型的实例对象。如此一来,该方法完成了两个目的:

    • 作为数据消费者的Widget被添加到订阅者名单中,当InheritedWidget管理的state发生数据变更时,它会接收到通知以准备重绘操作;
    • 消费者Widget会同时获得数据管理者state的引用。

    数据流向

    由于WidgetA(RaiseButton)和WidgetB(文本)均通过InheritedWidget订阅更改,所以任何传递到_MyInherited更新的数据流向可以由以下(简化)流程表示:

    1. 点击按钮之后,调用MyInheritedWidgetStateaddItem方法;
    2. addItem方法添加一个新的Item到_items中;
    3. setState()闭包调用以准备MyInheritedWidget的rebuild;
    4. 执行build()方法后,包含data(_items)的_MyInherited对象被创建;
    5. _MyInherited通过构造方法记录新的state;
    6. _MyInherited设置updateShouldNotify回调为true以完成对订阅者的通知;
    7. _MyInherited遍历所有订阅者(包括WidgetAWidgetB),通知他们进行rebuild;
    8. WidgetC不是订阅者,因此不会rebuild。

    然而,这样一来,WidgetAWidgetB都会进行rebuild,但是WidgetA自身并不需要rebuild,那如何防止访问到InheritedWidget的部分Widget不rebuild呢?
    其实,之所以会出现这样的情况,原因在于context.inheritFromWidgetOfExactType()方法会自动将Widget作为订阅链表上的一员,要防止这种情况发生需修改为如下代码:

    static MyInheritedWidgetState of([BuildContext context, bool rebuild = true]) {
          return (rebuild ? context.inheritFromWidgetOfExactType(_MyInherited) as _MyInherited
                          : context.ancestorWidgetOfExactType(_MyInherited) as _MyInherited).data;
    }
    

    然后修改WidgetA如下:

    class WidgetA extends StatelessWidget {
          @override
          Widget build(BuildContext context) {
          final MyInheritedWidgetState state = MyInheritedWidget.of(context, false);
                return new Container(
                      child: new RaisedButton(
                            child: new Text('Add Item'),
                            onPressed: () {
                                  state.addItem('new item');
                            },
                      ),
                );
          }
    }
    

    相关文章

      网友评论

          本文标题:Flutter基础 - 深入理解Widget

          本文链接:https://www.haomeiwen.com/subject/abqxcctx.html