
本篇文章將介紹從 setState 開始,到 futureBuilder 、 streamBuilder 來優(yōu)雅的構(gòu)建你的高質(zhì)量項目,而不引發(fā) setState 帶來的副作用,如對文章感興趣,請 點擊查看源碼。
基礎(chǔ)的setState更新數(shù)據(jù)
首先,我們使用基礎(chǔ)的 StatefulWidget 來創(chuàng)建頁面,如下:
class BaseStatefulDemo extends StatefulWidget {
@override
_BaseStatefulDemoState createState() => _BaseStatefulDemoState();
}
class _BaseStatefulDemoState extends State<BaseStatefulDemo> {
@override
Widget build(BuildContext context) {
return Container();
}
}
然后,我們使用 Future 來創(chuàng)建一些數(shù)據(jù),來模擬網(wǎng)絡(luò)請求,如下:
Future<List<String>> _getListData() async {
await Future.delayed(Duration(seconds: 1)); // 1秒之后返回數(shù)據(jù)
return List<String>.generate(10, (index) => '$index content');
}
在 initState() 方法中調(diào)用 _getListData() 來初始化數(shù)據(jù),如下:
List<String> _pageData = List<String>();
@override
void initState() {
_getListData().then((data) => setState(() {
_pageData = data;
}));
super.initState();
}
使用 ListView.builder 來處理這些數(shù)據(jù)構(gòu)建UI,如下:
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Base Stateful Demo'),
),
body: ListView.builder(
itemCount: _pageData.length,
itemBuilder: (buildContext, index) {
return Column(
children: <Widget>[
ListTile(
title: Text(_pageData[index]),
),
Divider(),
],
);
},
),
);
}
最后,我們就可以看到界面了 ?? ,如圖:

當(dāng)然,你也可以將 UI 顯示單獨提取成一個方法,方便后期維護,使代碼層次更清晰,如下:
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Base Stateful Demo'),
),
body: ListView.builder(
itemCount: _pageData.length,
itemBuilder: (buildContext, index) {
return getListDataUi(int index);
},
),
);
}
Widget getListDataUi(int index) {
return Column(
children: <Widget>[
ListTile(
title: Text(_pageData[index]),
),
Divider(),
],
);
}
繼續(xù),我們來完善它,正常從后端獲取數(shù)據(jù),后端應(yīng)該會給我們返回不同信息,根據(jù)這些信息需要處理不同的狀態(tài),如:
- BusyState(加載中):我們在界面上顯示一個加載指示器
- DataFetchedState(數(shù)據(jù)加載完成):我們延遲2秒,來模擬數(shù)據(jù)加載完成
- ErrorState(錯誤):顯示錯誤提示
- NoData(沒有數(shù)據(jù)):請求成功,但沒有數(shù)據(jù),顯示提示
先來處理 BusyState 加載指示器,如下:
bool get _fetchingData => _pageData == null; // 判斷數(shù)據(jù)是否為空
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Base Stateful Demo'),
),
body: _fetchingData
? Center(
child: CircularProgressIndicator( // 加載指示器
valueColor: AlwaysStoppedAnimation<Color>(Colors.yellow), // 設(shè)置指示器顏色
backgroundColor: Colors.yellow[100], // 設(shè)置背景色
),
)
: ListView.builder(
itemCount: _pageData.length,
itemBuilder: (buildContext, index) {
return getListDataUi(index);
},
),
);
}
效果如圖:

接著,我們來處理 ErrorState ,我給 _getListData() 添加 hasError 參數(shù)來模擬后端返回的錯誤,如下
Future<List<String>> _getListData({bool hasError = false}) async {
await Future.delayed(Duration(seconds: 1)); // 1秒之后返回數(shù)據(jù)
if (hasError) {
return Future.error('獲取數(shù)據(jù)出現(xiàn)問題,請再試一次');
}
return List<String>.generate(10, (index) => '$index content');
}
然后,在 initState() 方法中捕獲異常更新數(shù)據(jù),如下:
@override
void initState() {
_getListData(hasError: true)
.then((data) => setState(() {
_pageData = data;
}))
.catchError((error) => setState(() {
_pageData = [error];
}));
super.initState();
}
效果如圖( 當(dāng)然這里可以使用一個錯誤頁面來展示 ):

接著,我們來處理 NoData ,我給 _getListData() 添加 hasData 參數(shù)來模擬后端返回空數(shù)據(jù),如下:
Future<List<String>> _getListData(
{bool hasError = false, bool hasData = true}) async {
await Future.delayed(Duration(seconds: 1));
if (hasError) {
return Future.error('獲取數(shù)據(jù)出現(xiàn)問題,請再試一次');
}
if (!hasData) {
return List<String>();
}
return List<String>.generate(10, (index) => '$index content');
}
然后,在 initState() 方法更新數(shù)據(jù),如下:
@override
void initState() {
_getListData(hasError: false, hasData: false)
.then((data) => setState(() {
if (data.length == 0) {
data.add('No data fount');
}
_pageData = data;
}))
.catchError((error) => setState(() {
_pageData = [error];
}));
super.initState();
}
效果如圖:

這就是通過 setState() 來更新數(shù)據(jù),是不是很簡單,通常情況下我們這么使用是沒什么問題,但是,如果我們的頁面足夠復(fù)雜,要處理的狀態(tài)足夠多,我們需要使用更多的 setState() ,意味著我們要更多的代碼來更新數(shù)據(jù),而且,我們每次 setState() 的時候 build() 方法就會重新執(zhí)行一次( 這就是上文提到的副作用 )。
其實,Flutter 已經(jīng)提供了更優(yōu)雅的方式來更新我們的數(shù)據(jù)及處理狀態(tài),它就是我們接下來要介紹的 futureBuilder。
FutureBuilder
FutureBuilder 通過 future: 參數(shù)可以接收一個 Future ,并且通過 builder: 參數(shù)來構(gòu)建 UI ,builder: 參數(shù)是一個函數(shù),它提供了一個 snapshot 參數(shù)里面帶著我們需要的狀態(tài)和數(shù)據(jù)。
接下來,我們將上面的 StatefulWidget 改成 StatelessWidget ,并使用 FutureBuilder 替換,如下:
class FutureBuilderDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Future Builder Demo'),
),
body: FutureBuilder(
future: _getListData(),
builder: (buildContext, snapshot) {
if (snapshot.hasError) { // FutureBuilder 已經(jīng)給我們提供好了 error 狀態(tài)
return _getInfoMessage(snapshot.error);
}
if (!snapshot.hasData) { // FutureBuilder 已經(jīng)給我們提供好了空數(shù)據(jù)狀態(tài)
return Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Colors.yellow),
backgroundColor: Colors.yellow[100],
),
);
}
var listData = snapshot.data;
if (listData.length == 0) {
return _getInfoMessage('No data found');
}
return ListView.builder(
itemCount: listData.length,
itemBuilder: (buildContext, index) {
return Column(
children: <Widget>[
ListTile(
title: Text(listData[index]),
),
Divider(),
],
);
},
);
},
),
);
}
...
通過查看源碼,我們可以了解的 FutureBuilder 已經(jīng)給我處理好了一些基本狀態(tài),如圖

我們使用 _getInfoMessage() 方法來處理狀態(tài)提示,如下:
Widget _getInfoMessage(String msg) {
return Center(
child: Text(msg),
);
}
就這樣我們不使用任何一個 setState() 就能完成和上面一樣的效果,并且不會產(chǎn)生副作用,是不是很給力 ??。
但是,它并不是完美的,比如,我們想刷新數(shù)據(jù),我們需要重新調(diào)用 _getListData() 方法,結(jié)果它并沒有刷新。
StreamBuilder
StreamBuilder 通過 stream: 參數(shù)可以接收一個 stream ,同樣,通過 builder: 參數(shù)來構(gòu)建 UI ,和 futureBuilder 用法類似,唯一的好處就是,我們可以隨意控制 stream 的輸入輸出,添加任何的狀態(tài)來更新指定狀態(tài)下的 UI 。
首先,我們使用 enum 來表示我們的狀態(tài),在文件的頭部添加它,如下:
enum StreamViewState { Busy, DataRetrieved, NoData }
接著,使用 StreamController 創(chuàng)建一個流控制器,把 FutureBuilder 替換成 StreamBuilder ,把 future: 參數(shù) 改成 stream: 參數(shù),如下:
final StreamController<StreamDemoState> _stateController = StreamController<StreamDemoState>();
@override
Widget build(BuildContext context) {
return Scaffold(
...
body: StreamBuilder(
stream: model.homeState,
builder: (buildContext, snapshot) {
if (snapshot.hasError) {
return _getInfoMessage(snapshot.error);
}
// 使用 枚舉的 Busy 來更新數(shù)據(jù)
if (!snapshot.hasData || StreamViewState.Busy) {
return Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Colors.yellow),
backgroundColor: Colors.yellow[100],
),
);
}
//使用 枚舉的 NoData 來更新數(shù)據(jù)
if (listItems.length == StreamViewState.NoData) {
return _getInfoMessage('No data found');
}
return ListView.builder(
itemCount: listItems.length,
itemBuilder: (buildContext, index) {
return Column(
children: <Widget>[
ListTile(
title: Text(listItems[index]),
),
Divider(),
],
);
},
);
},
),
);
}
只是新增了枚舉值來判斷是否需要更新數(shù)據(jù),其他基本保持不變。
接下來,我需要修改 _getListData() 方法,使用流控制器添加狀態(tài)及數(shù)據(jù),如下:
Future _getListData({bool hasError = false, bool hasData = true}) async {
_stateController.add(StreamViewState.Busy);
await Future.delayed(Duration(seconds: 2));
if (hasError) {
return _stateController.addError('error'); // 往 stream 里新增 error 數(shù)據(jù)
}
if (!hasData) {
return _stateController.add(StreamViewState.NoData); // 往 stream 里新增無數(shù)據(jù)狀態(tài)
}
_listItems = List<String>.generate(10, (index) => '$index content');
_stateController.add(StreamViewState.DataRetrieved); // 往 stream 里新增數(shù)據(jù)獲取完成狀態(tài)
}
此時我們并沒有返回數(shù)據(jù),所以我們需要創(chuàng)建 listItems 存儲數(shù)據(jù),然后把 StatelessWidget 改成 StatefulWidget ,以便我們根據(jù) stream 的輸出來更新數(shù)據(jù),這個轉(zhuǎn)換非常方便,VS Code 編輯器可以使用 Option + Shift + R (Mac)或者 Ctrl + Shift + R (Win)快捷鍵 ,Android Studio 使用Option + Enter 快捷鍵,之后在 initState() 方法中初始化數(shù)據(jù),如下:
List<String> listItems;
@override
void initState() {
_getListData();
super.initState();
}
到這里我們已經(jīng)解決了 FutureBuilder 的局限性問題,我們可以新增一個 FloatingActionButton 來刷新數(shù)據(jù),如下:
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Stream Builder Demo'),
),
floatingActionButton: FloatingActionButton(
backgroundColor: Colors.yellow,
child: Icon(
Icons.cached,
color: Colors.black87,
),
onPressed: () {
model.dispatch(FetchData());
},
),
body: StreamBuilder(
...
),
);
}
現(xiàn)在,點擊 FloatingActionButton 加載指示器已經(jīng)顯示,但是,我們的 listItems 數(shù)據(jù)并沒真正的更新,點擊 FloatingActionButton 只是更新的加載狀態(tài)而已,而且我們的業(yè)務(wù)邏輯代碼和 UI 代碼還在同一個文件中,很顯然,他們已經(jīng)解耦,所以,我們可以繼續(xù)完善它,將業(yè)務(wù)邏輯代碼和 UI 代碼分離出來。
分離業(yè)務(wù)邏輯代碼和 UI 代碼
我們可以把處理 stream 的代碼抽離成一個類,如下:
import 'dart:async';
import 'dart:math';
import 'package:pro_flutter/demo/stream_demo/stream_demo_event.dart';
import 'package:pro_flutter/demo/stream_demo/stream_demo_state.dart';
enum StreamViewState { Busy, DataRetrieved, NoData }
class StreamDemoModel {
final StreamController<StreamDemoState> _stateController = StreamController<StreamDemoState>();
List<String> _listItems;
Stream<StreamDemoState> get streamState => _stateController.stream;
void dispatch(StreamDemoEvent event){
print('Event dispatched: $event');
if(event is FetchData) {
_getListData(hasData: event.hasData, hasError: event.hasError);
}
}
Future _getListData({bool hasError = false, bool hasData = true}) async {
_stateController.add(BusyState());
await Future.delayed(Duration(seconds: 2));
if (hasError) {
return _stateController.addError('error');
}
if (!hasData) {
return _stateController.add(DataFetchedState(data: List<String>()));
}
_listItems = List<String>.generate(10, (index) => '$index content');
_stateController.add(DataFetchedState(data: _listItems));
}
}
然后,把狀態(tài)也封裝成一個文件且將數(shù)據(jù)和狀態(tài)關(guān)聯(lián),如下:
class StreamDemoState{}
class InitializedState extends StreamDemoState {}
class DataFetchedState extends StreamDemoState {
final List<String> data;
DataFetchedState({this.data});
bool get hasData => data.length > 0;
}
class ErrorState extends StreamDemoState{}
class BusyState extends StreamDemoState{}
再封裝一個事件文件,如下:
class StreamDemoEvent{}
class FetchData extends StreamDemoEvent{
final bool hasError;
final bool hasData;
FetchData({this.hasError = false, this.hasData = true});
@override
String toString() {
return 'FetchData { hasError: $hasError, hasData: $hasData }';
}
}
最后,我們 UI 部分的代碼如下:
class _StreamBuilderDemoState extends State<StreamBuilderDemo> {
final model = StreamDemoModel(); // 創(chuàng)建 model
@override
void initState() {
model.dispatch(FetchData(hasData: true)); // 獲取 model 里的數(shù)據(jù)
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
...
body: StreamBuilder(
stream: model.streamState,
builder: (buildContext, snapshot) {
if (snapshot.hasError) {
return _getInformationMessage(snapshot.error);
}
var streamState = snapshot.data;
if (!snapshot.hasData || streamState is BusyState) { // 通過封裝的狀態(tài)類來判斷是否更新UI
return Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Colors.yellow),
backgroundColor: Colors.yellow[100],
),
);
}
if (streamState is DataFetchedState) { // 通過封裝的狀態(tài)類來判斷是否更新UI
if (!homeState.hasData) {
return _getInformationMessage('not found data');
}
}
return ListView.builder(
itemCount: streamState.data.length, // 此時,數(shù)據(jù)不再是本地數(shù)據(jù),而是從 stream 中輸出的數(shù)據(jù)
itemBuilder: (buildContext, index) =>
_getListItem(index, streamState.data),
);
},
),
);
}
...
}
此時,業(yè)務(wù)邏輯代碼和 UI 代碼已完全分離,且可擴展性和維護增強,且我們的數(shù)據(jù)和狀態(tài)已關(guān)聯(lián)起來,此時,點擊 FloatingActionButton 效果和上面一樣,且數(shù)據(jù)已更新。
最后附上博客、GitHub地址:
博客地址:https://h.lishaoy.net/futruebuilder-streambuilder
GitHub地址:https://github.com/persilee/flutter_pro