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(
  future: _calculation, // 用户定义的需要异步执行的代码,类型为Future或者null的变量或函数
  builder: (BuildContext context, AsyncSnapshot 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: [email protected]
 * 关于状态改变引起的不必要的页面刷新: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 createState() => FutureBuilderState();
}

class FutureBuilderState extends State {
  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或者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.noneConnectionState.waiting状态下,输出一个Text和居中·(Center)·显示并且内置文字CircularProgressIndicator的组件,其意义即:当异步函数_gerData未执行时,屏幕正中央显示文字:还没有开始网络请求。和正在执行时,显示一个刷新状态的控件。

_gerData执行完毕后,snapshot.connectionState的值即变为ConnectionState.done,此时即可输出根据HTTP请求获取到的数据生成对应的ListItem。由于ConnectionState.done是除了ConnectionState.noneConnectionState.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对象。这种状态有一些管理其生命周期的方法,就像方法initStatebuilddidUpdateWidget

initState在第一次创建状态对象时只调用一次,并且build每次我们需要构建要显示的窗口小部件时调用它,但是那是什么didUpdateWidget呢?只要附加到此State对象的窗口小部件发生更改,就会调用此方法。
当使用新输入重建窗口小部件时,将放置旧窗口小部件,并创建新窗口小部件并将其分配给State对象,并didUpdateWidget在重建之前调用它以执行我们想要执行的任何操作。

FutureBuilder这种情况下,这个方法看起来像这样:

@override
void didUpdateWidget(FutureBuilder 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始终接收相同的未来实例。

为此,我们将使用DartAsyncMemoizer。这个记忆器完全符合我们的要求!它需要一个异步函数,在第一次调用它时调用它,并缓存其结果。对于该函数的所有后续调用,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: [email protected]
* 关于状态改变引起的不必要的页面刷新: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 createState() => FutureBuilderState();
}

class FutureBuilderState extends State {
 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或者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']),
   );
 }
}

你可能感兴趣的:(flutter FutureBuilder的使用以及防止FutureBuilder不必要重绘的两种方法)