为了账号安全,请及时绑定邮箱和手机立即绑定

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

标签:
WebApp

那说起网络请求的控件,我们首先是不是会想起 FutureBuilder

FutureBuilder 给我们封装好了网络请求中的各种状态。

如果没有了解过,那么可以看我这篇文章:Flutter - FutureBuilder 异步UI神器。

这篇文章是早期写的,有些地方写的有些问题,但不重要!主要了解一下 FutureBuilder 的状态就可以了。


本篇文章中只是提供一种思路,欢迎一起探讨,也欢迎不吝赐教!

效果如下。

首先是没有开启服务的情况:

https://img1.sycdn.imooc.com//5da0a4a800017d7005020249.jpg

可以看到全部都是错误的信息,

然后开启服务:

https://img1.sycdn.imooc.com//5da0a4b70001227f04820259.jpg


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

那既然是网络请求,那首先我们要定义一个通用的网络请求方法。

每一家后台 API 的风格都不一样,有的是 RSETful,有的是我们最熟悉的 GET、POST。

这里就以 GET 为例,API 接口为 GitHub - 网易云音乐 Node.js API service。 [1]

网络请求使用的是 Dio,先创建一个 NetUtils.dart

初始化代码:



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 前面调用即完成初始化。

接着定义一个通用的网络请求:



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);

  }

}



这里代码很简单,方法需要传入三个参数:

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

方法内部我们捕获了 DioError,然后判断接口是否还返回了正常的内容。

例如:状态码不为2xx,但是仍然返回了数据,这样 Dio 是会抛出 DioError 的,需要我们自己捕获来处理。

如果返回了正常的数据,那我们还是返回回去,如果不是正常的数据,则直接抛出 Future.error(0)

使用该通用方法:



/// 新碟上架

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

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

我们从最开始的图中明显能看出来的,其实是有三个功能:

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

然鹅,细心的同学也发现问题了。

我们在网络请求中添加了一个 Loading,而且需要一个 BuildContext。

我们都知道,是不能在 initState() 方法中去使用这个 BuildContext 的。

所以,我们还要进行一个 第一帧回调



@override

void initState() {

  super.initState();

  WidgetsBinding.instance.addPostFrameCallback((call) {

    _request();

  });

}



这样就完成了我们的需求调研。

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

说的是一个通用的网络请求控件,其实就是把 FutureBuilder 封装一层。

请求数据并显示 Loading

但是,这里也有一个问题:

我们在最开始定义网络请求工具类的时候,每一个网络请求都是一个方法,而每个方法中都有或者没有参数。

我们也知道,FutureBuilder 需要传入一个 Future,那这可怎么办?

而且我们不能在使用该控件的时候调用网络请求方法,因为网络请求中封装了一个 Loading,这个 Loading 需要 BuildContext

既然如此,那我们只能传入方法(Function)了:



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,

});



这样,我们就可以在 第一帧回调 中来调用该网络请求了,这样一举两得:

既不用在使用该控件的时候调用方法,又避免了 Loading 使用 BuildContext 报错的问题。



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 就可以了。

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

这就需要我们封装好的网络请求和 FutureBuilder 有一个互动了,

网络请求的逻辑如下:

https://img1.sycdn.imooc.com//5da0a5f50001ed9404340223.jpg

这样正好就可以对应 FutureBuilder 的几种状态:

1.网络请求 -> ConnectionState.noneConnectionState.waiting2.显示Loading -> ConnectionState.active3.请求结束 -> ConnectionState.done4.是否有数据(无论对错)-> snapshot.hasData5.抛出错误 -> snapshot.hasError

了解这些之后,我们就可以写出代码:



  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,

个人建议这个时候返回自己定义好的加载中 Widget,因为后续在网络请求中的时候也返回该 Widget,这样不会显得乱。

然后在 ConnectionState.done 中判断是否存在数据,如果有的话,就显示传进来的 Widget。

如果返回错误,则返回错误的 Widget。

错误 Widget 可以点击重新请求

这个逻辑其实很简单,在我最开始说的文章中有讲解一部分。

那就是什么时候 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 不一样的情况下会重新走一遍流程。否则是不会走的。

而我们在上面也已经定义好了,因为传进来的是 Function 和 Params,我们可以随时重新创建该 Future:



void _request() {

  setState(() {

    if (widget.params == null)

      _future = widget.futureFunc(context);

    else

      _future = widget.futureFunc(context, params: widget.params);

  });

}



错误 Widget 的点击事件写成这个就 ok 了,这样就重新创建了该 FutureBuilder,也就是重新请求了。

总结

代码的话,我就不传上去了,因为这个只适用于一部分。

我这里只是提供一种思路,个人觉得还是不错的。

如果有什么想法的话,欢迎一起探讨,不吝赐教!


点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
移动开发工程师
手记
粉丝
30
获赞与收藏
45

关注作者,订阅最新文章

阅读免费教程

  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消