從零開始的Flutter之旅: InheritedWidget

午後一小憩發表於2020-06-09

從零開始的Flutter之旅: InheritedWidget

往期回顧

從零開始的Flutter之旅: StatelessWidget

從零開始的Flutter之旅: StatefulWidget

在之前的文章中,介紹了StatelessWidget與StatefulWidget的特性與它們的呈現原理。

這期要聊的是它們的另一個兄弟InheritedWidget。

特性

InheritedWidget是Flutter中的一個非常重要的功能元件,它能夠提供資料在widget樹中從上到下進行傳遞。保證資料在不同子widget中進行共享。這對於一些需要使用共享資料的場景非常有效,例如,在Flutter SDK中就是通過InheritedWidget來共享應用的主題與語言資訊。

可能你還有點模糊,別急,下面我們通過一個簡單的示例來了解InheritedWidget。

示例

相信開始學Flutter時都看過官方的計數器示例。我們將官方提供的計數器示例使用InheritedWidget進行改造。

首先我們需要一個CountInheritedWidget,它繼承於InheritedWidget。

class CountInheritedWidget extends InheritedWidget {
  CountInheritedWidget({@required this.count, Widget child})
      : super(child: child);
 
  // 共享資料,計數的數量
  final int count;
 
  // 統一的獲取CountInheritedWidget例項, 方便樹中子widget的獲取共享資料
  // 必須在State中呼叫才會有效
  static CountInheritedWidget of(BuildContext context) {
    // 呼叫共享資料的子widget將不會回撥didChangeDependencies方法,即子widget將不會更新
    // return context.getElementForInheritedWidgetOfExactType<CountInheritedWidget>().widget;
    return context.dependOnInheritedWidgetOfExactType<CountInheritedWidget>();
  }
 
  // true -> 通知樹中依賴改共享資料的子widget
  @override
  bool updateShouldNotify(CountInheritedWidget oldWidget) {
    return oldWidget.count != count;
  }
}
複製程式碼
  1. 在CountInheritedWidget中提供共享計數的數量count
  2. 同時為外部提供統一的獲取CountInheritedWidget例項的of方法
  3. 最後再重寫updateShouldNotify方法,來通知依賴該共享count的子widget進行更新

現在已經有了共享資料count的提供,接下來是在具體的子widget中進行使用。

我們抽離出一個CountText子widget

class CountText extends StatefulWidget {
  @override
  _CountTextState createState() {
    return _CountTextState();
  }
}
 
class _CountTextState extends State<CountText> {
  @override
  Widget build(BuildContext context) {
    return Text("count: ${CountInheritedWidget.of(context).count.toString()}");
  }
 
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    print("didChangeDependencies");
  }
}
複製程式碼
  1. 內部引用了CountInheritedWidget中的共享資料count,通過of方法獲取CountInheritedWidget例項
  2. didChangeDependencies可以用來監聽子widget依賴是否反生改變

最後,我們再將CountInheritedWidget與CountText結合起來,通過簡單的點選自增事件來看下效果

class CountWidget extends StatefulWidget {
  @override
  _CountState createState() {
    return _CountState();
  }
}
 
class _CountState extends State<CountWidget> {
  int count = 0;
 
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Count App',
      theme: new ThemeData(primarySwatch: Colors.blue),
      home: Scaffold(
        appBar: AppBar(
          title: Text("Count"),
        ),
        body: Center(
          child: CountInheritedWidget(
            count: count,
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                CountText(),
                RaisedButton(
                  onPressed: () => setState(() => count++),
                  child: Text("Increment"),
                )
              ],
            ),
          ),
        ),
      ),
    );
  }
}
複製程式碼

上面的層級關係是CountText剛好是CountInheritedWidget的子widget。

現在我們通過點選事件直接改變外部的count值,如果InheritedWidget從上到下資料傳到的效果能夠生效,那麼在CountText中引用的count將會與外部count同步,程式呈現的效果將會是自增的,同時由於依賴的count發生改變CountText中的didChangeDependencies也會回撥。

我們直接執行一下

從零開始的Flutter之旅: InheritedWidget

點選後的輸出日誌

I/flutter: didChangeDependencies
複製程式碼

說明InheritedWidget的效果已經生效,通過InheritedWidget的使用,我們可以很方便的在巢狀下層的子widget中拿到上層的資料,或者說整個widget的共享資料。

分析

在依賴方式改變時子widget的didChangeDependencies會回撥,但由於你可能會在該方法中做一些特殊的操作,例如網路請求。只是需要一次就可以。如果是套用我們上面的示例,將會在count子增時反覆呼叫。

為了防止didChangeDependencies的呼叫,我們再來看CountInheritedWidget的of方法中註釋的那部分

  static CountInheritedWidget of(BuildContext context) {
    // 呼叫共享資料的子widget將不會回撥didChangeDependencies方法,即子widget將不會更新
    // return context.getElementForInheritedWidgetOfExactType<CountInheritedWidget>().widget;
    return context.dependOnInheritedWidgetOfExactType<CountInheritedWidget>();
  }
複製程式碼

我們使用的是dependOnInheritedWidgetOfExactType方法,依賴的共享資料發生改變時會回撥子widget中的didChangeDependencies方法,如果我們不想要子widget呼叫該方法,可以使用註釋的程式碼,通過getElementForInheritedWidgetOfExactType方法來獲取共享資料。

如果此時我們再執行一下專案,點選count自增,控制檯將不會再輸出日誌。這樣就可以解決didChangeDependencies的反覆呼叫。

而這兩個方法的主要區別是在dependOnInheritedWidgetOfExactType呼叫的過程中會進行註冊依賴關係

  @override
  InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object aspect }) {
    assert(ancestor != null);
    _dependencies ??= HashSet<InheritedElement>();
    _dependencies.add(ancestor);
    ancestor.updateDependencies(this, aspect);
    return ancestor.widget;
  }
複製程式碼

所以dependOnInheritedWidgetOfExactType更新依賴的子widget中的didChangeDependencies方法。

思考下一個問題,雖然現在didChangeDependencies方法不會呼叫,但是CountText的build方法還是會執行。原因是在CountWidget中通過setState來改變count值,會重新build所用的子widget。但我們真正想要的只是更新子widget中依賴的CountInheritedWidget的元件值。

那麼如何解決呢?這裡提供一個解決方案是為子widget提供快取。可以通過封裝一個簡單的StatefulWidget,將子widget快取起來。如果對這塊感興趣的,可以期待我的後續文章。

推薦專案

下面介紹一個完整的Flutter專案,對於新手來說是個不錯的入門。

flutter_github,這是一個基於Flutter的Github客戶端同時支援Android與IOS,支援賬戶密碼與認證登陸。使用dart語言進行開發,專案架構是基於Model/State/ViewModel的MSVM;使用Navigator進行頁面的跳轉;網路框架使用了dio。專案正在持續更新中,感興趣的可以關注一下。

從零開始的Flutter之旅: InheritedWidget

當然如果你想了解Android原生,相信flutter_github的純Android版本AwesomeGithub是一個不錯的選擇。

如果你喜歡我的文章模式,或者對我接下來的文章感興趣,建議您關注我的微信公眾號:【Android補給站】

或者掃描下方二維碼,與我建立有效的溝通,同時更快更準的收到我的更新推送。

從零開始的Flutter之旅: InheritedWidget

相關文章