FutureBuilder and StreamBuilder 优雅的构建高质量项目
来源:     阅读:546
依创模板店
发布于 2020-11-08 03:19
查看主页
cover

本篇文章将详情从 setState 开始,到 futureBuilderstreamBuilder 来优雅的构建你的高质量项目,而不引发 setState 带来的反作用,如对文章感兴趣,请 点击查看源码。

基础的setState升级数据

首先,我们使用基础的 StatefulWidget 来创立页面,如下:

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,如下:

  @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(),            ],          );        },      ),    );  }

最后,我们即可以看到界面了 😎 ,如图:

list-data

当然,你也可以将 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(),                ],              );  }

继续,我们来完善它,正常从后台获取数据,后台应该会给我们返回不同信息,根据这些信息需要解决不同的状态,如:

先来解决 BusyState 加载指示器,如下:

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);              },            ),    );  }

效果如图:

indicator

接着,我们来解决 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();  }

效果如图( 当然这里可以使用一个错误页面来展现 ):

error

接着,我们来解决 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();  }

效果如图:

no-data

这就是通过 setState() 来升级数据,是不是很简单,通常情况下我们这么使用是没什么问题,但是,假如我们的页面足够复杂,要解决的状态足够多,我们需要使用更多的 setState() ,意味着我们要更多的代码来升级数据,而且,我们每次 setState() 的时候 build() 方法就会重新执行一次( 这就是上文提到的反作用 )。

其实,Flutter 已经提供了更优雅的方式来升级我们的数据及解决状态,它就是我们接下来要详情的 futureBuilder

FutureBuilder

FutureBuilder 通过 future: 参数可以接收一个 Future ,并且通过 builder: 参数来构建 UIbuilder: 参数是一个函数,它提供了一个 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 已经给我解决好了少量基本状态,如图

snapshot

我们使用 _getInfoMessage() 方法来解决状态提醒,如下:

  Widget _getInfoMessage(String msg) {    return Center(      child: Text(msg),    );  }

就这样我们不使用任何一个 setState() 就能完成和上面一样的效果,并且不会产生反作用,是不是很给力 💪。

但是,它并不是完美的,比方,我们想刷新数据,我们需要重新调用 _getListData() 方法,结果它并没有刷新。

StreamBuilder

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;@overridevoid 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 代码分离出来。

分离业务逻辑代码和 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地址: persilee/flutter_pro

免责声明:本文为用户发表,不代表网站立场,仅供参考,不构成引导等用途。 系统环境 windows
相关推荐
2019-08-06
TensorFlow工具快速入门教程5 TensorFlow简介
Video.js 5.x 解决rtmp视频流
Android 图片数据库那些事儿
Apache Gremlin电影推荐图数据
首页
搜索
订单
购物车
我的