Flutter系列六:InheritedWidget的使用和源码分析

在用Flutter进行界面开发时,我们经常会遇到数据传递的问题。但是由于Flutter采用树形结构,造成数据传递的链条有时候会很长,代码写起来也很不方便。

InheritedWidget可以让它的子节点能访问到它的公开属性,从而实现数据的跨Widget的传递。

InheritedWidget使用

我们先用一个Demo来看看InheritedWidget的使用方法。Demo如下,InheritedWidget子类InfoWidgetnumber数值变化后,底下的三个InfoChildWidget显示的number也会变化。

demo

接下来我们来写代码。

  • 由于InheritedWidget是抽象类,我们创建一个继承 自InheritedWidgetInfoWidget
class InfoWidget extends InheritedWidget {
  // 1
  final int number;
    
  // 2    
  InfoWidget({Key key, @required this.number, @required child})
      : super(key: key, child: child);
    
  //3    
  @override
  bool updateShouldNotify(InfoWidget oldWidget) {
    return number != oldWidget.number;
  }
  
  // 4
  static InfoWidget of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType(); 
  }
  
}

代码说明:

  1. number就是定义的共享的数据;
  2. InfoWidget的构造函数中,有三个参数除了key外,都是必传参数,number是外部传入的给InfoWidget共享的数据,child子Widget
  • InheritedWidgetWidget的子类,但是没有StatefulWidget类似的State,这样InheritedWidget的所有属性都是不可变的,所以数据是需要父Widget提供的。
  • childInheritedWidget的必传参数,所以子类也得是必传参数。
  1. InheritedWidget的子类需要重写updateShouldNotify方法,这个方法如果返回true,则会回调StatefulElementstatedidChangeDependencies方法;
  2. of这个静态方法是留给子Widget使用的,子Widget可以通过它获取到InheritedWidget的共享数据。

of方法名是个约定俗成,当然也可以随便取个合法的方法名。

  • 建一个Widget,它可以显示InfoWidget共享的数据
class InfoChildWidget extends StatelessWidget {
  
  // 1
  const InfoChildWidget();

  @override
  Widget build(BuildContext context) {
    // 2
    final int number = InfoWidget.of(context).number;
    return Text("$number", style: TextStyle(color: Colors.amber, fontSize: 40));
  }
}
  1. 使用InfoChildWidget常量构造函数是为了解决不必要的重建和销毁。
  2. InfoWidget.of(context)就是上面提到的给子Widget使用的of静态方法,然后取到number就可以直接显示了。
  • 使用
InfoWidget(
    number: _number,
        child: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                InfoChildWidget(),
                InfoChildWidget(),
                InfoChildWidget(),
              ],
            ),
        ),
    )

使用的时候是将InfoChildWidget做为InfoWidget子Widget,我这里特意中间加了CenterColumn,就是为了指出InfoChildWidget不一定需要是直接子Widget

  • 所有代码如下:
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _number = 0;
  void _incrementCounter() {
    _number = Random().nextInt(100);
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: InfoWidget(
          number: _number,
          child: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                InfoChildWidget(),
                InfoChildWidget(),
                InfoChildWidget(),
              ],
            ),
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

<!-- InfoWidget -->
class InfoWidget extends InheritedWidget {
  final int number;

  InfoWidget({Key key, @required this.number, @required child})
      : super(key: key, child: child);

  @override
  bool updateShouldNotify(InfoWidget oldWidget) {
    return number != oldWidget.number;
  }

  static InfoWidget of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType();
  }
}

<!-- InfoChildWidget -->
class InfoChildWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final int number = InfoWidget.of(context).number;
    return Text("$number", style: TextStyle(color: Colors.amber, fontSize: 40));
  }
}

效果如下:

效果

InheritedWidget源码分析

设置inheritedWidgets

每个Element都有一个keyInheritedWidget类型,值为InheritedElementMap属性_inheritedWidgets

Map<Type, InheritedElement>? _inheritedWidgets;

每个Widget生成的Element挂载到Element Tree上的时候都会调用mount方法:

<!-- Element -->
void mount(Element? parent, dynamic newSlot) {
    _updateInheritance();
}

mount方法会调用_updateInheritance方法:

<!-- Element -->
void _updateInheritance() {
    _inheritedWidgets = _parent?._inheritedWidgets;
}

如果不是InheritedElement,则_inheritedWidgets都指向父Element_inheritedWidgets

<!-- InheritedElement -->
void _updateInheritance() {
    final Map<Type, InheritedElement>? incomingWidgets = _parent?._inheritedWidgets;
    if (incomingWidgets != null)
      _inheritedWidgets = HashMap<Type, InheritedElement>.from(incomingWidgets);
    else
      _inheritedWidgets = HashMap<Type, InheritedElement>();
    _inheritedWidgets![widget.runtimeType] = this;
}

如果是InheritedElement,先拷贝一份父节点的_inheritedWidgets, 然后添加或者替换key为widget.runtimeType,值为InheritedElement的键值对。

注意:这里如果父类有相同的widget.runtimeType,则会被替换,也就是说如果有多个相同的InheritedWidget,子节点的Element只能找到离它最近的那个。

inheritedWidgets
子ElementInheritedElement并添加依赖

我们来看看of类方法调用的dependOnInheritedWidgetOfExactType方法:

<!-- Element -->
Set<InheritedElement>? _dependencies;

T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({Object? aspect}) {
    // 1
    final InheritedElement? ancestor = _inheritedWidgets == null ? null : _inheritedWidgets![T];
    if (ancestor != null) {
      // 2
      return dependOnInheritedElement(ancestor, aspect: aspect) as T;
    }
    _hadUnsatisfiedDependencies = true;
    return null;
}

InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object? aspect }) {
    _dependencies ??= HashSet<InheritedElement>();
    // 3
    _dependencies!.add(ancestor);
    // 3
    ancestor.updateDependencies(this, aspect);
    return ancestor.widget;
}
  1. _inheritedWidgets这个Map中找到类型对应的InheritedElement
  2. 如果找到则调用dependOnInheritedElement方法;dependOnInheritedElement方法主要是将InheritedElement加入到_dependencies这个Set中,然后InheritedElement调用updateDependencies方法把子Element加入到_dependents中。
<!-- InheritedElement -->
void updateDependencies(Element dependent, Object? aspect) {
    setDependencies(dependent, null);
}

void setDependencies(Element dependent, Object? value) {
    _dependents[dependent] = value;
}

final Map<Element, Object?> _dependents = HashMap<Element, Object?>();
updateDependencies
InheritedElement数据变化调用StatefulElementdidChangeDependencies方法:

InheritedWidget调用build方法的时候,会调用notifyClients方法:

void updated(InheritedWidget oldWidget) {
    if (widget.updateShouldNotify(oldWidget))
      super.updated(oldWidget);
}
  
void updated(covariant ProxyWidget oldWidget) {
    notifyClients(oldWidget);
}

notifyClients方法会对_dependents中的每个子Element调用notifyDependent方法,子Element会调用didChangeDependencies方法:

void notifyClients(InheritedWidget oldWidget) {
    for (final Element dependent in _dependents.keys) {
      notifyDependent(oldWidget, dependent);
    }
}

void notifyDependent(covariant InheritedWidget oldWidget, Element dependent) {
    dependent.didChangeDependencies();
}

子Element调用didChangeDependencies最后会重新构建:

void didChangeDependencies() {
    markNeedsBuild();
}

子ElementStateFulElement时,会将_didChangeDependencies置为true;

void didChangeDependencies() {
    super.didChangeDependencies();
    _didChangeDependencies = true;
}

当重新构建时,StateFulElement会调用statedidChangeDependencies方法。

void performRebuild() {
    if (_didChangeDependencies) {
      state.didChangeDependencies();
      _didChangeDependencies = false;
    }
    super.performRebuild();
}
didChangeDependencies

总结

InheritedWidget传递参数的方案只是把传参从Constructor变成了BuildContext。但是它还是有些的不完善的地方:

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

推荐阅读更多精彩内容