在用Flutter進行介面開發時,我們經常會遇到資料傳遞的問題。但是由於Flutter採用樹形結構,造成資料傳遞的鏈條有時候會很長,程式碼寫起來也很不方便。
InheritedWidget可以讓它的子節點能訪問到它的公開屬性,從而實現資料的跨Widget的傳遞。
InheritedWidget使用
我們先用一個Demo來看看InheritedWidget的使用方法。Demo如下,InheritedWidget子類InfoWidget的number
數值變化後,底下的三個InfoChildWidget顯示的number
也會變化。
接下來我們來寫程式碼。
- 由於InheritedWidget是抽象類,我們建立一個繼承 自InheritedWidget的InfoWidget。
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();
}
}
複製程式碼
程式碼說明:
number
就是定義的共享的資料;- InfoWidget的建構函式中,有三個引數除了
key
外,都是必傳引數,number
是外部傳入的給InfoWidget共享的資料,child
是子Widget;
- InheritedWidget是Widget的子類,但是沒有StatefulWidget類似的State,這樣InheritedWidget的所有屬性都是不可變的,所以資料是需要父Widget提供的。
child
是InheritedWidget的必傳引數,所以子類也得是必傳引數。
- InheritedWidget的子類需要重寫
updateShouldNotify
方法,這個方法如果返回true
,則會回撥StatefulElement中state的didChangeDependencies
方法; 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));
}
}
複製程式碼
- 使用InfoChildWidget的常量建構函式是為了解決不必要的重建和銷燬。
InfoWidget.of(context)
就是上面提到的給子Widget使用的of
靜態方法,然後取到number就可以直接顯示了。
- 使用
InfoWidget(
number: _number,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
InfoChildWidget(),
InfoChildWidget(),
InfoChildWidget(),
],
),
),
)
複製程式碼
使用的時候是將InfoChildWidget做為InfoWidget的子Widget,我這裡特意中間加了Center和Column,就是為了指出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都有一個key為InheritedWidget型別,值為InheritedElement的Map屬性_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只能找到離它最近的那個。
子Element對InheritedElement並新增依賴
我們來看看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;
}
複製程式碼
- 從
_inheritedWidgets
這個Map中找到型別對應的InheritedElement; - 如果找到則呼叫
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?>();
複製程式碼
InheritedElement資料變化呼叫StatefulElement的didChangeDependencies
方法:
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();
}
複製程式碼
當子Element為StateFulElement時,會將_didChangeDependencies
置為true;
void didChangeDependencies() {
super.didChangeDependencies();
_didChangeDependencies = true;
}
複製程式碼
當重新構建時,StateFulElement會呼叫state的didChangeDependencies
方法。
void performRebuild() {
if (_didChangeDependencies) {
state.didChangeDependencies();
_didChangeDependencies = false;
}
super.performRebuild();
}
複製程式碼
總結
InheritedWidget傳遞引數的方案只是把傳參從Constructor變成了BuildContext。但是它還是有些的不完善的地方:
- 某個型別的InheritedWidget只能獲取到最近的那一個;
- 重新構建沒法只重構依賴InheritedWidget的子Widget,效能上不是太好。