flutter FutureBuilder的使用以及防止FutureBuilder不必要重绘的两种方法
前言:
我们经常有这样的一个开发场景:一个页面进入之后先进行网络请求,此时显示一个圆圈(等待动画),等网络数据返回时显示一个展示网络数据的布局。例如下图:
我们通常的做法是
if(data==null){ return CircularProgressIndicator(); }else{ return ListView(...); }
大致就是数据返回之前我们加载一个组件,等数据返回值后,我们重绘页面返回另一个组件。
在flutter中,有一个新的实现方式,那就是我们即将要介绍的futureBuilder.
FutureBuilder用法和实现
Widget that builds itself based on the latest snapshot of interaction with a Future.
官方意思是一个基于与Future交互的最新快照构建自己的小部件。
先看一下它的构造方法:
const FutureBuilder({ Key key, this.future, //获取数据的方法 this.initialData, //初始的默认数据 @required this.builder }) : assert(builder != null), super(key: key);
主要看一下builder,这个是我们主要关心的,它是我们构建组件的策略。
接收两个参数:BuildContext context, AsyncSnapshot snapshot.
context就不解释了,snapshot就是_calculation在时间轴上执行过程的状态快照。
//FutureBuilder控件 new FutureBuilder<String>( future: _calculation, // 用户定义的需要异步执行的代码,类型为Future<String>或者null的变量或函数 builder: (BuildContext context, AsyncSnapshot<String> snapshot) { //snapshot就是_calculation在时间轴上执行过程的状态快照 switch (snapshot.connectionState) { case ConnectionState.none: return new Text('Press button to start'); //如果_calculation未执行则提示:请点击开始 case ConnectionState.waiting: return new Text('Awaiting result...'); //如果_calculation正在执行则提示:加载中 default: //如果_calculation执行完毕 if (snapshot.hasError) //若_calculation执行出现异常 return new Text('Error: ${snapshot.error}'); else //若_calculation执行正常完成 return new Text('Result: ${snapshot.data}'); } }, )
FutureBuilder通过子属性future获取用户需要异步处理的代码,用builder回调函数暴露出异步执行过程中的快照。我们通过builder的参数snapshot暴露的快照属性,定义好对应状态下的处理代码,即可实现异步执行时的交互逻辑。
看起来似乎有点绕口,我们看看下面这段代码:
/* * Created by 李卓原 on 2018/9/30. * email: zhuoyuan93@gmail.com * 关于状态改变引起的不必要的页面刷新:https://github.com/flutter/flutter/issues/11426#issuecomment-414047398 */ import 'dart:async'; import 'package:async/async.dart'; import 'package:flutter/material.dart'; import 'package:flutter_app/utils/HttpUtil.dart'; class FutureBuilderPage extends StatefulWidget { @override State<StatefulWidget> createState() => FutureBuilderState(); } class FutureBuilderState extends State<FutureBuilderPage> { String title = 'FutureBuilder使用'; Future _gerData() async { var response = HttpUtil() .get('http://api.douban.com/v2/movie/top250', data: {'count': 15}); return response; } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(title), ), floatingActionButton: FloatingActionButton( onPressed: () { setState(() { title = title + '.'; }); }, child: Icon(Icons.title), ), body: FutureBuilder( builder: _buildFuture, future: _gerData(), // 用户定义的需要异步执行的代码,类型为Future<String>或者null的变量或函数 ), ); } ///snapshot就是_calculation在时间轴上执行过程的状态快照 Widget _buildFuture(BuildContext context, AsyncSnapshot snapshot) { switch (snapshot.connectionState) { case ConnectionState.none: print('还没有开始网络请求'); return Text('还没有开始网络请求'); case ConnectionState.active: print('active'); return Text('ConnectionState.active'); case ConnectionState.waiting: print('waiting'); return Center( child: CircularProgressIndicator(), ); case ConnectionState.done: print('done'); if (snapshot.hasError) return Text('Error: ${snapshot.error}'); return _createListView(context, snapshot); default: return null; } } Widget _createListView(BuildContext context, AsyncSnapshot snapshot) { List movies = snapshot.data['subjects']; return ListView.builder( itemBuilder: (context, index) => _itemBuilder(context, index, movies), itemCount: movies.length * 2, ); } Widget _itemBuilder(BuildContext context, int index, movies) { if (index.isOdd) { return Divider(); } index = index ~/ 2; return ListTile( title: Text(movies[index]['title']), leading: Text(movies[index]['year']), trailing: Text(movies[index]['original_title']), ); } }
在build方法中,我们返回了一个Scaffold,主要的代码在body中,包裹了一个FutureBuilder,
我们在它的builder方法中,对不同状态返回了不同的控件。
snapshot.connectionState就是异步函数_gerData的执行状态,用户通过定义在ConnectionState.none和ConnectionState.waiting状态下,输出一个Text和居中·(Center)·显示并且内置文字CircularProgressIndicator的组件,其意义即:当异步函数_gerData未执行时,屏幕正中央显示文字:还没有开始网络请求。和正在执行时,显示一个刷新状态的控件。
当_gerData执行完毕后,snapshot.connectionState的值即变为ConnectionState.done,此时即可输出根据HTTP请求获取到的数据生成对应的ListItem。由于ConnectionState.done是除了ConnectionState.none和ConnectionState.waiting以外的唯一值,所以代码中在switch下用default也可(ConnectionState.active好像在整个过程中没有调用)。
由于通过FutureBuilder内的builder()函数即可操控控件的状态和重绘,我们不必通过自己写异步状态的判断和多次使用setState()实现页面上加载中和加载完成显示效果的切换,因为FutureBuilder内部自带了执行setState()的方法。
现在一个FutureBuilder的构建就算完成了。
防止FutureBuilder进行不必要的重绘
如果只是写一个FutureBuilder,我们就不需要floatingActionButton里的一系列东西,所以这时候就到它的出场了。
代码中的意思,每次点击它,就在我们标题后面加一个“.” , 看一下效果
确实是改变了标题,但是整个页面也随着setState而进行了不必要的重绘,这就是我们本篇的重点了。
即使AppBar和FutureBuilder没有任何关联,每次我们改变它的值(通过调用setState), FutureBuilder都会再次经历整个生命周期!它重新取代future,导致不必要的流量,并再次显示负载,导致糟糕的用户体验。
这个问题以各种方式表现出来。在某些情况下,它甚至不像上面的例子那么明显。例如:
从当前不在屏幕上的页面生成的网络流量
热重装不能正常工作
更新某些“继承的窗口小部件”中的值时丢失导航器状态
等等…
但是这一切的原因是什么?我们如何解决它?
didUpdateWidget问题
注意:在本节中,我将详细介绍FutureBuilder的工作原理。如果您对此不感兴趣,可以跳到解决方案。
如果我们仔细看看代码FutureBuilder,我们发现它是一个StatefulWidget。我们知道,StatefulWidgets维护一个长期存在的State对象。这种状态有一些管理其生命周期的方法,就像方法initState,build和didUpdateWidget。
initState在第一次创建状态对象时只调用一次,并且build每次我们需要构建要显示的窗口小部件时调用它,但是那是什么didUpdateWidget呢?只要附加到此State对象的窗口小部件发生更改,就会调用此方法。
当使用新输入重建窗口小部件时,将放置旧窗口小部件,并创建新窗口小部件并将其分配给State对象,并didUpdateWidget在重建之前调用它以执行我们想要执行的任何操作。
在FutureBuilder这种情况下,这个方法看起来像这样:
@override void didUpdateWidget(FutureBuilder<T> oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.future != widget.future) { if (_activeCallbackIdentity != null) { _unsubscribe(); _snapshot = _snapshot.inState(ConnectionState.none); } _subscribe(); } }
它基本上是说:如果在重建时,新窗口小部件具有与旧窗口小部件不同的Future实例,则重复所有内容:取消订阅,并再次订阅。
但我们不是提供相同的Future吗?我们称之为同一个功能!好吧,Future的情况不一样了。我们的功能正在完成同样的工作,但随后又回归了一个与旧的不同的新Future。
因此,我们想要做的是在第一次调用时存储或缓存函数的输出,然后在再次调用函数时提供相同的输出。此过程称为记忆(memoization)。
解决方案 1 :Memoize the future
简单来说,Memoization缓存函数的返回值,并在再次调用该函数时重用它。Memoization主要用于函数式语言,其中函数是确定性的(它们总是为相同的输入返回相同的输出),但我们可以在这里使用简单的memoization来解决我们的问题,以确保FutureBuilder始终接收相同的未来实例。
为此,我们将使用Dart的AsyncMemoizer。这个记忆器完全符合我们的要求!它需要一个异步函数,在第一次调用它时调用它,并缓存其结果。对于该函数的所有后续调用,memoizer返回相同的先前计算的未来。
因此,为了解决我们的问题,我们首先在我们的小部件中创建一个AsyncMemoizer实例:
final AsyncMemoizer _memoizer = AsyncMemoizer();
注意:你不应该在StatelessWidget中实例化memoizer,因为Flutter在每次重建时都会处理StatelessWidgets,这基本上可以达到目的。您应该在StatefulWidget中实例化它,或者在它可以持久化的地方实例化它。
之后,我们将修改_fetchData函数以使用该memoizer:
_gerData() { return _memoizer.runOnce(() async { return await HttpUtil() .get('http://api.douban.com/v2/movie/top250', data: {'count': 15}); }); }
我们用AsyncMemoizer.runOnce包装我们的函数,它完全听起来像它的声音;它只运行一次该函数,并在再次调用时返回缓存的Future。
就是这样!我们的FutureBuilder现在只是第一次触发:
现在,我们其他地方进行setState也不会导致FutureBuilder的重绘了。
为了解决这个问题,我们使用Dart的AsyncMemoizer每次都传递相同的Future实例。
解决方法2 在构建函数之外调用Future
问题是每次发布重建时都会调用FutureBuilder状态的didUpdateWidget。此函数检查旧的future对象是否与新的对象不同,如果是,则重新启动FutureBuilder。为了解决这个问题,我们可以在构建函数之外的某个地方调用Future。例如,在initState中,将其保存在成员变量中,并将此变量传递给FutureBuilder。
比如:
var _futureBuilderFuture; ... @override void initState() { ///用_futureBuilderFuture来保存_gerData()的结果,以避免不必要的ui重绘 _futureBuilderFuture = _gerData(); } ... FutureBuilder( future: _futureBuilderFuture , ....
这里使用_futureBuilderFuture来保存_gerData()的结果,这样我们传递给FutureBuilder的是一个成员变量,而不是一个方法就不会多次调用了。
看一下完整代码:
/* * Created by 李卓原 on 2018/9/30. * email: zhuoyuan93@gmail.com * 关于状态改变引起的不必要的页面刷新:https://github.com/flutter/flutter/issues/11426#issuecomment-414047398 */ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_app/utils/HttpUtil.dart'; class FutureBuilderPage extends StatefulWidget { @override State<StatefulWidget> createState() => FutureBuilderState(); } class FutureBuilderState extends State<FutureBuilderPage> { String title = 'FutureBuilder使用'; var _futureBuilderFuture; Future _gerData() async { var response = HttpUtil() .get('http://api.douban.com/v2/movie/top250', data: {'count': 15}); return response; } @override void initState() { // TODO: implement initState super.initState(); ///用_futureBuilderFuture来保存_gerData()的结果,以避免不必要的ui重绘 _futureBuilderFuture = _gerData(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(title), ), floatingActionButton: FloatingActionButton( onPressed: () { setState(() { title = title + '.'; }); }, child: Icon(Icons.title), ), body: RefreshIndicator( onRefresh: _gerData, child: FutureBuilder( builder: _buildFuture, future: _futureBuilderFuture, // 用户定义的需要异步执行的代码,类型为Future<String>或者null的变量或函数 ), ), ); } ///snapshot就是_calculation在时间轴上执行过程的状态快照 Widget _buildFuture(BuildContext context, AsyncSnapshot snapshot) { switch (snapshot.connectionState) { case ConnectionState.none: print('还没有开始网络请求'); return Text('还没有开始网络请求'); case ConnectionState.active: print('active'); return Text('ConnectionState.active'); case ConnectionState.waiting: print('waiting'); return Center( child: CircularProgressIndicator(), ); case ConnectionState.done: print('done'); if (snapshot.hasError) return Text('Error: ${snapshot.error}'); return _createListView(context, snapshot); default: return Text('还没有开始网络请求'); } } Widget _createListView(BuildContext context, AsyncSnapshot snapshot) { List movies = snapshot.data['subjects']; return ListView.builder( itemBuilder: (context, index) => _itemBuilder(context, index, movies), itemCount: movies.length * 2, ); } Widget _itemBuilder(BuildContext context, int index, movies) { if (index.isOdd) { return Divider(); } index = index ~/ 2; return ListTile( title: Text(movies[index]['title']), leading: Text(movies[index]['year']), trailing: Text(movies[index]['original_title']), ); } }
来源:https://blog.csdn.net/u011272795/article/details/83010974