15

优雅的使用 FutureBuilder and StreamBuilder 构建项目

 3 years ago
source link: https://h.lishaoy.net/futruebuilder-streambuilder
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

bINZjuj.png!web

本篇文章将介绍从 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(),
          ],
        );
      },
    ),
  );
}

最后,我们就可以看到界面了 :sunglasses: ,如图:

JfyQzyE.png!web

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

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

效果如图:

ia6niqJ.png!web

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

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

RbiIZvU.png!web

接着,我们来处理 NoData ,我给 _getListData() 添加 hasError 参数来模拟后端,如下:

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

效果如图:

meUFrir.png!web

这就是通过 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) {
            return _getInfoMessage(snapshot.error);
          }

          if (!snapshot.hasData) {
            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(),
                ],
              );
            },
          );
        },
      ),
    );
  }

  ...

我们使用 _getInfoMessage() 方法来处理状态提示,如下:

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

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

但是,它并不是完美的,比如,我们想刷新数据,我们需要重新调用 _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(

        ...
        
      ),
    );
  }

现在,我们的 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 代码已完全分离,且可扩展性和维护增强。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK