BuildContext与InheritedWidget

在Flutter中Widjet,State,BuildContext,InheritedWidget在整个小部件里都是非常重要的概念。如何自定义,如何使用都需要对这几个概念十分了解。

BuildContext

BuildContext只不过是对构建的所有窗口小部件的树结构中的窗口小部件的位置的引用。简而言之,将BuildContext视为Widgets树的一部分,Widget将附加到此树。一个BuildContext只属于一个小部件。
如果窗口小部件“A”具有子窗口小部件,则窗口小部件“A”的BuildContext将成为直接子窗口BuildContexts的父BuildContext。很明显BuildContexts是链接的,并且正在组成BuildContexts树(父子关系)。

BuildContext可见性(简化语句):
“ Something ”仅在其自己的BuildContext或其父BuildContext的BuildContext中可见。

比如一个例子是,考虑Scaffold> Center> Column> Text,我希望从最后面的Text找到最顶端的Widjet:

context.ancestorWidgetOfExactType(Scaffold)=>通过从Text上下文转到树结构来返回第一个Scaffold。

从父BuildContext,也可以找到一个后代(=子)Widget,但不建议这样做
在上文总结了State,那么State与BuildContext有啥关系呢?

State和BuildContext之间的关系

对于有状态窗口小部件,状态与BuildContext关联。此关联是永久性的 ,State对象永远不会更改其BuildContext。即使可以在树结构周围移动Widget BuildContext,State仍将与该BuildContext保持关联。当State与BuildContext关联时,State被视为已挂载。

由于State对象与BuildContext相关联,这意味着State对象不能(直接)通过另一个BuildContext访问!

State链接到一个BuildContext,BuildContext链接到Widget的一个实例。

Widget唯一标识 - key

在Flutter中,每个Widget都是唯一标识的。这个唯一标识由构建/渲染时的框架定义。
此唯一标识对应于可选的Key参数。如果省略,Flutter将为您生成一个。
在某些情况下,您可能需要强制使用此密钥,以便可以通过其密钥访问窗口小部件。为此,您可以使用以下帮助程序之一:GlobalKey ,LocalKey,UniqueKey 或ObjectKey。

// GlobalKey确保关键是在整个应用程序唯一的。强制使用Widget的唯一标识:
GlobalKey myKey = new GlobalKey();
    ...
    @override
    Widget build(BuildContext context){
        return new MyWidget(
            key: myKey
        );
    }

有时,父窗口小部件可能需要访问其直接子节点的状态才能执行特定任务。在这种情况下,要访问这些直接子项State,您需要了解它们。我们先看看以下列子:让我们考虑一个基本示例,当用户点击按钮时显示SnackBar。由于SnackBar是Scaffold的子Widget,它不能直接被Scaffold身体的任何其他孩子访问(还记得上下文的概念及其层次结构/树结构吗?)。因此,访问它的唯一方法是通过ScaffoldState,它公开一个公共方法来显示SnackBar。

import 'package:flutter/material.dart';

class SnackBarDemo extends StatefulWidget{

  @override
  State<StatefulWidget> createState() {
    return new SnackState();
  }

}

class SnackState extends State<SnackBarDemo>{

  final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();

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

}
InheritedWidget

在说这个部件的时候,先来看看这个一个需求:如何在子Widget中获取到另外一个Widget的state的属性?如下代码:

import 'package:flutter/material.dart';

class OneWidget extends StatefulWidget {
  OneState one;

  @override
  State<StatefulWidget> createState() {
    one = new OneState();
    return one;
  }
}

class OneState extends State<OneWidget> {
  // 私有 暴露一些getter / setter
  Color _color = Colors.red;

  Color get color => _color;

  @override
  Widget build(BuildContext context) {
    return new Center(
      child: new Tow(),
    );
  }
}

class Tow extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final OneWidget widget = context.ancestorWidgetOfExactType(OneWidget);
    final OneState state = widget?.one;
    return new Container(
      color: state == null ?  Colors.blue : state.color,
    );
  }
}

虽然实现了,但子窗口小部件如何知道它何时需要重建?在这个解决方案,它不知道。它必须等待重建才能刷新其内容,这不是很方便。下面将讨论Inherited Widget的概念,它可以解决这个问题。

InheritedWidget允许在窗口小部件树中有效地传播(和共享)信息:InheritedWidget是一个特殊的Widget,您可以将其作为另一个子树的父级放在Widgets树中。该子树的所有小部件都必须能够与该InheritedWidget公开的数据进行交互。他的用法其实在很多系统的API中常见比如:Theme.of(context).textTheme,MediaQuery.of(context).size

class ChildWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final MyInheritedWidget inheritedWidget = MyInheritedWidget.of(context);
    return new Text('我是测试',style: Theme.of(context).textTheme.title,);
  }
}

inherited widget就像对其他的Widget的一个实现或者说是补充,就像Theme.of(context)返回你能拿到一个ThemeData,并使用其内部自定义的属性改变你当前widget的显示效果。那么如何体使用?可以看看下面列子:

import 'package:flutter/material.dart';

// 共享的数据
class InheritedData{
  final String data;
  const InheritedData(this.data);
}

// MyInheritedWidget作为所有widget的根旨在“共享”所有小部件(与子树的一部分)中的某些数据。
class MyInheritedWidget extends InheritedWidget {
  InheritedData data; // 共享的数据

  MyInheritedWidget({
    Key key,
    @required Widget child,
    this.data,
  }) : super(key: key, child: child);

  // 暴露给外部以便获得MyInheritedWidget,允许所有子窗口小部件获取最接近上下文的MyInheritedWidget的实例
  static MyInheritedWidget of(BuildContext context) {
    return context.inheritFromWidgetOfExactType(MyInheritedWidget);
  }

  // 用于告诉InheritedWidget是否必须将通知传递给所有子窗口小部件(已注册/已订阅),如果对数据应用了修改
  @override
  bool updateShouldNotify(MyInheritedWidget oldWidget) =>
      data != oldWidget.data;
}

class ChildWidget extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    // 子Widget就可以拿到父Widget的数据
    final MyInheritedWidget inheritedWidget = MyInheritedWidget.of(context);
    final InheritedData data = inheritedWidget.data;
    print("data的值为:"+data.data);
    return new Text(data.data);
  }
}

// 父容器
class ParentWidget extends StatelessWidget {

  InheritedData data;

  @override
  Widget build(BuildContext context) {
    // 构造器对数据容器进行初始化操作,保证后面拿到的data不是空
    data = new InheritedData("来打我啊");
    return new MyInheritedWidget(data: data,child: new ChildWidget());
  }
}

上面InheritedWidget其实可以看出是一个公共的数据共享空间,之前说她还有更新Widget功能,可以看下面示例,点击+和-两个按钮实现InheritedWidget中数据的增加与减少,并且用一个Widget显示InheritedWidget中数据的增加情况:

import 'package:flutter/material.dart';

/**
 * 共享的model
 */
class InheritedModel {
  final int count;

  InheritedModel(this.count);
}

/**
 * 定义InheritedWidget
 */
class TestInheritedWidget extends InheritedWidget {
  // 共享的数据
  final InheritedModel mode;

  // 自增的方法
  final Function() increment;

  // 自减的方法
  final Function() reduce;


  TestInheritedWidget({
    Key key,
    @required this.mode,
    @required this.increment,
    @required this.reduce,
    @required Widget child,
  }) : super(key: key, child: child);

  //是否重建widget就取决于数据是否相同
  @override
  bool updateShouldNotify(TestInheritedWidget oldWidget) =>
      mode != oldWidget.mode;

  static TestInheritedWidget of(BuildContext context) =>
      context.inheritFromWidgetOfExactType(TestInheritedWidget);
}

/**
 * 自增的widget
 */
class IncrementWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final testInheritedWidget = TestInheritedWidget.of(context);
    final mode = testInheritedWidget.mode;
    print('IncrementWidget中的count:${mode.count}');
    return new Padding(
      padding: const EdgeInsets.only(left: 10.0, top: 10.0, right: 10.0),
      child: new RaisedButton(
          onPressed: testInheritedWidget.increment,
          textColor: Colors.red,
          child: new Text("+")),
    );
  }
}


/**
 * 自减的widget
 */
class ReduceWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final testInheritedWidget = TestInheritedWidget.of(context);
    final mode = testInheritedWidget.mode;
    print('ReduceWidget中的count:${mode.count}');
    return new Padding(
      padding: const EdgeInsets.only(left: 10.0, top: 10.0, right: 10.0),
      child: new RaisedButton(
          onPressed: testInheritedWidget.reduce,
          textColor: Colors.green,
          child: new Text("-")),
    );
  }
}

/**
 * 展示数据更新的Widget
 */
class ShowCountWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final testInheritedWidget = TestInheritedWidget.of(context);
    final modeData = testInheritedWidget.mode;
    print('ShowCountWidget中的count:${modeData.count}');
    return new Padding(
      padding: const EdgeInsets.only(left: 10.0, top: 10.0, right: 10.0),
      child: new Text(
        '当前count:${modeData.count}',
        style: new TextStyle(fontSize: 20.0),
      ),
    );
  }
}

/**
 * 因为需要在state中改变不同的增减操作,所以选择StatefulWidget
 */
class InheritedWidgetTestContainer extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return new _InheritedWidgetTestContainerState();
  }
}

class _InheritedWidgetTestContainerState
    extends State<InheritedWidgetTestContainer> {
  InheritedModel model;

  _initData() {
    model = new InheritedModel(0);
  }

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

  _incrementCount() {
    setState(() {
      model = new InheritedModel(model.count + 1);
    });
  }

  _reduceCount() {
    setState(() {
      model = new InheritedModel(model.count - 1);
    });
  }

  @override
  Widget build(BuildContext context) {
    return new TestInheritedWidget(
        mode: model,
        increment: _incrementCount,
        reduce: _reduceCount,
        child: new Scaffold(
          appBar: new AppBar(
            title: new Text('我是测试widget更新'),
          ),
          body: new Column(
            children: <Widget>[
              new IncrementWidget(),
              new ShowCountWidget(),
              new ReduceWidget(),
            ],
          ),
        )
    );
  }
}

上面的代码中其实就是以TestInheritedWidget作为数据共享的根节点,在其节点下的所有Widget在数据发现变化的时候都会重新更新。那么我们有一个需求,就是在一个节点下有A,B,C三个Widget,我希望B在A点击的时候状态会改变,但是A,C不改变如何实现:

import 'package:flutter/material.dart';

class Item {
  String reference;

  Item(this.reference);
}

// _MyInherited是一个InheritedWidget,每次我们通过点击“Widget A”按钮添加一个Item时都会重新创建它
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;// 由于data是永远不会改变的,所以这里需要更新子组件的话直接设置为true
  }
}

// MyInheritedWidget是一个Widget,其状态包含Items列表。可以通过“(BuildContext context)的静态MyInheritedWidgetState”访问此状态。
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, bool rebuild = true]){
    return (rebuild ? context.inheritFromWidgetOfExactType(_MyInherited) as _MyInherited
        : context.ancestorWidgetOfExactType(_MyInherited) as _MyInherited).data;// ancestorWidgetOfExactType:返回给定类型的最近祖先小部件,这个组件创建过一次就不会重新创建
  }
}

//MyInheritedWidgetState公开一个getter(itemsCount)和一个方法(addItem),以便它们可以被小部件使用,这是子小部件树的一部分
class MyInheritedWidgetState extends State<MyInheritedWidget>{
  /// List of Items
  List<Item> _items = <Item>[];

  /// Getter (number of items)
  int get itemsCount => _items.length;

  // 这个方法没吊用一次都会重新执行build方法,所以_MyInherited也会重新执行一遍
  void addItem(String reference){
    setState((){
      _items.add(new Item(reference));
    });
  }

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

// 每次我们向State添加一个Item时,MyInheritedWidgetState都会重建
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(),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

// WidgetA是一个简单的RaisedButton,当按下它时,从最近的MyInheritedWidget调用addItem方法,它本身是不需要重新更新的,所以我们设计让他不会重新更新
class WidgetA extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final MyInheritedWidgetState state = MyInheritedWidget.of(context,false);
    print("WidgetA重新更新了~~~");
    return new Container(
      child: new RaisedButton(
        child: new Text('Add Item'),
        onPressed: () {
          state.addItem('new item');
        },
      ),
    );
  }
}

// WidgetB是一个简单的文本,显示最接近的MyInheritedWidget级别的项目数
class WidgetB extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final MyInheritedWidgetState state = MyInheritedWidget.of(context,true);//这段代码其实就相当于注册订阅,true表示需要接受更新
    print("WidgetB重新更新了~~~");
    return new Text('${state.itemsCount}');
  }
}

class WidgetC extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    print("WidgetC重新更新了~~~");
    return new Text('I am Widget C');
  }
}

上面代码详解:说白了就是给订阅的人进行Widget的更新
当子Widget调用MyInheritedWidget.of(context)时,它会调用MyInheritedWidget的以下方法,并传递自己的BuildContext。

 static MyInheritedWidgetState of([BuildContext context, bool rebuild = true]){
    return (rebuild ? context.inheritFromWidgetOfExactType(_MyInherited) as _MyInherited
        : context.ancestorWidgetOfExactType(_MyInherited) as _MyInherited).data;// ancestorWidgetOfExactType:返回给定类型的最近祖先小部件,这个组件创建过一次就不会重新创建
  }

在内部,除了简单地返回MyInheritedWidgetState的实例之外,它还将消费者窗口小部件订阅到更改通知。
整个过程如下:

  • 调用MyInheritedWidget.of(context,true)这个方法本质返回的是MyInheritedWidgetState类。它还将消费者窗口小部件订阅到更改通知。这里核心是A与B传入了context,是通过AB的context获取到继承小部件,而C没有传入。
  • 调用MyInheritedWidgetState类中的void addItem(String reference)方法,因为setState()方法,所以会重新执行build方法,最后也就是_MyInherited重新创建,也就是updateShouldNotify执行了。其实就是_MyInherited中的data重新了,里面的数据也就是最新的。它检查是否需要“通知”“使用者”(答案为是)
  • 迭代整个消费者列表(这里是Widget B)并请求他们重建
  • 由于Wiget C不是消费者,因此不会重建。因为C没有传入context获取对应的MyInheritedWidgetState,而且MyInheritedWidgetState也没有重新创建_MyInherited,所以就算C在MyInheritedWidget组件的节点下,也不会接收到更新的通知。

总的来说就是利用StatefulWidget来存储需要改变的数据,然后StatefulWidget的根节点设置为InheritedWidget,然后利用StatefulWidget的state的setState方法重新创建生成新的InheritedWidget节点,最后也就InheritedWidget中定义的新的StatefulWidget变量,然后updateShouldNotify重新调用通知相关订阅的Widget更新。

这里其实就有点像Android中的eventbus订阅了就能接收到通知类似。在flutter中类似的框架有Redux,这个后面再总结。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,504评论 4 365
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,898评论 1 300
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,218评论 0 248
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,322评论 0 214
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,693评论 3 290
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,812评论 1 223
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,010评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,747评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,476评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,700评论 2 251
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,190评论 1 262
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,541评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,206评论 3 240
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,129评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,903评论 0 199
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,894评论 2 283
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,748评论 2 274

推荐阅读更多精彩内容