一、問題概述
之前在使用 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() 來模擬從伺服器獲取資料
執行上面的程式碼,會出現下面的情況。
可以發現,每次 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 可以滿足我們的需求。
總結一下就是,這個類可以保證函式只執行一次,並且把結果儲存起來,當這個函式多次被呼叫時,就把這個結果返回。
要使用這個類,先他新增 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';
});
複製程式碼
最終效果如下:
這裡可能有人有疑問,就是這樣實現之後,每次重新整理都不會再重新整理 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';
}
}
複製程式碼
最後
歡迎關注「Flutter 程式設計開發」微信公眾號 。