本篇文章将介绍从 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