前言:
我們經(jīng)常有這樣的一個(gè)開(kāi)發(fā)場(chǎng)景:一個(gè)頁(yè)面進(jìn)入之后先進(jìn)行網(wǎng)絡(luò)請(qǐng)求,此時(shí)顯示一個(gè)圓圈(等待動(dòng)畫)倍啥,等網(wǎng)絡(luò)數(shù)據(jù)返回時(shí)顯示一個(gè)展示網(wǎng)絡(luò)數(shù)據(jù)的布局虽缕。例如下圖:
我們通常的做法是
if(data==null){
return CircularProgressIndicator();
}else{
return ListView(...);
}
大致就是數(shù)據(jù)返回之前我們加載一個(gè)組件氮趋,等數(shù)據(jù)返回值后,我們重繪頁(yè)面返回另一個(gè)組件诉植。
在flutter中晾腔,有一個(gè)新的實(shí)現(xiàn)方式,那就是我們即將要介紹的futureBuilder.
FutureBuilder用法和實(shí)現(xiàn)
Widget that builds itself based on the latest snapshot of interaction with a Future.
官方意思是一個(gè)基于與Future交互的最新快照構(gòu)建自己的小部件壁查。
先看一下它的構(gòu)造方法:
const FutureBuilder({
Key key,
this.future, //獲取數(shù)據(jù)的方法
this.initialData, //初始的默認(rèn)數(shù)據(jù)
@required this.builder
}) : assert(builder != null),
super(key: key);
主要看一下builder睡腿,這個(gè)是我們主要關(guān)心的嫉到,它是我們構(gòu)建組件的策略何恶。
接收兩個(gè)參數(shù):BuildContext context, AsyncSnapshot snapshot.
context就不解釋了嚼黔,snapshot就是_calculation在時(shí)間軸上執(zhí)行過(guò)程的狀態(tài)快照。
//FutureBuilder控件
new FutureBuilder<String>(
future: _calculation, // 用戶定義的需要異步執(zhí)行的代碼疫赎,類型為Future<String>或者null的變量或函數(shù)
builder: (BuildContext context, AsyncSnapshot<String> snapshot) { //snapshot就是_calculation在時(shí)間軸上執(zhí)行過(guò)程的狀態(tài)快照
switch (snapshot.connectionState) {
case ConnectionState.none: return new Text('Press button to start'); //如果_calculation未執(zhí)行則提示:請(qǐng)點(diǎn)擊開(kāi)始
case ConnectionState.waiting: return new Text('Awaiting result...'); //如果_calculation正在執(zhí)行則提示:加載中
default: //如果_calculation執(zhí)行完畢
if (snapshot.hasError) //若_calculation執(zhí)行出現(xiàn)異常
return new Text('Error: ${snapshot.error}');
else //若_calculation執(zhí)行正常完成
return new Text('Result: ${snapshot.data}');
}
},
)
FutureBuilder通過(guò)子屬性future
獲取用戶需要異步處理的代碼捧搞,用builder
回調(diào)函數(shù)暴露出異步執(zhí)行過(guò)程中的快照胎撇。我們通過(guò)builder的參數(shù)snapshot
暴露的快照屬性晚树,定義好對(duì)應(yīng)狀態(tài)下的處理代碼雅采,即可實(shí)現(xiàn)異步執(zhí)行時(shí)的交互邏輯婚瓜。
看起來(lái)似乎有點(diǎn)繞口,我們看看下面這段代碼:
/*
* Created by 李卓原 on 2018/9/30.
* email: zhuoyuan93@gmail.com
* 關(guān)于狀態(tài)改變引起的不必要的頁(yè)面刷新:https://github.com/flutter/flutter/issues/11426#issuecomment-414047398
*/
import 'dart:async';
import 'package:async/async.dart';
import 'package:flutter/material.dart';
import 'package:flutter_app/utils/HttpUtil.dart';
class FutureBuilderPage extends StatefulWidget {
@override
State<StatefulWidget> createState() => FutureBuilderState();
}
class FutureBuilderState extends State<FutureBuilderPage> {
String title = 'FutureBuilder使用';
Future _gerData() async {
var response = HttpUtil()
.get('http://api.douban.com/v2/movie/top250', data: {'count': 15});
return response;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
title = title + '.';
});
},
child: Icon(Icons.title),
),
body: FutureBuilder(
builder: _buildFuture,
future: _gerData(), // 用戶定義的需要異步執(zhí)行的代碼,類型為Future<String>或者null的變量或函數(shù)
),
);
}
///snapshot就是_calculation在時(shí)間軸上執(zhí)行過(guò)程的狀態(tài)快照
Widget _buildFuture(BuildContext context, AsyncSnapshot snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
print('還沒(méi)有開(kāi)始網(wǎng)絡(luò)請(qǐng)求');
return Text('還沒(méi)有開(kāi)始網(wǎng)絡(luò)請(qǐng)求');
case ConnectionState.active:
print('active');
return Text('ConnectionState.active');
case ConnectionState.waiting:
print('waiting');
return Center(
child: CircularProgressIndicator(),
);
case ConnectionState.done:
print('done');
if (snapshot.hasError) return Text('Error: ${snapshot.error}');
return _createListView(context, snapshot);
default:
return null;
}
}
Widget _createListView(BuildContext context, AsyncSnapshot snapshot) {
List movies = snapshot.data['subjects'];
return ListView.builder(
itemBuilder: (context, index) => _itemBuilder(context, index, movies),
itemCount: movies.length * 2,
);
}
Widget _itemBuilder(BuildContext context, int index, movies) {
if (index.isOdd) {
return Divider();
}
index = index ~/ 2;
return ListTile(
title: Text(movies[index]['title']),
leading: Text(movies[index]['year']),
trailing: Text(movies[index]['original_title']),
);
}
}
在build方法中茂附,我們返回了一個(gè)Scaffold,主要的代碼在body中营曼,包裹了一個(gè)FutureBuilder,
我們?cè)谒腷uilder方法中锻全,對(duì)不同狀態(tài)返回了不同的控件鳄厌。
snapshot.connectionState
就是異步函數(shù)_gerData
的執(zhí)行狀態(tài)妈踊,用戶通過(guò)定義在ConnectionState.none
和ConnectionState.waiting
狀態(tài)下廊营,輸出一個(gè)Text和居中·(Center)·顯示并且內(nèi)置文字CircularProgressIndicator的組件,其意義即:當(dāng)異步函數(shù)_gerData
未執(zhí)行時(shí)呐伞,屏幕正中央顯示文字:還沒(méi)有開(kāi)始網(wǎng)絡(luò)請(qǐng)求
伶氢。和正在執(zhí)行時(shí),顯示一個(gè)刷新?tīng)顟B(tài)的控件鞍历。
當(dāng)_gerData
執(zhí)行完畢后,snapshot.connectionState
的值即變?yōu)?code>ConnectionState.done扇救,此時(shí)即可輸出根據(jù)HTTP請(qǐng)求獲取到的數(shù)據(jù)生成對(duì)應(yīng)的ListItem迅腔。由于ConnectionState.done
是除了ConnectionState.none
和ConnectionState.waiting
以外的唯一值靠娱,所以代碼中在switch下用default也可(ConnectionState.active
好像在整個(gè)過(guò)程中沒(méi)有調(diào)用)。
由于通過(guò)FutureBuilder
內(nèi)的builder()
函數(shù)即可操控控件的狀態(tài)和重繪锌雀,我們不必通過(guò)自己寫異步狀態(tài)的判斷和多次使用setState()
實(shí)現(xiàn)頁(yè)面上加載中和加載完成顯示效果的切換腋逆,因?yàn)?code>FutureBuilder內(nèi)部自帶了執(zhí)行setState()
的方法。
現(xiàn)在一個(gè)FutureBuilder的構(gòu)建就算完成了等脂。
防止FutureBuilder進(jìn)行不必要的重繪
如果只是寫一個(gè)FutureBuilder,我們就不需要floatingActionButton里的一系列東西上遥,所以這時(shí)候就到它的出場(chǎng)了粉楚。
代碼中的意思,每次點(diǎn)擊它解幼,就在我們標(biāo)題后面加一個(gè)“.” , 看一下效果
確實(shí)是改變了標(biāo)題撵摆,但是整個(gè)頁(yè)面也隨著setState而進(jìn)行了不必要的重繪特铝,這就是我們本篇的重點(diǎn)了壹瘟。
即使AppBar和FutureBuilder沒(méi)有任何關(guān)聯(lián)稻轨,每次我們改變它的值(通過(guò)調(diào)用setState)殴俱, FutureBuilder都會(huì)再次經(jīng)歷整個(gè)生命周期!它重新取代future明场,導(dǎo)致不必要的流量苦锨,并再次顯示負(fù)載,導(dǎo)致糟糕的用戶體驗(yàn)拉庶。
這個(gè)問(wèn)題以各種方式表現(xiàn)出來(lái)砍的。在某些情況下莺治,它甚至不像上面的例子那么明顯。例如:
- 從當(dāng)前不在屏幕上的頁(yè)面生成的網(wǎng)絡(luò)流量
- 熱重裝不能正常工作
- 更新某些“繼承的窗口小部件”中的值時(shí)丟失導(dǎo)航器狀態(tài)
- 等等…
但是這一切的原因是什么床佳?我們?nèi)绾谓鉀Q它砌们?
didUpdateWidget問(wèn)題
注意:在本節(jié)中搁进,我將詳細(xì)介紹FutureBuilder的工作原理饼问。如果您對(duì)此不感興趣莱革,可以跳到解決方案。
如果我們仔細(xì)看看代碼FutureBuilder捐名,我們發(fā)現(xiàn)它是一個(gè)StatefulWidget镶蹋。我們知道贺归,StatefulWidgets維護(hù)一個(gè)長(zhǎng)期存在的State對(duì)象除破。這種狀態(tài)有一些管理其生命周期的方法瑰枫,就像方法initState
光坝,build
和didUpdateWidget
盯另。
initState
在第一次創(chuàng)建狀態(tài)對(duì)象時(shí)只調(diào)用一次鸳惯,并且build每次我們需要構(gòu)建要顯示的窗口小部件時(shí)調(diào)用它芝发,但是那是什么didUpdateWidget
呢?只要附加到此State對(duì)象的窗口小部件發(fā)生更改格郁,就會(huì)調(diào)用此方法例书。
當(dāng)使用新輸入重建窗口小部件時(shí)决采,將放置舊窗口小部件织狐,并創(chuàng)建新窗口小部件并將其分配給State對(duì)象,并didUpdateWidget在重建之前調(diào)用它以執(zhí)行我們想要執(zhí)行的任何操作筏勒。
在FutureBuilder
這種情況下厨埋,這個(gè)方法看起來(lái)像這樣:
@override
void didUpdateWidget(FutureBuilder<T> oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.future != widget.future) {
if (_activeCallbackIdentity != null) {
_unsubscribe();
_snapshot = _snapshot.inState(ConnectionState.none);
}
_subscribe();
}
}
它基本上是說(shuō):如果在重建時(shí)捐顷,新窗口小部件具有與舊窗口小部件不同的Future實(shí)例废赞,則重復(fù)所有內(nèi)容:取消訂閱唉地,并再次訂閱。
但我們不是提供相同的Future嗎耘沼?我們稱之為同一個(gè)功能菠隆!好吧骇径,F(xiàn)uture的情況不一樣了既峡。我們的功能正在完成同樣的工作运敢,但隨后又回歸了一個(gè)與舊的不同的新Future忠售。
因此传惠,我們想要做的是在第一次調(diào)用時(shí)存儲(chǔ)或緩存函數(shù)的輸出,然后在再次調(diào)用函數(shù)時(shí)提供相同的輸出稻扬。此過(guò)程稱為記憶(memoization
)卦方。
解決方案 1 :Memoize the future
簡(jiǎn)單來(lái)說(shuō),Memoization
緩存函數(shù)的返回值泰佳,并在再次調(diào)用該函數(shù)時(shí)重用它盼砍。Memoization
主要用于函數(shù)式語(yǔ)言,其中函數(shù)是確定性的(它們總是為相同的輸入返回相同的輸出)逝她,但我們可以在這里使用簡(jiǎn)單的memoization
來(lái)解決我們的問(wèn)題浇坐,以確保FutureBuilder
始終接收相同的未來(lái)實(shí)例。
為此黔宛,我們將使用Dart
的AsyncMemoizer
觉渴。這個(gè)記憶器完全符合我們的要求!它需要一個(gè)異步函數(shù)回右,在第一次調(diào)用它時(shí)調(diào)用它渺氧,并緩存其結(jié)果。對(duì)于該函數(shù)的所有后續(xù)調(diào)用弧腥,memoizer
返回相同的先前計(jì)算的未來(lái)铡买。
因此澡为,為了解決我們的問(wèn)題谷徙,我們首先在我們的小部件中創(chuàng)建一個(gè)AsyncMemoizer實(shí)例:
final AsyncMemoizer _memoizer = AsyncMemoizer();
注意:你不應(yīng)該在StatelessWidget中實(shí)例化memoizer,因?yàn)镕lutter在每次重建時(shí)都會(huì)處理StatelessWidgets蛤织,這基本上可以達(dá)到目的摊鸡。您應(yīng)該在StatefulWidget中實(shí)例化它是辕,或者在它可以持久化的地方實(shí)例化它。
之后,我們將修改_fetchData函數(shù)以使用該memoizer:
_gerData() {
return _memoizer.runOnce(() async {
return await HttpUtil()
.get('http://api.douban.com/v2/movie/top250', data: {'count': 15});
});
}
我們用AsyncMemoizer.runOnce包裝我們的函數(shù),它完全聽(tīng)起來(lái)像它的聲音;它只運(yùn)行一次該函數(shù)裸弦,并在再次調(diào)用時(shí)返回緩存的Future。
就是這樣!我們的FutureBuilder現(xiàn)在只是第一次觸發(fā):
現(xiàn)在,我們其他地方進(jìn)行setState也不會(huì)導(dǎo)致FutureBuilder的重繪了槽奕。
為了解決這個(gè)問(wèn)題囱持,我們使用Dart的AsyncMemoizer每次都傳遞相同的Future實(shí)例掩幢。
解決方法2 在構(gòu)建函數(shù)之外調(diào)用Future
問(wèn)題是每次發(fā)布重建時(shí)都會(huì)調(diào)用FutureBuilder
狀態(tài)的didUpdateWidget
芍阎。此函數(shù)檢查舊的future
對(duì)象是否與新的對(duì)象不同肿轨,如果是藻茂,則重新啟動(dòng)FutureBuilder
优俘。為了解決這個(gè)問(wèn)題叶雹,我們可以在構(gòu)建函數(shù)之外的某個(gè)地方調(diào)用Future
沾瓦。例如缕探,在initState
中豁鲤,將其保存在成員變量中,并將此變量傳遞給FutureBuilder
炫狱。
比如:
var _futureBuilderFuture;
...
@override
void initState() {
///用_futureBuilderFuture來(lái)保存_gerData()的結(jié)果,以避免不必要的ui重繪
_futureBuilderFuture = _gerData();
}
...
FutureBuilder(
future: _futureBuilderFuture ,
....
這里使用_futureBuilderFuture來(lái)保存_gerData()的結(jié)果视译,這樣我們傳遞給FutureBuilder的是一個(gè)成員變量限番,而不是一個(gè)方法就不會(huì)多次調(diào)用了。
看一下完整代碼:
/*
* Created by 李卓原 on 2018/9/30.
* email: zhuoyuan93@gmail.com
* 關(guān)于狀態(tài)改變引起的不必要的頁(yè)面刷新:https://github.com/flutter/flutter/issues/11426#issuecomment-414047398
*/
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_app/utils/HttpUtil.dart';
class FutureBuilderPage extends StatefulWidget {
@override
State<StatefulWidget> createState() => FutureBuilderState();
}
class FutureBuilderState extends State<FutureBuilderPage> {
String title = 'FutureBuilder使用';
var _futureBuilderFuture;
Future _gerData() async {
var response = HttpUtil()
.get('http://api.douban.com/v2/movie/top250', data: {'count': 15});
return response;
}
@override
void initState() {
// TODO: implement initState
super.initState();
///用_futureBuilderFuture來(lái)保存_gerData()的結(jié)果惜互,以避免不必要的ui重繪
_futureBuilderFuture = _gerData();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
title = title + '.';
});
},
child: Icon(Icons.title),
),
body: RefreshIndicator(
onRefresh: _gerData,
child: FutureBuilder(
builder: _buildFuture,
future:
_futureBuilderFuture, // 用戶定義的需要異步執(zhí)行的代碼鲁沥,類型為Future<String>或者null的變量或函數(shù)
),
),
);
}
///snapshot就是_calculation在時(shí)間軸上執(zhí)行過(guò)程的狀態(tài)快照
Widget _buildFuture(BuildContext context, AsyncSnapshot snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
print('還沒(méi)有開(kāi)始網(wǎng)絡(luò)請(qǐng)求');
return Text('還沒(méi)有開(kāi)始網(wǎng)絡(luò)請(qǐng)求');
case ConnectionState.active:
print('active');
return Text('ConnectionState.active');
case ConnectionState.waiting:
print('waiting');
return Center(
child: CircularProgressIndicator(),
);
case ConnectionState.done:
print('done');
if (snapshot.hasError) return Text('Error: ${snapshot.error}');
return _createListView(context, snapshot);
default:
return Text('還沒(méi)有開(kāi)始網(wǎng)絡(luò)請(qǐng)求');
}
}
Widget _createListView(BuildContext context, AsyncSnapshot snapshot) {
List movies = snapshot.data['subjects'];
return ListView.builder(
itemBuilder: (context, index) => _itemBuilder(context, index, movies),
itemCount: movies.length * 2,
);
}
Widget _itemBuilder(BuildContext context, int index, movies) {
if (index.isOdd) {
return Divider();
}
index = index ~/ 2;
return ListTile(
title: Text(movies[index]['title']),
leading: Text(movies[index]['year']),
trailing: Text(movies[index]['original_title']),
);
}
}