InheritedWidget的使用和原始碼分析

chonglingliu發表於2021-03-25

在用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,效能上不是太好。

相關文章