本篇文章將介紹從 setState
開始,到 futureBuilder
、 streamBuilder
來優雅的構建你的高質量項目,而不引起 setState
帶來的反作用,如對文章感興趣,請 點擊查看源碼。git
首先,咱們使用基礎的 StatefulWidget
來建立頁面,以下:github
class BaseStatefulDemo extends StatefulWidget { @override _BaseStatefulDemoState createState() => _BaseStatefulDemoState(); } class _BaseStatefulDemoState extends State<BaseStatefulDemo> { @override Widget build(BuildContext context) { return Container(); } }
而後,咱們使用 Future
來建立一些數據,來模擬網絡請求,以下:後端
Future<List<String>> _getListData() async { await Future.delayed(Duration(seconds: 1)); // 1秒以後返回數據 return List<String>.generate(10, (index) => '$index content'); }
在 initState()
方法中調用 _getListData()
來初始化數據,以下:網絡
List<String> _pageData = List<String>(); @override void initState() { _getListData().then((data) => setState(() { _pageData = data; })); super.initState(); }
使用 ListView.builder
來處理這些數據構建UI,以下:app
@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(), ], ); }, ), ); }
最後,咱們就能夠看到界面了 😎 ,如圖:less
固然,你也能夠將 UI 顯示單獨提取成一個方法,方便後期維護,使代碼層次更清晰,以下:async
@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(), ], ); }
繼續,咱們來完善它,正常從後端獲取數據,後端應該會給咱們返回不一樣信息,根據這些信息須要處理不一樣的狀態,如:編輯器
先來處理 BusyState 加載指示器,以下:ide
bool get _fetchingData => _pageData == null; // 判斷數據是否爲空 @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Base Stateful Demo'), ), body: _fetchingData ? Center( child: CircularProgressIndicator( // 加載指示器 valueColor: AlwaysStoppedAnimation<Color>(Colors.yellow), // 設置指示器顏色 backgroundColor: Colors.yellow[100], // 設置背景色 ), ) : ListView.builder( itemCount: _pageData.length, itemBuilder: (buildContext, index) { return getListDataUi(index); }, ), ); }
效果如圖:函數
接着,咱們來處理 ErrorState ,我給 _getListData()
添加 hasError
參數來模擬後端返回的錯誤,以下
Future<List<String>> _getListData({bool hasError = false}) async { await Future.delayed(Duration(seconds: 1)); // 1秒以後返回數據 if (hasError) { return Future.error('獲取數據出現問題,請再試一次'); } return List<String>.generate(10, (index) => '$index content'); }
而後,在 initState()
方法中捕獲異常更新數據,以下:
@override void initState() { _getListData(hasError: true) .then((data) => setState(() { _pageData = data; })) .catchError((error) => setState(() { _pageData = [error]; })); super.initState(); }
效果如圖( 固然這裏可使用一個錯誤頁面來展現 ):
接着,咱們來處理 NoData ,我給 _getListData()
添加 hasData
參數來模擬後端返回空數據,以下:
Future<List<String>> _getListData( {bool hasError = false, bool hasData = true}) async { await Future.delayed(Duration(seconds: 1)); if (hasError) { return Future.error('獲取數據出現問題,請再試一次'); } if (!hasData) { return List<String>(); } return List<String>.generate(10, (index) => '$index content'); }
而後,在 initState()
方法更新數據,以下:
@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()
來更新數據,是否是很簡單,一般狀況下咱們這麼使用是沒什麼問題,可是,若是咱們的頁面足夠複雜,要處理的狀態足夠多,咱們須要使用更多的 setState()
,意味着咱們要更多的代碼來更新數據,並且,咱們每次 setState()
的時候 build()
方法就會從新執行一次( 這就是上文提到的反作用 )。
其實,Flutter 已經提供了更優雅的方式來更新咱們的數據及處理狀態,它就是咱們接下來要介紹的 futureBuilder
。
FutureBuilder
經過 future: 參數能夠接收一個 Future
,而且經過 builder: 參數來構建 UI ,builder: 參數是一個函數,它提供了一個 snapshot
參數裏面帶着咱們須要的狀態和數據。
接下來,咱們將上面的 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 已經給咱們提供好了 error 狀態 return _getInfoMessage(snapshot.error); } if (!snapshot.hasData) { // FutureBuilder 已經給咱們提供好了空數據狀態 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
已經給我處理好了一些基本狀態,如圖
咱們使用 _getInfoMessage()
方法來處理狀態提示,以下:
Widget _getInfoMessage(String msg) { return Center( child: Text(msg), ); }
就這樣咱們不使用任何一個 setState()
就能完成和上面同樣的效果,而且不會產生反作用,是否是很給力 💪。
可是,它並非完美的,好比,咱們想刷新數據,咱們須要從新調用 _getListData()
方法,結果它並無刷新。
StreamBuilder
經過 stream: 參數能夠接收一個 stream
,一樣,經過 builder: 參數來構建 UI ,和 futureBuilder
用法相似,惟一的好處就是,咱們能夠隨意控制 stream
的輸入輸出,添加任何的狀態來更新指定狀態下的 UI 。
首先,咱們使用 enum
來表示咱們的狀態,在文件的頭部添加它,以下:
enum StreamViewState { Busy, DataRetrieved, NoData }
接着,使用 StreamController
建立一個流控制器,把 FutureBuilder
替換成 StreamBuilder
,把 future: 參數 改爲 stream: 參數,以下:
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 來更新數據 if (!snapshot.hasData || StreamViewState.Busy) { return Center( child: CircularProgressIndicator( valueColor: AlwaysStoppedAnimation<Color>(Colors.yellow), backgroundColor: Colors.yellow[100], ), ); } //使用 枚舉的 NoData 來更新數據 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(), ], ); }, ); }, ), ); }
只是新增了枚舉值來判斷是否須要更新數據,其餘基本保持不變。
接下來,我須要修改 _getListData()
方法,使用流控制器添加狀態及數據,以下:
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 數據 } if (!hasData) { return _stateController.add(StreamViewState.NoData); // 往 stream 裏新增無數據狀態 } _listItems = List<String>.generate(10, (index) => '$index content'); _stateController.add(StreamViewState.DataRetrieved); // 往 stream 裏新增數據獲取完成狀態 }
此時咱們並無返回數據,因此咱們須要建立 listItems
存儲數據,而後把 StatelessWidget
改爲 StatefulWidget
,以便咱們根據 stream
的輸出來更新數據,這個轉換很是方便,VS Code 編輯器可使用 Option + Shift + R
(Mac)或者 Ctrl + Shift + R
(Win)快捷鍵 ,Android Studio 使用Option + Enter
快捷鍵,以後在 initState()
方法中初始化數據,以下:
List<String> listItems; @override void initState() { _getListData(); super.initState(); }
到這裏咱們已經解決了 FutureBuilder
的侷限性問題,咱們能夠新增一個 FloatingActionButton
來刷新數據,以下:
@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( ... ), ); }
如今,點擊 FloatingActionButton
加載指示器已經顯示,可是,咱們的 listItems
數據並沒真正的更新,點擊 FloatingActionButton
只是更新的加載狀態而已,並且咱們的業務邏輯代碼和 UI 代碼還在同一個文件中,很顯然,他們已經解耦,因此,咱們能夠繼續完善它,將業務邏輯代碼和 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)); } }
而後,把狀態也封裝成一個文件且將數據和狀態關聯,以下:
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(); // 建立 model @override void initState() { model.dispatch(FetchData(hasData: true)); // 獲取 model 裏的數據 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) { // 經過封裝的狀態類來判斷是否更新UI return Center( child: CircularProgressIndicator( valueColor: AlwaysStoppedAnimation<Color>(Colors.yellow), backgroundColor: Colors.yellow[100], ), ); } if (streamState is DataFetchedState) { // 經過封裝的狀態類來判斷是否更新UI if (!homeState.hasData) { return _getInformationMessage('not found data'); } } return ListView.builder( itemCount: streamState.data.length, // 此時,數據再也不是本地數據,而是從 stream 中輸出的數據 itemBuilder: (buildContext, index) => _getListItem(index, streamState.data), ); }, ), ); } ... }
此時,業務邏輯代碼和 UI 代碼已徹底分離,且可擴展性和維護加強,且咱們的數據和狀態已關聯起來,此時,點擊 FloatingActionButton
效果和上面同樣,且數據已更新。
最後附上個人博客、GitHub地址:
博客地址:https://h.lishaoy.net/futruebuilder-streambuilder
GitHub地址:https://github.com/persilee/flutter_pro