Flutter | 定义一个通用的多功能网络请求 Widget
不过,后续还是会每周最少更新两篇的!
那说起网络请求的控件,我们首先是不是会想起 FutureBuilder
?
FutureBuilder
给我们封装好了网络请求中的各种状态。
如果没有了解过,那么可以看我这篇文章: Flutter – FutureBuilder 异步UI神器。
这篇文章是早期写的,有些地方写的有些问题,但不重要!主要了解一下 FutureBuilder
的状态就可以了。
本篇文章中只是提供一种思路,欢迎一起探讨,也欢迎不吝赐教!
效果如下。
首先是没有开启服务的情况:
可以看到全部都是错误的信息,
然后开启服务:
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 _get( BuildContext context, String url, { Map 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:用于 showLoading
2.
url:API 地址
3.
params:该网络请求的参数,可以为空
方法内部我们捕获了 DioError
,然后判断接口是否还返回了正常的内容。
例如:状态码不为2xx,但是仍然返回了数据,这样 Dio 是会抛出 DioError
的,需要我们自己捕获来处理。
如果返回了正常的数据,那我们还是返回回去,如果不是正常的数据,则直接抛出 Future.error(0)
。
使用该通用方法:
/// 新碟上架 static Future getAlbumData( BuildContext context, { Map params = const { 'offset': 0, 'limit': 10, }, }) async { var response = await _get(context, '/top/album', params: params); return AlbumData.fromJson(response.data); }
我们就可以像这样来使用刚才定义好的方法,也方便我们后续定义一个通用的 FutureBuilder
。
2. 确认网络请求控件所需要的功能
我们从最开始的图中明显能看出来的,其实是有三个功能:
1.
请求数据并显示 Loading
2.
正常时返回正常数据,错误时返回错误 Widget
3.
错误 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 = Widget Function( BuildContext context, T value, );
final ValueWidgetBuilder builder; final Function futureFunc; final Map params;
CustomFutureBuilder({ @required this.futureFunc, @required this.builder, this.params, });
这样,我们就可以在 第一帧回调
中来调用该网络请求了,这样一举两得:
既不用在使用该控件的时候调用方法,又避免了 Loading 使用 BuildContext 报错的问题。
Future _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
有一个互动了,
网络请求的逻辑如下:
这样正好就可以对应 FutureBuilder
的几种状态:
1.
网络请求 -> ConnectionState.none
、 ConnectionState.waiting
2.
显示Loading -> ConnectionState.active
3.
请求结束 -> ConnectionState.done
4.
是否有数据(无论对错)-> snapshot.hasData
5.
抛出错误 -> 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 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
,也就是 重新请求
了。
总结
代码的话,我就不传上去了,因为这个只适用于一部分。
我这里只是提供一种思路,个人觉得还是不错的。
如果有什么想法的话,欢迎一起探讨,不吝赐教!
另我个人创建了一个「Flutter 交流群」,可以添加我个人微信 「17610912320」来入群。