【 源码之间 - Flutter 】 FutureBuilder 使用

3,235 阅读3分钟

一、前言:

主要就是请求网络api,返回数据,展业界面。根据不同的状态显示不同的界面。

加载中加载完成加载失败

一、示例demo详述:

1.关于异步请求

FutureBuilder需要一个异步任务作为构造入参
通过wanandroid的开发api进行文章列表的获取, Api.fetch(int page)

class Api {
  static Future<List<Article>> fetch(int page) async {
    var url = 'https://www.wanandroid.com/article/list/$page/json';
    var rep = await http.get(url);
    await Future.delayed(Duration(seconds: 3));
    if (rep.statusCode == 200) {
      var datas = json.decode(rep.body)['data']['datas'] as List;
      return datas.map(Article.formMap).toList();
    }
    return null;
  }
}



文章用三个字段表示

class Article {
  final String title;
  final String url;
  final String time;

  const Article({this.title, this.time, this.url});

  static Article formMap(dynamic json) {
    if (json == null) return null;
    return Article(
      title: json['title'] ?? '未知',
      url: json['link'] ?? '',
      time: json['niceDate'] ?? '',
    );
  }

  @override
  String toString() {
    return 'Article{title: $title, url: $url, time: $time}';
  }
}

2. FutureBuilder的使用

先定义异步任务和当前页码,在使用FutureBuilder进行构造组件。全代码见文尾。

class _HomePageState extends State<HomePage> {
  Future<List<Article>> _articles;
  var _page = 0;

  @override
  void initState() {
    _articles = Api.fetch(_page);
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        //略...
      ),
      body: FutureBuilder<List<Article>>(
        future: _articles,
        builder: _builderList,
      ),
    );
  }


二、FutureBuilder源码分析

1. FutureBuilder组件类
  • FutureBuilder是一个具有泛型T的类,T代表异步的数据类型,这里也就是List<Article>
  • FutureBuilder是一个StatefulWidget,主要有三个成员变量:

1】. futureFuture<T> 类型----待执行的异步任务
2】. builderAsyncWidgetBuilder<T>类型----异步组件构造器
3】. initialDataT 类型----初始数据

class FutureBuilder<T> extends StatefulWidget {
  /// Creates a widget that builds itself based on the latest snapshot of
  /// interaction with a [Future].
  ///
  /// The [builder] must not be null.
  const FutureBuilder({
    Key key,
    this.future,
    this.initialData,
    @required this.builder,
  }) : assert(builder != null),
       super(key: key);

final Future<T> future;
final AsyncWidgetBuilder<T> builder;
final T initialData;

2. _FutureBuilderState状态类
  • StatefulWidget主要依赖State进行构建组件,所以这里重要的是_FutureBuilderState

其中有两个成员变量_activeCallbackIdentity_snapshot

class _FutureBuilderState<T> extends State<FutureBuilder<T>> {

  Object _activeCallbackIdentity;
  AsyncSnapshot<T> _snapshot;

_FutureBuilderState#initState中对_snapshot进行初始化

@override
void initState() {
  super.initState();
  _snapshot = AsyncSnapshot<T>.withData(ConnectionState.none, widget.initialData);
  _subscribe();
}

3. AsyncSnapshot状态量类

所以先看一下_snapshot对象所对应的AsyncSnapshot<T>
它核心是三个成员变量,记录状态、数据和异常情况
并且提供一些命名构造方便创建对象和一些get方法方便使用

final ConnectionState connectionState;  # 连接状态
final T data;  # 数据
final Object error; # 异常

简单来说,就是装着三个东西的瓶子。


  • 还有个比较重要的是连接的状态ConnectionState
enum ConnectionState {
  none, # 初始化时最初
  waiting, # 刚开始执行异步任务时,等待期
  active, # Stream中激活但未结束
  done, # 结束
}

现在回看_FutureBuilderState#initState中对_snapshot进行初始化时:
连接状态是none,数据是提供的初始数据,没有则为null

@override
void initState() {
  super.initState();
  _snapshot = AsyncSnapshot<T>.withData(ConnectionState.none, widget.initialData);
  _subscribe();
}

4. FutureBuilder的核心逻辑
  • _snapshot初始化完成,然后执行_subscribe()这是FutureBuilder的灵魂

如果widget.future非空,会创建callbackIdentity标识,然后启动异步任务
接着将_snapshot的状态设为ConnectionState.waiting

void _subscribe() {
  if (widget.future != null) {
    final Object callbackIdentity = Object();
    _activeCallbackIdentity = callbackIdentity;
    widget.future.then<void>((T data) {
     //昝略...
    }, onError: (Object error) {
     //昝略...
    });
    _snapshot = _snapshot.inState(ConnectionState.waiting);
  }
}

  • initState完成,之后会调用State#build

这里是用来外部传的builder方法来创建组件,其中会回调_snapshot给外界使用
这时_snapshot的状态是waiting;

@override
Widget build(BuildContext context) => widget.builder(context, _snapshot);

在外界处理通过_builderList方法创建组件

body: FutureBuilder<List<Article>>(
  future: _articles,
  builder: _builderList,
),

根据回调的snapshot,你可以决定返回的界面
比如现在是ConnectionState.waiting,就可以返回loading组件

Widget _builderList(
    BuildContext context, AsyncSnapshot<List<Article>> snapshot) {
  switch (snapshot.connectionState) {
    //...其他昝略
    case ConnectionState.waiting: //<--- 当前waiting
      print('-------ConnectionState.waiting---------');
      return _buildLoading();
      break;
  }

}

  • 接下来异步事件完成后,会回调then中的函数,也就是源码中的这里
  • 可以看出回调中会将异步返回的数据放在_snapshot这个瓶子里,并setState
  • 这样_snapshot更新后,会重新执行build方法,又会回调外界的_builderList
void _subscribe() {
  if (widget.future != null) {
    final Object callbackIdentity = Object();
    _activeCallbackIdentity = callbackIdentity;
    widget.future.then<void>((T data) {
      if (_activeCallbackIdentity == callbackIdentity) {
        setState(() {
          _snapshot = AsyncSnapshot<T>.withData(ConnectionState.done, data);
        });
      }
    }, onError: (Object error) {
      if (_activeCallbackIdentity == callbackIdentity) {
        setState(() {
          _snapshot = AsyncSnapshot<T>.withError(ConnectionState.done, error);
        });
      }
    });
    _snapshot = _snapshot.inState(ConnectionState.waiting);
  }
}

这样就会跳到ConnectionState.done 而返回列表组件
当发生异常snapshot.hasError会为true,这样可以构建错误界面

Widget _builderList( BuildContext context, AsyncSnapshot<List<Article>> snapshot) {
  if (snapshot.hasError) {
     return _buildError(snapshot.error);
  }
    
  switch (snapshot.connectionState) {
    //...其他昝略
    case ConnectionState.done:
      print('-------ConnectionState.done---${snapshot.hasData}------'
      if (snapshot.hasData) {
        return _buildList(snapshot.data);
      }
      break;
  }
}

Widget _buildList(List<Article> data) {
  return CupertinoScrollbar(
    child: ListView.separated(
        separatorBuilder: (_, index) => Divider(),
        itemCount: data.length,
        itemBuilder: (_, index) => ListTile(
              title: Text(
                data[index].title,
                maxLines: 1,
                overflow: TextOverflow.ellipsis,
              ),
              subtitle: Text(
                data[index].url,
                style: TextStyle(fontSize: 10),
              ),
              trailing: Text(data[index].time.split(' ')[0]),
            )),
  );
}

所以FutureBuilder的能力就是根据异步任务的执行情况,向外界暴露出状态方便构建不同的界面


5. 父组件刷新时的_FutureBuilderState的行为
  • 在点击加号时,更新异步方法,获取下一页数据,然后父组件执行setState
void _doAdd() {
  setState(() {
    _page++;
    _articles = Api.fetch(_page);
  });
}

此时并不会走State#initState,而是didUpdateWidget
当两个异步任务不同时,则会先取消之前的,然后再执行_subscribe
之后的事就和上面是一样的了。

@override
void didUpdateWidget(FutureBuilder<T> oldWidget) {
  super.didUpdateWidget(oldWidget);
  if (oldWidget.future != widget.future) {
    if (_activeCallbackIdentity != null) {
      _unsubscribe();
      _snapshot = _snapshot.inState(ConnectionState.none);
    }
    _subscribe();
  }
}

取消也很简单,标识置空即可。

void _unsubscribe() {
  _activeCallbackIdentity = null;
}

FutureBuilder的源码也就这些,看到了也就不是很难。说白了就是在封装一下异步任务执行情况,本质也是靠setState进行更新子组件。


尾声

欢迎Star和关注FlutterUnit 的发展,让我们一起携手,成为Unit一员。
另外本人有一个Flutter微信交流群,欢迎小伙伴加入,共同探讨Flutter的问题,期待与你的交流与切磋。

@张风捷特烈 2020.05.10 未允禁转
我的公众号:编程之王
联系我--邮箱:1981462002@qq.com --微信:zdl1994328
~ END ~


附录: demo 源码

1. api.dart
import 'dart:convert';

/// create by 张风捷特烈 on 2020/5/9
/// contact me by email 1981462002@qq.com
/// 说明:
import 'package:http/http.dart' as http;

class Api {
  static Future<List<Article>> fetch(int page) async {
    var url = 'https://www.wanandroid.com/article/list/$page/json';
    var rep = await http.get(url);
    await Future.delayed(Duration(seconds: 3));
    if (rep.statusCode == 200) {
      var datas = json.decode(rep.body)['data']['datas'] as List;
      return datas.map(Article.formMap).toList();
    }
    return null;
  }
}

class Article {
  final String title;
  final String url;
  final String time;

  const Article({this.title, this.time, this.url});

  static Article formMap(dynamic json) {
    if (json == null) return null;
    return Article(
      title: json['title'] ?? '未知',
      url: json['link'] ?? '',
      time: json['niceDate'] ?? '',
    );
  }

  @override
  String toString() {
    return 'Article{title: $title, url: $url, time: $time}';
  }
}

2. main.dart
import 'package:flutter/cupertino.dart';

/// create by 张风捷特烈 on 2020/5/9
/// contact me by email 1981462002@qq.com
/// 说明:

import 'package:flutter/material.dart';
import 'api.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Future Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: HomePage());
  }
}

class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  Future<List<Article>> _articles;
  var _page = 0;

  @override
  void initState() {
    _articles = Api.fetch(_page);
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        onPressed: _doAdd,
        child: Icon(Icons.add),
      ),
      appBar: AppBar(
        title: Text('FutureBuilder    当前页  $_page'),
        actions: <Widget>[
          IconButton(
            icon: Icon(Icons.remove),
            onPressed: _doMinus,
          )
        ],
      ),
      body: FutureBuilder<List<Article>>(
        future: _articles,
        builder: _builderList,
      ),
    );
  }

  Widget _builderList(
      BuildContext context, AsyncSnapshot<List<Article>> snapshot) {
    if (snapshot.hasError) {
      return _buildError(snapshot.error);
    }

    switch (snapshot.connectionState) {
      case ConnectionState.none:
        print('-------ConnectionState.none---------');
        break;
      case ConnectionState.waiting:
        print('-------ConnectionState.waiting---------');
        return _buildLoading();
        break;
      case ConnectionState.active:
        print('-------ConnectionState.active---------');
        break;
      case ConnectionState.done:
        print('-------ConnectionState.done---${snapshot.hasData}------');
        if (snapshot.hasData) {
          return _buildList(snapshot.data);
        }
        break;
    }
    return Container(
      color: Colors.orange,
    );
  }

  Widget _buildLoading() => Center(
        child: CircularProgressIndicator(),
      );

  Widget _buildList(List<Article> data) {
    return CupertinoScrollbar(
      child: ListView.separated(
          separatorBuilder: (_, index) => Divider(),
          itemCount: data.length,
          itemBuilder: (_, index) => ListTile(
                title: Text(
                  data[index].title,
                  maxLines: 1,
                  overflow: TextOverflow.ellipsis,
                ),
                subtitle: Text(
                  data[index].url,
                  style: TextStyle(fontSize: 10),
                ),
                trailing: Text(data[index].time.split(' ')[0]),
              )),
    );
  }

  void _doAdd() {
    setState(() {
      _page++;
      _articles = Api.fetch(_page);
    });
  }

  void _doMinus() {
    if (_page <= 0) {
      return;
    }

    setState(() {
      _page--;
      _articles = Api.fetch(_page);
    });
  }

  Widget _buildError(Object error) => Center(
        child: Padding(
          padding: const EdgeInsets.all(28.0),
          child: Text(
            'A Error: ${error.toString()}',
            style: TextStyle(color: Colors.red, fontSize: 20),
          ),
        ),
      );
}