停止 FutrueBuilder 的重複重新整理和執行

Flutter程式設計開發發表於2020-04-19

一、問題概述

之前在使用 FutureBuilder 的過程中發現了一個問題,就是每次重新整理介面的時候,FutrueBuilder 中的 future 函式都會執行,雖然不會造成什麼嚴重問題(甚至有的時候需求就是這樣的),但是在某些情況下,這個問題是一定要解決的(後面會說明)。

github 上有一個相關的 issue: FutureBuilder fire

我在 Medium 上看到了一篇相關的文章來解釋這個問題,這裡分享給大家。

二、問題復現

要復現這個問題很簡單,只要頁面中有 FutureBuilder 元件,那麼每次呼叫 setState 都會導致 FutureBuilder 重新整理一次,future 函式執行一次。

假設我們的程式碼如下:

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: HomeScreen()
    );
  }
}

class HomeScreen extends StatefulWidget {
  @override
  _HomeScreenState createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {

  bool _switchValue;

  @override
  void initState() {
    super.initState();
    this._switchValue = false;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Switch(
            value: this._switchValue,
            onChanged: (newValue) {
              setState(() {
                this._switchValue = newValue;
              });
            },
          ),
          FutureBuilder(
              future: this._fetchData(),
              builder: (context, snapshot) {
                switch (snapshot.connectionState) {
                  case ConnectionState.none:
                  case ConnectionState.waiting:
                    return Center(
                      child: CircularProgressIndicator()
                    );
                  default:
                    return Center(
                      child: Text(snapshot.data)
                    );
                }
              }
          ),
        ],
      ),
    );
  }

  _fetchData() async {
    await Future.delayed(Duration(seconds: 2));
    return 'REMOTE DATA';
  }
}
複製程式碼
  • Switch 元件狀態會觸發 setState()
  • FutrueBuilder 通過 _fetchData() 來模擬從伺服器獲取資料

執行上面的程式碼,會出現下面的情況。

停止 FutrueBuilder 的重複重新整理和執行

可以發現,每次 switch 狀態的改變,都會導致 FutrueBuilder 重新整理一次,但是在上面的程式碼裡,switch 和 FutureBuilder 其實是沒有任何聯絡的。由於任何 setState 操作都會導致 FutureBuilder 重新整理一次,那麼可能就會導致以下問題:

  • 當介面已經不可見時依然有程式碼在執行(消耗效能與流量等)
  • 熱過載不正確
  • 在 Inherited 元件裡面更新數值將導致 Navigator 狀態丟失
  • etc...

三、原因分析

FutureBuilder 其實是一個 StatefulWidget 型別的元件。

class FutureBuilder<T> extends StatefulWidget {
  /// Creates a widget that builds itself based on the latest snapshot of
  /// interaction with a [Future].
  ///
  /// The [builder] must not be null.
  const FutureBuilder({
    Key key,
    this.future,
    this.initialData,
    @required this.builder,
  }) : assert(builder != null),
       super(key: key);
...
}
複製程式碼

StatefulWidgets 元件將會持有一個生命週期很長的 State 物件,這個 State 物件有很多方法和生命週期有關。

  • initState : 在物件建立之後呼叫一次。
  • build 每次當我們重新整理展示元件時,這個方法都會被呼叫。
  • didUpdateWidget 當一個老元件被回收一個新元件重建,並且這個新 Widget 和當前 State 物件繫結到一起的時候,didUpdateWidget 就會被回撥。簡單的說就是,當一個元件關聯的 State 物件改變時,didUpdateWidget 就會執行,這裡可以進行一些 rebuild 之前需要進行的操作。

在 FutureBuilder 裡面,didUpdateWidget方法重寫如下:

@override
void didUpdateWidget(FutureBuilder<T> oldWidget) {
  super.didUpdateWidget(oldWidget);
  if (oldWidget.future != widget.future) {
    if (_activeCallbackIdentity != null) {
      _unsubscribe();
      _snapshot = _snapshot.inState(ConnectionState.none);
    }
    _subscribe();
  }
}
複製程式碼

在這裡會進行判斷,當新的 widget 和 老的 widget 的 future 不是同一個物件例項的時候,就會重複執行一遍: _unsubscribe() 和 _subscribe() ,也就是 FutureBuilder 的生命週期重新走了一遍。

因此現在的問題就是,每次介面重新整理的時候,FutureBuilder 老狀態元件的 future 例項都和新狀態元件的 futrue 例項不是同一個,那麼我們要做的就是,讓 FutureBuilder 新老元件獲取相同的 future 例項即可。

四、解決方案

在一些函式式語言裡面,當一個函式確定之後,只要輸入相同,那麼這個函式一定會輸出相同的結果,因此我們可以把這個結果儲存起來,當這個函式再次被呼叫的時候,我們直接返回上一次執行的結果即可。

放到我們這裡就是,我們需要把 futrue 物件例項儲存起來,每次都獲取這個相同的例項,就可以解決上面的問題。

dart 裡面正好提供了一個類 AsyncMemoizer 可以滿足我們的需求。

停止 FutrueBuilder 的重複重新整理和執行

總結一下就是,這個類可以保證函式只執行一次,並且把結果儲存起來,當這個函式多次被呼叫時,就把這個結果返回。

要使用這個類,先他新增 async 庫依賴(注意依賴版本不要衝突):

  async: ^2.3.0
複製程式碼

我們在程式碼裡面初始化 AsnycMemorizer 物件例項:

final AsyncMemoizer _memoizer = AsyncMemoizer();
複製程式碼

最後把之前定義的 future 函式結合 memorizer 使用:

_fetchData() {
  return this._memoizer.runOnce(() async {
    await Future.delayed(Duration(seconds: 2));
    return 'REMOTE DATA';
  });
複製程式碼

最終效果如下:

停止 FutrueBuilder 的重複重新整理和執行

這裡可能有人有疑問,就是這樣實現之後,每次重新整理都不會再重新整理 FutureBuilder 了,那麼如果我想控制到底要不要重新整理,我該怎麼做?我覺得可以這樣,加入一個變數,根據這個變數來判斷是否返回相同例項,從而達到是否重新整理的效果。

  _fetchData(bool flag) async{
    if(flag){
      return this._memoizer.runOnce(() async {
        await Future.delayed(Duration(seconds: 2));
        return 'REMOTE DATA';
      });
    }else{
      await Future.delayed(Duration(seconds: 5));
      return 'REMOTE DATA';
    }
  }
複製程式碼

github

最後

歡迎關注「Flutter 程式設計開發」微信公眾號 。

停止 FutrueBuilder 的重複重新整理和執行

相關文章