Flutter 入门指北(Part 13)之网络


码个蛋(codeegg)第 677 次推文

HttpClientdart 自带的网络请求方式,在 dart:io 包下。使用 HttpClient 作为请求分以下几个步骤:

1. 创建 HttpClient 实例

HttpClient client = HttpClient();

2. 打开连接,并设置一些头参数,请求参数等

// 如果 url 中没有查询参数可直接创建
Uri uri = Uri.parse('https://www.xxx.com');
// 如果存在查询参数则在 Uri 中添加
Uri uri = Uri(scheme: 'https', host: 'www.xxx.com', queryParameters: {'a': 'AAA'});
// 打开连接
HttpClientRequest request = await client.getUrl(uri);
request.headers.add('token', 'Bear ${'x' * 20}'); // 添加头部 token 信息
// 如果是 post 或者 put 请求,通过 `add` 添加请求体
// 因为 `add` 方法需要传入 `List<int>` 参数,可以通过 utf8.encode 进行编码
request.add(utf8.encode('{"a": "aaa"}'));
// 也可以通过添加流的方式进行添加
request.addStream(input);

3. 连接服务器

// 设置 request 后通过 request.close() 获取一个响应对象 HttpClientResponse,
// 包括响应头,响应内容等
HttpClientResponse response = await request.close();

4. 读取服务器响应内容

String responseBody = await response.transform(utf8.decoder).join();

5. 关闭实例

client.close();

例如我们要去请求 Bird.so 的首页并显示,我们可以这么实现

_httpClientRequest() async {
 HttpClient client;
 // try catch finally 用于捕获请求过程中发生的异常,在 finally 中设置保证 client 能够关闭
 try {
 client = HttpClient();
 HttpClientRequest request = await client.getUrl(Uri.parse(_BIRD_SO_URL));
 HttpClientResponse response = await request.close();
 String strResponse = await response.transform(utf8.decoder).join();
 setState(() => _netBack = strResponse);
 } catch (e) {
 print('${e.toString()}');
 setState(() => _netBack = 'Fail');
 } finally {
 client.close();
 }
 }

很显然,用 HttpClient 请求相对来说是个非常麻烦的过程,如果要涉及到文本上传之类的,那么就会更麻烦了,所以这边引入一个网络请求的插件 dio,写本文的时候版本为 2.1.0。

Dio

dio 是个非常强大的网络请求库,他的方式类似 OkHttp,我们可以直接查看官方文档(https://github.com/flutterchina/dio/blob/master/README-ZH.md),使用方式非常简单,创建一个 Dio 实例,然后就可以通过 getpost 等方式发起请求,返回 Future<Response>,而且支持多个并发请求,可以设置返回响应的类型,监听上传下载进度等等,看着就很给力。对于简单的方式,这边就不做太多介绍,主要讲下拦截器,也是非常给力的一部分。比如我们需要请求这么个接口 https://randomuser.me/api/

这个接口通过 get 请求,可以加入任意的查询参数。比如我们需要实现一个请求加解密的过程,如果每次都在上传参数或者返回请求的时候去加密,解密的话,就做了非常多无用功了,那么这时候拦截器就派上用场了。先定义下加解密的规则,上传的参数统一转为小写,不存在大写,请求回的数据,不能含有 info 字段。看下如何实现:

_dioRequest() async {
 BaseOptions options = BaseOptions(connectTimeout: 5000, receiveTimeout: 60000);
 Dio dio = Dio(options);
 
 dio.interceptors.add(InterceptorsWrapper(onRequest: (opt) {
 // 获取查询的参数
 Map params = opt.queryParameters;
 // 将所有的参数转为小写,因为查询参数通过 map 形式上传
 params.forEach((key, value) =>
 opt.queryParameters[key] = '$value'.toLowerCase());
 // 这边还可以做些别的操作,例如需要 token 进行用户身份验证,则通过头部进行添加
 // opt.headers['authorization'] = 'token';
 // 在官网中,提供了 lock 和 unlock 的写法,被 lock 后,接下来的请求会进入队列等待,
 // 直到 unlock 后才能继续,可以用于几个请求,后续的需要用到前面的返回值的情况使用
 
 // 返回修改后的 RequestOptions
 return opt;
 }, onResponse: (resp) {
 // 返回响应体后,将 info 字段的内容切除,并将 json 拼接完成
 resp.data = '${'${resp.data}'.split(', info').first}}';
 return resp;
 }, onError: (error) {
 // 发生错误时的回调
 return error;
 }));

 // 发送一个请求,可以查看下打印的结果
 Response response = await dio.get(_USER_ME_URL, queryParameters: {'a': 'AAA', 'b': 'BbBbBb'});
 print(response.data);
 print(response.request.headers);
 print(response.request.queryParameters);
 setState(() => _netBack = response.data.toString()); // 界面显示 response.data
 }

看下最后的显示信息

请求体的头部成功加上了 authorization 参数,请求的参数全部变为小写,返回的信息也把 info 字段值去除。在很多时候,请求接口后,需要将 json 转换成 pojo 类来处理,可以通过 json_serializable 这个三方插件实现,这边提供文章Flutter Json自动反序列化——json_serializable v1.5.1(https://juejin.im/post/5b5f00e7e51d45190571172f),当然这种方式比较麻烦,这里推荐个 Android Studio 下的插件 dart_json_format 直接搜索就可以,如果用的是 Vitual Code 或者别的不是 JetBrains 系列的,这里有个转换的网址JsontoDart(https://javiercbk.github.io/json_to_dart/)。

以上代码查看 http_main.dart 文件

实践一下下

不知道小伙还记得前面讲的 BLoC 没有,忘了可以查看 Flutter 状态管理及 BLoC,这里结合 BLoCDio 实现界面和逻辑分离的小例子,接口使用前面提到的 https://randomuser.me/api/ 接口。网络应该是比较常用的,所以对其进行一些封装还是很有必要的,这边提供下我自己封装的方法:

import 'package:dio/dio.dart';

// 用于错误信息回调
typedef ErrorCallback = void Function(String msg);

class HttpUtils {
 static const GET = 'get';
 static const POST = 'post';

 static Dio _dio;

 static HttpUtils _instance;

 Dio get hp => _dio;

 // dio 可以在 BaseOptions 中指定域名 baseUrl,
 // 后续接口就不需要再添加域名了
 // 如果请求的接口域名发生了变化,只要把全部 url 写全,就会自动使用新的域名
 HttpUtils._internal(String base) {
 // 生成一个单例,防止多次打开关闭造成开销
 _dio = Dio(BaseOptions(baseUrl: base, connectTimeout: 10000, receiveTimeout: 10000));
 }

 factory HttpUtils(String base) {
 if (_instance == null) _instance = HttpUtils._internal(base);
 return _instance;
 }

 // 添加拦截器
 addInterceptor(List<InterceptorsWrapper> interceptors) {
 _dio.interceptors.clear();
 _dio.interceptors.addAll(interceptors);
 }

 Future<Response<T>> getRequest<T>(url, {Map params, ErrorCallback callback}) =>
 _request(url, GET, params: params, callback: callback);

 Future<Response<T>> postRequest<T>(url, {Map params, ErrorCallback callback}) =>
 _request(url, POST, params: params, callback: callback);

 Future<Response> download(url, path, {ProgressCallback receive, CancelToken token}) =>
 _dio.download(url, path, onReceiveProgress: receive, cancelToken: token);

 // T 可以指定返回的类型,String 或者 Map<String, dynamic>
 Future<Response<T>> _request<T>(
 url,
 String method, {
 Map params, // 上传的参数
 Options opt,
 ErrorCallback callback, // 错误回调
 ProgressCallback send, // 上传进度监听
 ProgressCallback receive, // 下载监听
 CancelToken token, // 用于取消的 token,可以多个请求绑定一个 token
 }) async {
 try {
 Response<T> rep;

 if (method == GET) {
 // 如果不是重新创建 Dio 实例,get 方法使用 queryParams 会出错,不懂原因,使用拼接没有问题
 if (params != null && params.isNotEmpty) {
 var sb = StringBuffer('?');
 params.forEach((key, value) {
 sb.write('$key=$value&');
 });
 // get 请求下拼接路径
 url += sb.toString().substring(0, sb.length - 1);
 }
 rep = await _dio.get(url, options: opt, onReceiveProgress: receive, cancelToken: token);
 } else if (method == POST) {
 // post 参数放请求体
 rep = params == null
 ? await _dio.post(url, options: opt, cancelToken: token, onSendProgress: send, onReceiveProgress: receive)
 : await _dio.post(url,
 data: params, options: opt, cancelToken: token, onSendProgress: send, onReceiveProgress: receive);
 }

 // 如果 statusCode 不是 200 则错误回调,返回空的 Response
 if (rep.statusCode != 200 && callback != null) {
 callback('network error, and code is ${rep.statusCode}');
 return null;
 }
 return rep;
 } catch (e) {
 if (callback != null) {
 callback('network error, catch error: ${e.toString()}');
 }
 return null;
 }
 }
}

封装后就可以愉快的调用了,如果有别的请求方式后期可以继续扩展。继续看代码,创建一个 application.dart 文件,用于存放全局参数

class Application {
 static HttpUtils http;
}

并在 main() 方法中进行初始化,接下来就可以直接使用

void main() {
 Application.http = HttpUtils('https://randomuser.me');
 
 runApp(DemoApp());

 // 透明状态栏
 if (Platform.isAndroid) {
 SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(statusBarColor: Colors.transparent));
 }
}

看下最后的实现效果吧,刚进入没有数据则通过转圈圈提示,加载完数据后,点击头像更换下。

实现 BLoC 需要有一个管理类

class UserBloc extends BaseBloc {
 RandomUserModel _user;

 RandomUserModel get user => _user;

 BehaviorSubject<RandomUserModel> _controller = BehaviorSubject();

 Observable<RandomUserModel> get stream => Observable(_controller.stream);

 // 网络请求获取新的数据,并更新
 updateUserInfo() {
 Application.http.getRequest('/api').then((response) {
 // RandomUserModel 就是接口返回的 json 转成的 model 类
 RandomUserModel model = RandomUserModel.fromMap(response.data);
 _user = model;
 // add 到 controller 通知修改
 _controller.add(model);
 });
 }

 @override
 void dispose() {
 _controller?.close(); // 及时销毁
 }
}

设置好管理类后,就可以来编写界面了,界面也比较简单


class UserPageDemo extends StatelessWidget {
 // 将首字母大写
 String _upperFirst(String content) {
 assert(content != null && content.isNotEmpty);
 return '${content.substring(0, 1).toUpperCase()}${content.substring(1)}';
 }

 // 地址信息通用部件
 Widget _userLocation(String info) => Padding(
 padding: const EdgeInsets.only(top: 4.0),
 child: Text(info, style: TextStyle(color: Colors.white, fontSize: 16.0)));

 @override
 Widget build(BuildContext context) {
 UserBloc _bloc = BlocProvider.of<UserBloc>(context);
 _bloc.updateUserInfo();

 return Scaffold(
 // StreamBuilder 接受更新数据的 stream
 body: StreamBuilder(
 builder: (_, AsyncSnapshot<RandomUserModel> snapshot) => Container(
 alignment: Alignment.center,
 decoration: BoxDecoration(
 gradient: LinearGradient(
 begin: Alignment.topCenter,
 end: Alignment.bottomCenter,
 colors: [Colors.blue[600], Colors.blue[400]])),
 child: !snapshot.hasData
 ? CupertinoActivityIndicator(radius: 12.0)
 : Column(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
 InkWell( // 用于切换数据
 child: ClipOval( // 圆形头像
 child: FadeInImage.assetNetwork(
 placeholder: 'images/ava_default.png', image: snapshot.data.results[0].picture.large),
 ),
 onTap: () => _bloc.updateUserInfo()), // 更新数据
 Padding(
 padding: const EdgeInsets.only(top: 20.0),
 child: Text(
 '${_upperFirst(snapshot.data.results[0].name.first)} ${_upperFirst(snapshot.data.results[0].name.last)}',
 style: TextStyle(color: Colors.white, fontSize: 24.0)),
 ),
 Text('${snapshot.data.results[0].email}',
 style: TextStyle(color: Colors.white, fontSize: 18.0)),
 _userLocation('${snapshot.data.results[0].location.street}'),
 _userLocation('${_upperFirst(snapshot.data.results[0].location.city)}'),
 _userLocation('${_upperFirst(snapshot.data.results[0].location.state)}'),
 ]),
 ),
 initialData: _bloc.user, // 注入初始值
 stream: _bloc.stream), // 注入更新 stream
 );
 }
}

以上代码查看 bloc_network 包下的所有文件

当然了,福利是不可少的,但是需要你到项目中自己去找。差不多入门的部分就讲到这了,接下来考虑加个实战,总之先等等吧,我找个好的题材接口来写。

最后代码的地址还是要的:

  1. 文章中涉及的代码:demos(https://github.com/kukyxs/flutter_arts_demos_app)

  2. 基于郭神 cool weather 接口的一个项目,实现 BLoC 模式,实现状态管理:flutter_weather(https://github.com/kukyxs/flutter_weather)

  3. 一个课程(当时买了想看下代码规范的,代码更新会比较慢,虽然是跟着课上的一些写代码,但是还是做了自己的修改,很多地方看着不舒服,然后就改成自己的实现方式了):flutter_shop(https://github.com/kukyxs/flutter_shop)

如果对你有帮助的话,记得给个 Star,先谢过,你的认可就是支持我继续写下去的动力~

结尾

往期Flutter系列文,保你一周掌握!(持续更新!!!)

近期文章:

专属社群:

《这件事情,我终于想明白了》 

今日问题:

Flutter还在学吗?

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值