FutureBuilder and StreamBuilder 优雅的构建高质量项目

image

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

基础的setState更新数据

首先,咱们使用基础的 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

no-shadow

固然,你也能够将 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(加载中):咱们在界面上显示一个加载指示器
  • DataFetchedState(数据加载完成):咱们延迟2秒,来模拟数据加载完成
  • ErrorState(错误):显示错误提示
  • NoData(没有数据):请求成功,但没有数据,显示提示

先来处理 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);
              },
            ),
    );
  }

效果如图:函数

no-shadow

接着,咱们来处理 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();
  }

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

no-shadow

接着,咱们来处理 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-shadow

这就是经过 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;

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

分离业务逻辑代码和 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

相关文章
相关标签/搜索