Flutter | 定义一个通用的多功能网络请求 Widget

2019-10-0907:01:32APP与小程序开发Comments3,258 views1字数 4518阅读模式

说起网络请求的控件,我们首先是不是会想起 FutureBuilder文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16806.html

FutureBuilder 给我们封装好了网络请求中的各种状态。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16806.html

本篇文章中只是提供一种思路,欢迎一起探讨,也欢迎不吝赐教!文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16806.html

效果如下。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16806.html

首先是没有开启服务的情况:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16806.html

Flutter | 定义一个通用的多功能网络请求 Widget

可以看到全部都是错误的信息,文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16806.html

然后开启服务:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16806.html

Flutter | 定义一个通用的多功能网络请求 Widget

1. 先定义一个通用的网络请求

那既然是网络请求,那首先我们要定义一个通用的网络请求方法。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16806.html

每一家后台 API 的风格都不一样,有的是 RSETful,有的是我们最熟悉的 GET、POST。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16806.html

这里就以 GET 为例,API 接口为 GitHub - 网易云音乐 Node.js API service。 文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16806.html

网络请求使用的是 Dio,先创建一个 NetUtils.dart文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16806.html

初始化代码:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16806.html

static Dio _dio;

static void init() async {
  Directory tempDir = await getTemporaryDirectory();
  String tempPath = tempDir.path;
  CookieJar cj = PersistCookieJar(dir: tempPath);
  _dio = Dio(BaseOptions(baseUrl: 'http://127.0.0.1:3000'))
    ..interceptors.add(CookieManager(cj))
    ..interceptors.add(LogInterceptor(responseBody: true, requestBody: true));
}
复制代码

在 runApp 前面调用即完成初始化。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16806.html

接着定义一个通用的网络请求:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16806.html

static Future<Response> _get(
  BuildContext context,
  String url, {
    Map<String, dynamic> params,
  }) async {
  Loading.showLoading(context);
  try {
    return await _dio.get(url, queryParameters: params);
  } on DioError catch (e) {
    if (e.response is Map) {
      return Future.value(e.response);
    } else {
      return Future.error(0);
    }
  } finally {
    Loading.hideLoading(context);
  }
}
复制代码

这里代码很简单,方法需要传入三个参数:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16806.html

  1. context:用于 showLoading
  2. url:API 地址
  3. params:该网络请求的参数,可以为空

方法内部我们捕获了 DioError,然后判断接口是否还返回了正常的内容。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16806.html

例如:状态码不为2xx,但是仍然返回了数据,这样 Dio 是会抛出 DioError 的,需要我们自己捕获来处理。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16806.html

如果返回了正常的数据,那我们还是返回回去,如果不是正常的数据,则直接抛出 Future.error(0)文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16806.html

使用该通用方法:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16806.html

/// 新碟上架
static Future<AlbumData> getAlbumData(
  BuildContext context, {
    Map<String, dynamic> params = const {
      'offset': 0,
      'limit': 10,
    },
  }) async {
  var response = await _get(context, '/top/album', params: params);
  return AlbumData.fromJson(response.data);
}
复制代码

我们就可以像这样来使用刚才定义好的方法,也方便我们后续定义一个通用的 FutureBuilder文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16806.html

2. 确认网络请求控件所需要的功能

我们从最开始的图中明显能看出来的,其实是有三个功能:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16806.html

  1. 请求数据并显示 Loading
  2. 正常时返回正常数据,错误时返回错误 Widget
  3. 错误 Widget 可以点击重新请求

然鹅,细心的同学也发现问题了。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16806.html

我们在网络请求中添加了一个 Loading,而且需要一个 BuildContext。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16806.html

我们都知道,是不能在 initState() 方法中去使用这个 BuildContext 的。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16806.html

所以,我们还要进行一个 第一帧回调文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16806.html

@override
void initState() {
  super.initState();
  WidgetsBinding.instance.addPostFrameCallback((call) {
    _request();
  });
}
复制代码

这样就完成了我们的需求调研。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16806.html

3. 编写通用网络请求控件

说的是一个通用的网络请求控件,其实就是把 FutureBuilder 封装一层。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16806.html

请求数据并显示 Loading

但是,这里也有一个问题:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16806.html

我们在最开始定义网络请求工具类的时候,每一个网络请求都是一个方法,而每个方法中都有或者没有参数。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16806.html

我们也知道,FutureBuilder 需要传入一个 Future,那这可怎么办?文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16806.html

而且我们不能在使用该控件的时候调用网络请求方法,因为网络请求中封装了一个 Loading,这个 Loading 需要 BuildContext文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16806.html

既然如此,那我们只能传入方法(Function)了:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16806.html

typedef ValueWidgetBuilder<T> = Widget Function(
  BuildContext context,
  T value,
);


final ValueWidgetBuilder<T> builder;
final Function futureFunc;
final Map<String, dynamic> params;

CustomFutureBuilder({
  @required this.futureFunc,
  @required this.builder,
  this.params,
});
复制代码

这样,我们就可以在 第一帧回调 中来调用该网络请求了,这样一举两得:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16806.html

既不用在使用该控件的时候调用方法,又避免了 Loading 使用 BuildContext 报错的问题。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16806.html

Future<T> _future;

@override
void initState() {
  super.initState();
  WidgetsBinding.instance.addPostFrameCallback((call) {
    _request();
  });
}

void _request() {
  setState(() {
    if (widget.params == null)
      _future = widget.futureFunc(context);
    else
      _future = widget.futureFunc(context, params: widget.params);
  });
}
复制代码

首先我们定义了一个 Future,然后在 第一帧回调 中初始化该 Future 就可以了。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16806.html

正常时返回正常数据,错误时返回错误 Widget

这就需要我们封装好的网络请求和 FutureBuilder 有一个互动了,文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16806.html

网络请求的逻辑如下:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16806.html

Flutter | 定义一个通用的多功能网络请求 Widget

这样正好就可以对应 FutureBuilder 的几种状态:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16806.html

  1. 网络请求 -> ConnectionState.noneConnectionState.waiting
  2. 显示Loading -> ConnectionState.active
  3. 请求结束 -> ConnectionState.done
  4. 是否有数据(无论对错)-> snapshot.hasData
  5. 抛出错误 -> snapshot.hasError

了解这些之后,我们就可以写出代码:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16806.html

  Widget build(BuildContext context) {
    return _future == null
        ? Container(
            alignment: Alignment.center,
            height: ScreenUtil().setWidth(200),
            child: CupertinoActivityIndicator(),
          )
        : FutureBuilder(
            future: _future,
            builder: (context, snapshot) {
              switch (snapshot.connectionState) {
                case ConnectionState.none:
                case ConnectionState.waiting:
                case ConnectionState.active:
                  return Container(
                    alignment: Alignment.center,
                    height: ScreenUtil().setWidth(200),
                    child: CupertinoActivityIndicator(),
                  );
                case ConnectionState.done:
                  if (snapshot.hasData) {
                    return widget.builder(context, snapshot.data);
                  } else if (snapshot.hasError) {
                    return NetErrorWidget(
                      callback: () {
                        _request();
                      },
                    );
                  }
              }
              return Container();
            },
          );
  }
复制代码

首先判断 _future 是否为 null,如果为空,那么则表示还没有初始化该 Future,文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16806.html

个人建议这个时候返回自己定义好的加载中 Widget,因为后续在网络请求中的时候也返回该 Widget,这样不会显得乱。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16806.html

然后在 ConnectionState.done 中判断是否存在数据,如果有的话,就显示传进来的 Widget。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16806.html

如果返回错误,则返回错误的 Widget。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16806.html

错误 Widget 可以点击重新请求

这个逻辑其实很简单,在我最开始说的文章中有讲解一部分。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16806.html

那就是什么时候 FutureBuilder 会重新创建?文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16806.html

@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 不一样的情况下会重新走一遍流程。否则是不会走的。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16806.html

而我们在上面也已经定义好了,因为传进来的是 Function 和 Params,我们可以随时重新创建该 Future:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16806.html

void _request() {
  setState(() {
    if (widget.params == null)
      _future = widget.futureFunc(context);
    else
      _future = widget.futureFunc(context, params: widget.params);
  });
}
复制代码

错误 Widget 的点击事件写成这个就 ok 了,这样就重新创建了该 FutureBuilder,也就是重新请求了。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16806.html

作者:Flutter笔记
链接:https://juejin.im/post/5d9b4cd3f265da5bb746495f
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16806.html

  • 本站内容整理自互联网,仅提供信息存储空间服务,以方便学习之用。如对文章、图片、字体等版权有疑问,请在下方留言,管理员看到后,将第一时间进行处理。
  • 转载请务必保留本文链接:https://www.cainiaoxueyuan.com/xcx/16806.html

Comment

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定