本篇文章将介绍从 setState
开始,到 futureBuilder
、 streamBuilder
来优雅的构建你的高质量项目,而不引发 setState
带来的副作用,如对文章感兴趣,请 点击查看源码。
基础的setState更新数据
首先,我们使用基础的 StatefulWidget
来创建页面,如下:
1 | class BaseStatefulDemo extends StatefulWidget { |
然后,我们使用 Future
来创建一些数据,来模拟网络请求,如下:
1 | Future<List<String>> _getListData() async { |
在 initState()
方法中调用 _getListData()
来初始化数据,如下:
1 | List<String> _pageData = List<String>(); |
使用 ListView.builder
来处理这些数据构建UI,如下:
1 |
|
最后,我们就可以看到界面了 😎 ,如图:
当然,你也可以将 UI 显示单独提取成一个方法,方便后期维护,使代码层次更清晰,如下:
1 |
|
继续,我们来完善它,正常从后端获取数据,后端应该会给我们返回不同信息,根据这些信息需要处理不同的状态,如:
- BusyState(加载中):我们在界面上显示一个加载指示器
- DataFetchedState(数据加载完成):我们延迟2秒,来模拟数据加载完成
- ErrorState(错误):显示错误提示
- NoData(没有数据):请求成功,但没有数据,显示提示
先来处理 BusyState 加载指示器,如下:
1 | bool get _fetchingData => _pageData == null; // 判断数据是否为空 |
效果如图:
接着,我们来处理 ErrorState ,我给 _getListData()
添加 hasError
参数来模拟后端返回的错误,如下
1 | Future<List<String>> _getListData({bool hasError = false}) async { |
然后,在 initState()
方法中捕获异常更新数据,如下:
1 |
|
效果如图( 当然这里可以使用一个错误页面来展示 ):
接着,我们来处理 NoData ,我给 _getListData()
添加 hasData
参数来模拟后端返回空数据,如下:
1 | Future<List<String>> _getListData( |
然后,在 initState()
方法更新数据,如下:
1 |
|
效果如图:
这就是通过 setState()
来更新数据,是不是很简单,通常情况下我们这么使用是没什么问题,但是,如果我们的页面足够复杂,要处理的状态足够多,我们需要使用更多的 setState()
,意味着我们要更多的代码来更新数据,而且,我们每次 setState()
的时候 build()
方法就会重新执行一次( 这就是上文提到的副作用 )。
其实,Flutter 已经提供了更优雅的方式来更新我们的数据及处理状态,它就是我们接下来要介绍的 futureBuilder
。
FutureBuilder
FutureBuilder
通过 future: 参数可以接收一个 Future
,并且通过 builder: 参数来构建 UI ,builder: 参数是一个函数,它提供了一个 snapshot
参数里面带着我们需要的状态和数据。
接下来,我们将上面的 StatefulWidget
改成 StatelessWidget
,并使用 FutureBuilder
替换,如下:
1 | class FutureBuilderDemo extends StatelessWidget { |
通过查看源码,我们可以了解的 FutureBuilder
已经给我处理好了一些基本状态,如图
我们使用 _getInfoMessage()
方法来处理状态提示,如下:
1 | Widget _getInfoMessage(String msg) { |
就这样我们不使用任何一个 setState()
就能完成和上面一样的效果,并且不会产生副作用,是不是很给力 💪。
但是,它并不是完美的,比如,我们想刷新数据,我们需要重新调用 _getListData()
方法,结果它并没有刷新。
StreamBuilder
StreamBuilder
通过 stream: 参数可以接收一个 stream
,同样,通过 builder: 参数来构建 UI ,和 futureBuilder
用法类似,唯一的好处就是,我们可以随意控制 stream
的输入输出,添加任何的状态来更新指定状态下的 UI 。
首先,我们使用 enum
来表示我们的状态,在文件的头部添加它,如下:
1 | enum StreamViewState { Busy, DataRetrieved, NoData } |
接着,使用 StreamController
创建一个流控制器,把 FutureBuilder
替换成 StreamBuilder
,把 future: 参数 改成 stream: 参数,如下:
1 |
|
只是新增了枚举值来判断是否需要更新数据,其他基本保持不变。
接下来,我需要修改 _getListData()
方法,使用流控制器添加状态及数据,如下:
1 | Future _getListData({bool hasError = false, bool hasData = true}) async { |
此时我们并没有返回数据,所以我们需要创建 listItems
存储数据,然后把 StatelessWidget
改成 StatefulWidget
,以便我们根据 stream
的输出来更新数据,这个转换非常方便,VS Code 编辑器可以使用 Option + Shift + R
(Mac)或者 Ctrl + Shift + R
(Win)快捷键 ,Android Studio 使用Option + Enter
快捷键,之后在 initState()
方法中初始化数据,如下:
1 | List<String> listItems; |
到这里我们已经解决了 FutureBuilder
的局限性问题,我们可以新增一个 FloatingActionButton
来刷新数据,如下:
1 |
|
现在,点击 FloatingActionButton
加载指示器已经显示,但是,我们的 listItems
数据并没真正的更新,点击 FloatingActionButton
只是更新的加载状态而已,而且我们的业务逻辑代码和 UI 代码还在同一个文件中,很显然,他们已经解耦,所以,我们可以继续完善它,将业务逻辑代码和 UI 代码分离出来。
分离业务逻辑代码和 UI 代码
我们可以把处理 stream
的代码抽离成一个类,如下:
1 | import 'dart:async'; |
然后,把状态也封装成一个文件且将数据和状态关联,如下:
1 | class StreamDemoState{} |
再封装一个事件文件,如下:
1 | class StreamDemoEvent{} |
最后,我们 UI 部分的代码如下:
1 | class _StreamBuilderDemoState extends State<StreamBuilderDemo> { |
此时,业务逻辑代码和 UI 代码已完全分离,且可扩展性和维护增强,且我们的数据和状态已关联起来,此时,点击 FloatingActionButton
效果和上面一样,且数据已更新。