Flutter中Widget 的State、Context 、 InheritedWidget

本文介绍了Flutter应用程序中Widget,State,Context和InheritedWidget的重要概念。特别注意InheritedWidget,它是最重要且记录较少的Widget之一。

难度:初学者

前言

在Flutter中Widget, State和Context是一个非常重要的概念,每一个开发人员需要充分了解。
但是,文档量巨大,并不能总是清楚地解释这个概念。

我会用自己知识来解释这些概念,本文的真正目的是试图澄清以下内容:

  • Stateful和Stateless Widget的区别
  • 什么是上下文Context
  • 什么是state以及如何使用它
  • context与state之间的关系
  • InheritedWidget以及在 Widgets tree中传播信息的方式
  • rebuild的概念

本文也适用于Medium-Flutter社区

第1部分:概念

Widget的概念

Flutter中,几乎所有东西都是Widget

Widget视为可视组件(或与应用程序的可视方面交互的组件)。

当您需要构建与布局直接或间接相关的任何内容时,您正在使用** Widget**。

Widget Tree的概念

** Widgets**以树结构组织。

包含其他Widgets小部件称为Parent Widget(或Widget container)。包含在Parent WidgetWidgets部件称为Children Widgets

让我们用Flutter自动生成的基础应用程序来说明这一点。这是简化的代码,仅限于build方法:

@override
Widget build(BuildContext){
    return new Scaffold(
      appBar: new AppBar(
        title: new Text(widget.title),
      ),
      body: new Center(
        child: new Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            new Text(
              'You have pushed the button this many times:',
            ),
            new Text(
              '$_counter',
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
      floatingActionButton: new FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: new Icon(Icons.add),
      ),
    );
}

如果我们现在考虑这个基本示例,我们将获得以下Widgets树结构(限制代码中存在的Widgets列表):

state_basic_tree.png

语境概念

另一个重要的概念是Context

*context *只是一个Widget在所有构建的Widget的树结构中的位置的引用。。

简而言之,将context视为Widgets树的一部分,其中Widget附加到此树。

ContextWidget一一对应。

如果一个Widget A有子Widget,Widget A的context将成为一级子Widget Context的Parent Context

读到这一点,很明显,Context是链接的,并且正在组成一个Context树(父子关系)。

如果我们现在尝试在上图中说明Context的概念,我们获得(仍然是一个非常简化的视图)每种颜色代表一个Context除了MyApp,它是不同的):

state_basic_context_tree.png

Context可见性简而言之):

某些Widget只能在其自己的Context中或在其父Context中可见。

由子Context中我们很容易找到一个ancestor(= parent)Widget。

一个例子是,考虑Scaffold> Center> Column> Text:context.ancestorWidgetOfExactType(Scaffold)=>通过从Text Context转到树结构来返回第一个Scaffold。

从父Context中,也可以找到后代(=子)Widget,但不建议这样做(我们稍后会讨论)。

Widget的类型

Widget有两种类型:

Stateless Widget

这些可视组件中的一些除了它们自己的配置信息之外不依赖于任何其他信息,该信息其直接父级构建时提供。

换句话说,这些Widget一旦创建就不必关心任何变体

这些小部件称为Stateless Widget

这种小部件的典型示例可以是Text,Row,Column,Container ......其中,在构建时,我们只是将一些参数传递给它们。

参数可以是装饰(decoration),尺寸(dimensions)甚至其他Widget中的任何内容。不要紧。唯一重要的是这个配置一旦应用,在下一个构建过程之前不会改变。

无状态Widget只能在加载/构建(loaded/build)时才绘制一次,这意味着无法基于任何事件或用户操作重绘该Widget。

Stateless Widget生命周期

这是与Stateless Widget相关的代码的典型结构。

如您所见,我们可以将一些额外的参数传递给它的构造函数。但是,请记住,这些参数将改变在以后阶段。

class MyStatelessWidget extends StatelessWidget {

    MyStatelessWidget({
        Key key,
        this.parameter,
    }): super(key:key);

    final parameter;

    @override
    Widget build(BuildContext context){
        return new ...
    }
}

即使有另一种方法可以被覆盖(createElement),后者也几乎不会被覆盖。唯一需要被覆盖的是构建(build)

这种Stateless Widget的生命周期很简单:

  • 初始化
  • 通过build()渲染

Stateful Widget

其他一些小部件将处理一些在Widget生命周期会发生变化的内部数据。因此,该数据变得动态

此Widget保存的数据集在此Widget的生命周期中可能会有所不同,称为State

这些窗口小部件称为Stateful Widget

此类Widget的示例可以是用户可以选择的复选框列表,也可以是根据条件禁用的Button。

State概念

一个** State**界定的“ 行为一部份” StatefulWidget实例。

它包含旨在与Widget 交互/干扰的信息:

  • 行为
  • 布局

应用于State的任何更改都会强制Widget 重建

State与Context之间的关系

对于Stateful WidgetStateContext相关联。此关联是永久性的State对象永远不会更改其Context

即使可以在树结构周围移动Widget Context,State仍将与该Context相关联。

StateContext关联时State被视为已挂载(mounted)

超重要点

Context相关联的State对象,State对象不能(直接)通过另一个Context来访问!(我们将在稍后讨论这个问题)。


Stateful Widget生命周期

既然已经引入了基本概念,那么现在是时候深入了解......

这是与Stateful Widget相关的典型代码结构。

由于本文的主要目的是用“变量(variable)”数据来解释State的概念,我将故意跳过与某些Stateful Widget overridable方法相关的任何解释,这些方法与此没有特别的关系。这些可* overridable方法是didUpdateWidget,deactivate,reassemble*。这些将在下一篇文章中讨论。

class MyStatefulWidget extends StatefulWidget {

    MyStatefulWidget({
        Key key,
        this.parameter,
    }): super(key: key);

    final parameter;

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

class _MyStatefulWidgetState extends State<MyStatefulWidget> {

    @override
    void initState(){
        super.initState();

        // Additional initialization of the State
    }

    @override
    void didChangeDependencies(){
        super.didChangeDependencies();

        // Additional code
    }

    @override
    void dispose(){
        // Additional disposal code

        super.dispose();
    }

    @override
    Widget build(BuildContext context){
        return new ...
    }
}

下图显示了与创建Stateful Widget相关的操作/调用序列(简化版本)。在图的右侧,您将注意到流中的State对象的内部状态。您还将看到Context与State关联的时刻,从而变为可用(mounted)。

state_diagram.png

所以让我们用一些额外的细节来解释它:

initState()

initState()方法是第一个方法(构造函数后面),一旦State对象被创建被调用。需要执行其他初始化时,将覆盖此方法。典型的初始化与动画,控制器有关......如果重写此方法,则需要首先调用super.initState()方法。

在这个方法中,Context可用但你还不能真正使用它,因为框架还没有完全将状态与它相关联。

一旦initState()方法完成,State对象现在被初始化并且Context可用。

在此State对象的生命周期内不再调用此方法。

didChangeDependencies()

所述didChangeDependencies()方法是将被调用的第二方法。

在此阶段,由于Context可用,您可以使用它。

如果您的Widget链接到InheritedWidget和/或您需要初始化某些监听(listeners)(基于Context),则通常会覆盖此方法。

请注意,如果您的窗口小部件链接到InheritedWidget,则每次重建此Widget时都会调用此方法。

如果重写此方法,则应首先调用super.didChangeDependencies()

build()

build(BuildContext context)方法在didChangeDependencies() (和didUpdateWidget)之后调用。

这是您构建Widget(可能还有任何子树)的地方。

每次State对象更改时(或者当InheritedWidget需要通知“ 已注册 ”的Widget时)都会调用此方法!

为了强制重建,您可以调用*setState((){…}) *方法。

dispose()

dispose()方法在Widget被销毁时调用。

如果需要执行一些清理(例如监听器),则重写此方法,然后立即调用super.dispose()

Stateless or Stateful Widget?

这是许多开发人员需要问自己的问题:我是否需要我的Widget无状态或有状态?

为了回答这个问题,请问问自己:

在我的Widget的生命周期中,我是否需要考虑一个将要更改的变量,何时更改,将强制** rebuilt**Widget?

如果问题的答案是肯定的,那么您需要一个有状态Widget,否则,您需要一个无状态Widget。

一些例子:

  • 用于显示复选框列表的Widget。要显示复选框,您需要考虑一系列项目。每个项目都是一个具有标题和状态的对象。如果单击复选框,则切换相应的item.status;

    在这种情况下,您需要使用StatefulWidget来记住项目的状态,以便能够重绘复选框。

  • 带有表格的屏幕。该屏幕允许用户填写表单的Widget并将表单发送到服务器。

    在这种情况下,除非你需要验证表或提交之前,做任何其他动作,一个StatelessWidget可能就足够了。


Stateful Widget由2部分组成

还记得Stateful小部件的结构吗?有两个部分:

Widget的主要定义

class MyStatefulWidget extends StatefulWidget {
    MyStatefulWidget({
        Key key,
        this.color,
    }): super(key: key);

    final Color color;

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

第一部分“ MyStatefulWidget通常是Widget 的公共部分。当您要将其添加到窗口小部件树时,可以实例化此部件。此部分在Widget的生命周期内不会发生变化,但可能接受可能由其相应的State实例使用的参数。

请注意,在Widget的第一部分级别定义的任何变量通常 不会在其生命周期内发生变化。

Widget State定义

class _MyStatefulWidgetState extends State<MyStatefulWidget> {
    ...
    @override
    Widget build(BuildContext context){
        ...
    }
}

第二部分“ _MyStatefulWidgetState ”是在Widget的生命周期中变化的部分,并强制每次应用修改时重建Widget的这个特定实例。名称开头的“ _ ”字符使该类对.dart文件是私有的。

如果需要在.dart文件之外引用此类,请不要使用“ _ ”前缀。

所述_MyStatefulWidgetState类可以访问被存储在任何可变MyStatefulWidget,使用Widget。{变量名称}。在此示例中:widget.color


Widget唯一标识Key

在Flutter中,每个Widget都是唯一标识的。这个唯一标识由构建/渲染时的框架定义。

此唯一标识对应于可选的Key参数。如果省略,Flutter将为您生成一个。

在某些情况下,您可能需要强制使用此Key,以便可以通过其key访问Widget。

为此,您可以使用以下帮助程序之一:GlobalKeyLocalKeyUniqueKeyObjectKey

GlobalKey确保关键是在整个应用程序唯一的。

强制使用Widget的唯一标识:

    GlobalKey myKey = new GlobalKey();
    ...
    @override
    Widget build(BuildContext context){
        return new MyWidget(
            key: myKey
        );
    }

第2部分:如何访问State?

如前所述,State链接到一个ContextContext链接到Widget的一个实例对象

1. Widget本身

从理论上讲,唯一能够访问StateWidget State本身

在这种情况下,没有困难。Widget State类访问其任何变量。

2.一个直接的子Widget

有时,父Widget可能需要访问其直接子节点的State才能执行特定任务。

在这种情况下,要访问这些直接的子State,您需要了解它们。

给某人打电话的最简单方法是通过名字。在Flutter中,每个Widget都有一个唯一的标识,它由框架在构建/渲染时确定。如前所示,您可以使用key参数强制使用Widget的标识。

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

一旦确定StateKey, Widget可以通过以下方式访问其子级的State

myWidgetStateKey.currentState

让我们考虑一个基本示例,当用户点击按钮时显示SnackBar。由于SnackBar是Scaffold的子Widget,它不能直接访问Scaffold身体的任何其他孩子(请记住Context的概念及其层次结构/树结构?)。因此,访问它的唯一方法是通过ScaffoldState,它公开一个公共方法来显示SnackBar。

    class _MyScreenState extends State<MyScreen> {
        /// the unique identity of the Scaffold
        final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();

        @override
        Widget build(BuildContext context){
            return new Scaffold(
                key: _scaffoldKey,
                appBar: new AppBar(
                    title: new Text('My Screen'),
                ),
                body: new Center(
                    new RaiseButton(
                        child: new Text('Hit me'),
                        onPressed: (){
                            _scaffoldKey.currentState.showSnackBar(
                                new SnackBar(
                                    content: new Text('This is the Snackbar...'),
                                )
                            );
                        }
                    ),
                ),
            );
        }
    }

3.Ancestor Widget

假设您有一个属于另一个Widget的子树的Widget,如下图所示。

state_child_get_state.png

为了实现这一目标,需要满足3个条件:

1.“ Widget with State ”(红色)需要暴露其State

为了公开它的State,Widget需要在创建时记录它,如下所示:

class MyExposingWidget extends StatefulWidget {

   MyExposingWidgetState myState;

   @override
   MyExposingWidgetState createState(){
      myState = new MyExposingWidgetState();
      return myState;
   }
}

2.“ Widget State ”需要暴露一些getter / setter

为了让“ * stranger* ” set/get State的属性,Widget State需要通过以下方式授权访问:

  • public property(不推荐)
  • getter / setter

例:

class MyExposingWidgetState extends State<MyExposingWidget>{
   Color _color;

   Color get color => _color;
   ...
}

3.“ 对于想要获取State对象的Widget ”(蓝色)需要获得对State对象的的引用

class MyChildWidget extends StatelessWidget {
   @override
   Widget build(BuildContext context){
      final MyExposingWidget widget = context.ancestorWidgetOfExactType(MyExposingWidget);
      final MyExposingWidgetState state = widget?.myState;

      return new Container(
         color: state == null ? Colors.blue : state.color,
      );
   }
}

这个解决方案很容易实现,但子Widget如何知道它何时需要重建?

有了这个解决方案,它没有。它必须等待重建才能刷新其内容,这不是很方便。

下一节将讨论Inherited Widget的概念,它可以解决这个问题。


InheritedWidget

简而言之,InheritedWidget允许在Widget树中有效地传播(和共享)信息。

InheritedWidget是一个特殊的Widget,您将在widgets树中作为另一个子树的父Widget。该子树的所有Widget都必须能够与该InheritedWidget公开的数据进行交互

Basics

为了解释它,让我们考虑以下代码:

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(子子树的一部分)中的某些数据。

如前所述,为了能够传播/共享某些数据,需要将InheritedWidget定位在Widget的顶部,这解释了传递给InheritedWidget基础构造函数的@required Widget child

static MyInheritedWidget of(BuildContext context)方法,允许所有Widget获取最接近Context的MyInheritedWidget的实例(参见后面的内容)。

最后,重写updateShouldNotify方法用于告诉InheritedWidget,如果对数据应用了修改(请参阅下文),是否必须将通知传递给所有子Widget(已注册/已订阅)。

因此,我们需要将它放在树节点级别,如下所示:

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

子Widget如何访问InheritedWidget的数据?

在构建子进程时,后者将获得对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 new Container(
         color: inheritedWidget.data.color,
      );
   }
}

如何在Widget之间进行交互?

请考虑以下显示Widget树结构的图表。


WX20190730-155539@2x.png

为了说明一种交互方式,我们假设如下:

  • “Widget A”是一个将项目添加到购物车的按钮;
  • “Widget B”是一个显示购物车中商品数量的文本;
  • “Widget C”位于小部件B旁边,是一个内置任何文本的文本;
  • 我们希望“Widget B”在按下“Widget A”时自动在购物车中显示正确数量的项目,但我们不希望重建“Widget C”

InheritedWidget在此是最为合适的Widget!

代码示例

我们先写下代码,然后解释如下:

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() => new 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(new Item(reference));
    });
  }

  @override
  Widget build(BuildContext context){
    return new _MyInherited(
      data: this,
      child: widget.child,
    );
  }
}

class MyTree extends StatefulWidget {
  @override
  _MyTreeState createState() => new _MyTreeState();
}

class _MyTreeState extends State<MyTree> {
  @override
  Widget build(BuildContext context) {
    return new MyInheritedWidget(
      child: new Scaffold(
        appBar: new AppBar(
          title: new Text('Title'),
        ),
        body: new Column(
          children: <Widget>[
            new WidgetA(),
            new Container(
              child: new Row(
                children: <Widget>[
                  new Icon(Icons.shopping_cart),
                  new WidgetB(),
                  new WidgetC(),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

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

class WidgetB extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final MyInheritedWidgetState state = MyInheritedWidget.of(context);
    return new Text('${state.itemsCount}');
  }
}

class WidgetC extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Text('I am Widget C');
  }
}

说明

在这个非常基本的例子中,

  • _MyInherited是一个InheritedWidget,每次我们通过点击“Widget A”按钮添加一个Item时都会重新创建
  • MyInheritedWidget是一个Widget,其状态包含Items列表。可以通过(BuildContext context)静态MyInheritedWidgetState访问此状态
  • MyInheritedWidgetState公开一个getter(itemsCount)和一个方法(addItem),以便它们可以被小部件使用,这是小部件树的一部分
  • 每次我们将一个Item添加到State时,MyInheritedWidgetState都会重建
  • MyTree类只是构建一个小部件树,将MyInheritedWidget作为树的父级
  • WidgetA是一个简单的RaisedButton,当按下它时,从最近的MyInheritedWidget调用addItem方法
  • WidgetB是一个简单的文本,显示最接近的 MyInheritedWidget级别的项目数

这一切如何运作?

注册Widget以供以后通知

当子Widget调用MyInheritedWidget.of(context)时,传递它自己的context,它调用MyInheritedWidget的以下方法。

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

在内部,除了简单地返回MyInheritedWidgetState的实例之外,它还将将通知传递给所有子Widget(已注册/已订阅)。

在场景后面,对这个静态方法的简单调用实际上做了两件事:

  • 消费者(consumer)Widget被自动添加到(subscribers)列表中,之后如果InheritedWidget(这里_MyInherited)数据被修改,会被重建
  • _MyInheritedWidget(又名MyInheritedWidgetState)中引用的数据将返回给使用者(consumer)

由于'Widget A'和'Widget B'都已使用InheritedWidget订阅,因此如果对_MyInherited应用了修改,则当单击Widget A 的RaisedButton时,操作流程如下(简化版本):

  1. 调用MyInheritedWidgetStateaddItem方法
  2. MyInheritedWidgetState.addItem方法将新项添加到List
  3. 调用setState()以重建MyInheritedWidget
  4. 使用List的新内容创建_MyInherited的新实例
  5. _MyInherited记录在参数(数据)中传递的新State
  6. 作为InheritedWidget,它会检查是否有需要通知消费者(答案为真)
  7. 它迭代整个消费者列表(这里是Widget A和Widget B)并请求他们重建
  8. 由于Wiget C不是消费者,因此不会重建。

但是,Widget A和Widget B都重建了,而重建Wiget A却没用,因为它没有任何改变。如何防止这种情况发生?

访问“Inherited Widget”Widget时阻止某些Widget重建

Widget A也被重建的原因来自它访问MyInheritedWidgetState的方式

如前所述,调用context.inheritFromWidgetOfExactType()方法的事实会自动将Widget订阅到使用者列表中。

该解决方案,以防止该自动订阅,同时仍然允许该Widget A访问MyInheritedWidgetState是改变的静态方法MyInheritedWidget如下:

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

通过添加布尔额外参数...

  • 如果rebuild参数为true(默认情况下),我们使用普通方法(并且Widget将添加到订阅者列表中)
  • 如果rebuild参数是假的,我们仍然可以访问数据,但不使用内部实现的的InheritedWidget

因此,要完成解决方案,我们还需要稍微更新Widget A的代码,如下所示(我们添加false额外参数):

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');
        },
      ),
    );
  }
}

它就是,按下它时不再重建Widget A.

特别注意Routes, Dialogs…

Routes, Dialog Context与应用程序绑定。

这意味着即使在屏幕A内部您要求显示另一个屏幕B(例如,在当前的屏幕上),也无法轻松地从两个屏幕中的任何一个屏幕关联它们自己的Context。

屏幕B了解屏幕A上下文的唯一方法是从屏幕A获取它作为Navigator.of(context).push(...)的参数。

参考链接

结论

关于这些主题还有很多话要说......特别是在InheritedWidget上

在下一篇文章中,我将介绍通知器/监听器的概念,这在使用State和传送数据的方式中也非常有趣。


翻译不容易,大家且看且珍惜
原文

推荐阅读更多精彩内容