flutter-网络处理

2024/5/7 flutterdart

🌙 1.使用 Http 包

# 安装依赖
flutter pub add http
1
2

https://pub.dev/documentation/http/latest/ (opens new window)

🌙 1.1简单使用:

// 导入 http 包
import 'package:http/http.dart' as http;

// 定义一个 Post 类
class Post {
  final int userId;
  final int id;
  final String title;
  final String body;

  Post(
      {required this.userId,
      required this.id,
      required this.title,
      required this.body});

  // 将 JSON 数据转换为 Post 实例
  factory Post.fromJson(Map<String, dynamic> json) {
    return Post(
      userId: json['userId'],
      id: json['id'],
      title: json['title'],
      body: json['body'],
    );
  }
}

// 定义一个 fetchPost() 方法
// 将HTTP响应转换为 Post 类实例
Future<Post> fetchPost([int id = 1]) async {
  final response = await http
      .get(Uri.parse('https://jsonplaceholder.typicode.com/posts/$id'));

  if (response.statusCode == 200) {
    return Post.fromJson(json.decode(response.body));
  } else {
    throw Exception('Failed to load post');
  }
}

// 定义一个 MyApp 类
class _MyAppState extends State<HttpDemo> {
  late Future<Post> post;
  int id = 1;

  
  void initState() {
    super.initState();
    post = fetchPost(id); // called inside initState()
  }

  
  Widget build(BuildContext context) {
    return Center(
        child: FutureBuilder<Post>(
            future: post, // 调用 fetchPost() 方法
            builder: (context, snapshot) {
              if (snapshot.hasData) {
                return Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    crossAxisAlignment: CrossAxisAlignment.center,
                    children: [
                      Text(
                        snapshot.data!.title,
                        style: const TextStyle(
                            fontSize: 18,
                            color: Colors.blue,
                            decoration: TextDecoration.underline),
                      ),
                      ElevatedButton(
                          onPressed: () {
                            // SystemChannels.textInput
                            //     .invokeMethod('TextInput.show');
                            setState(() {
                              post = fetchPost(id++);
                            });
                          },
                          child: const Icon(
                            Icons.navigate_next,
                            size: 30,
                          ))
                    ]);
              } else if (snapshot.hasError) {
                return Text("${snapshot.error}");
              }
              return const CircularProgressIndicator();
            }));
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89

🌙 1.2支持传参:

var client = http.Client();
try {
  var response = await client.post(
      Uri.https('example.com', 'whatsit/create'),
      body: {'name': 'doodle', 'color': 'blue'});
  var decodedResponse = jsonDecode(utf8.decode(response.bodyBytes)) as Map;
  var uri = Uri.parse(decodedResponse['uri'] as String);
  print(await client.get(uri));
} finally {
  client.close();
}
1
2
3
4
5
6
7
8
9
10
11

🌙 1.3支持重试:

import 'package:http/http.dart' as http;
import 'package:http/retry.dart';

Future<void> main() async {
  final client = RetryClient(http.Client());
  try {
    print(await client.read(Uri.http('example.org', '')));
  } finally {
    client.close();
  }
}
1
2
3
4
5
6
7
8
9
10
11

默认情况下,会重试任何响应状态码为503 Temporary Failure的请求,最多重试三次 。它在第一次重试之前等待500ms,并且每次延迟增加1.5倍。所有这些都可以使用RetryClient()构造函数进行定制。

🌙 1.4Restful 风格的 HTTP api:

Functions
delete(Uri url, {Map<String, String>? headers, Object? body, Encoding? encoding}) → Future<Response>
Sends an HTTP DELETE request with the given headers to the given URL.

get(Uri url, {Map<String, String>? headers}) → Future<Response>
Sends an HTTP GET request with the given headers to the given URL.

head(Uri url, {Map<String, String>? headers}) → Future<Response>
Sends an HTTP HEAD request with the given headers to the given URL.

patch(Uri url, {Map<String, String>? headers, Object? body, Encoding? encoding}) → Future<Response>
Sends an HTTP PATCH request with the given headers and body to the given URL.

post(Uri url, {Map<String, String>? headers, Object? body, Encoding? encoding}) → Future<Response>
Sends an HTTP POST request with the given headers and body to the given URL.

put(Uri url, {Map<String, String>? headers, Object? body, Encoding? encoding}) → Future<Response>
Sends an HTTP PUT request with the given headers and body to the given URL.

read(Uri url, {Map<String, String>? headers}) → Future<String>
Sends an HTTP GET request with the given headers to the given URL and returns a Future that completes to the body of the response as a String.

readBytes(Uri url, {Map<String, String>? headers}) → Future<Uint8List>
Sends an HTTP GET request with the given headers to the given URL and returns a Future that completes to the body of the response as a list of bytes.

runWithClient<R>(R body(), Client clientFactory(), {ZoneSpecification? zoneSpecification}) → R
Runs body in its own Zone with the Client returned by clientFactory set as the default Client.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

🌙 2.使用 dio 包

dio 是一个强大的 HTTP 网络请求库,支持全局配置、Restful API、FormData、拦截器、 请求取消、Cookie 管理、文件上传/下载、超时、自定义适配器、转换器等。

# 安装依赖
flutter pub add dio
1
2

https://pub.dev/documentation/dio/latest/ (opens new window)

🌙 2.1简单使用:

import 'package:dio/dio.dart';

final dio = Dio();

Future<Post> fetchPost([int id = 1]) async {
  final response = await dio.get('https://jsonplaceholder.typicode.com/posts/$id');

  if (response.statusCode == 200) {
    return Post.fromJson(response.data);
  } else {
    throw Exception('Failed to load post');
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

🌙 2.2创建实例:

import 'package:dio/dio.dart';

import 'package:dio/dio.dart';

// 创建一个 Dio 实例
Dio dio = Dio(
  // 配置 Dio 实例的选项
  BaseOptions(
    // 设置请求的基本 URL
    baseUrl: "https://api.example.com",
    // 设置连接超时时间为 5000 毫秒(5 秒)
    connectTimeout: Duration(milliseconds: 5000),
    // 设置接收超时时间为 3000 毫秒(3 秒)
    receiveTimeout: Duration(milliseconds: 3000),
  ),
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

🌙 2.3Restful 风格 api:

// get demo
getDemo() async {
  try {
    Response response = await dio.get(
        '/todos',
        queryParameters: {
          'page': 1,
          'limit': 10,
          'status': 'completed',
          'title': 'foo',
          'userId': 1,
          'tags': ['foo', 'bar'],
        },
        options: Options(
          headers: {
            'Authorization': 'Bearer 1234567890',
            'Accept': 'application/json',
            'Content-Type': 'application/json',
            'X-Custom-Header': 'foo',
          },)
    );
    print(response);
  } on DioException catch (e) {
      print(e.message);
  }
}

// post demo
postDemo() async {
  try {
    Response response = await dio.post('/todos',
        data: {
          'page': 1,
          'limit': 10,
          'status': 'completed',
          'title': 'foo',
          'userId': 1,
          'tags': ['foo', 'bar'],
        },
        options: Options(
          headers: {
            'Authorization': 'Bearer 1234567890',
            'Accept': 'application/json',
            'Content-Type': 'application/json',
            'X-Custom-Header': 'foo',
          },
        ));
    print(response);
  } on DioException catch (e) {
    print(e.message);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

🌙 2.4异常处理:

  • cancel:请求取消。当请求在完成前被取消时,会触发此错误。
  • connectionTimeout:连接超时。发生此错误时,表示客户端在与服务器建立连接时超出指定的时间限制。
  • sendTimeout:发送超时。当请求在发送数据到服务器时超时,会触发此错误。
  • receiveTimeout:接收超时。当等待服务器响应超出设定的时间限制时,会触发此错误。
  • badResponse:服务器响应错误。当服务器的响应状态码不在预期的范围内时,会触发此错误。
  • connectionError:连接错误。当请求由于网络连接问题失败时,会触发此错误。
  • badCertificate:证书验证失败。这种情况通常发生在 HTTPS 请求中,当服务器的 SSL 证书不被客户端信任时,就会抛出此类型的异常。
  • unknown:未知错误。当发生未预料到的其他错误时,会使用此类型。
 /// 源码定义如下:
/// The exception enumeration indicates what type of exception
/// has happened during requests.
enum DioExceptionType {
  /// Caused by a connection timeout.
  connectionTimeout,

  /// It occurs when url is sent timeout.
  sendTimeout,

  ///It occurs when receiving timeout.
  receiveTimeout,

  /// Caused by an incorrect certificate as configured by [ValidateCertificate].
  badCertificate,

  /// The [DioException] was caused by an incorrect status code as configured by
  /// [ValidateStatus].
  badResponse,

  /// When the request is cancelled, dio will throw a error with this type.
  cancel,

  /// Caused for example by a `xhr.onError` or SocketExceptions.
  connectionError,

  /// Default error type, Some other [Error]. In this case, you can use the
  /// [DioException.error] if it is not null.
  unknown,
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

代码示例:

handleErrorDemo() async {
  try {
    Response response = await dio.get("/user?id=123");
  } on DioException catch (e) {
    switch (e.type) {
      case DioExceptionType.connectionTimeout:
        // 连接超时处理
        print('连接超时');
        break;
      case DioExceptionType.sendTimeout:
        // 发送超时处理
        print('发送超时');
        break;
      case DioExceptionType.receiveTimeout:
        // 接收超时处理
        print('接收超时');
        break;
      case DioExceptionType.badResponse:
        // 服务器响应错误处理
        print('服务器响应错误,状态码:${e.response?.statusCode}');
        break;
      case DioExceptionType.cancel:
        // 请求取消处理
        print('请求被取消');
        break;
      case DioExceptionType.connectionError:
        // 连接错误处理
        print('连接错误');
        break;
      case DioExceptionType.unknown:
      default:
        // 其他错误处理
        print('未知错误');
        break;
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

🌙 2.5 Dio拦截器

Dio initDio() {
  final dio = Dio();
  // 添加拦截器
  dio.interceptors.add(InterceptorsWrapper(
    onRequest: (options, handler) {
      // 在请求发送前添加逻辑
      // 例如,添加一个自定义的请求头
      options.headers["Custom-Header"] = "custom-header";
      options.headers["Authorization"] = "Bearer 123456";
      // 继续执行请求
      return handler.next(options);
    },
    onResponse: (response, handler) {
      // 在响应返回后添加逻辑
      // 例如,打印响应数据
      print(response.data);
      // 继续执行响应
      return handler.next(response);
    },
    onError: (DioException e, handler) {
      // 在发生错误时添加逻辑
      // 例如,根据错误类型显示不同的错误信息
      print(e.message);
      // 继续执行错误处理
      return handler.next(e);
    },
  ));

  return dio;
}

final dio = initDio();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

可见,这个拦截器的配置和axios 配置拦截器非常相似,对前端开发来说,配置拦截器非常简单,而且功能非常强大。

使用该拦截器之后,查看请求头可以看到:

GET /posts/1 HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: zh-CN,zh;q=0.9
Authorization: Bearer 123456 # 自定义请求头
Connection: keep-alive
Custom-Header: custom-header # 自定义请求头
Host: localhost:5200
If-None-Match: W/"124-yiKdLzqO5gfBrJFrcdJ8Yq0LGnU"
Origin: http://localhost:58278
Referer: http://localhost:58278/
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-site
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36
sec-ch-ua: "Google Chrome";v="123", "Not:A-Brand";v="8", "Chromium";v="123"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

🌙 3.解决web跨域 (opens new window)

使用 shelf_proxy, 创建文件./lib/server/cors.dart:

import 'package:shelf/shelf_io.dart' as shelf_io;
import 'package:shelf_proxy/shelf_proxy.dart';

//本地域名
const String localHost = 'localhost';

//本地端口
const int localPort = 5200;

//目标域名
const String targetUrl = 'https://jsonplaceholder.typicode.com';

Future main() async {
  var server = await shelf_io.serve(
    proxyHandler(targetUrl),
    localHost,
    localPort,
  );
  // 添加上跨域的这几个header
  server.defaultResponseHeaders.add('Access-Control-Allow-Origin', '*');
  server.defaultResponseHeaders.add('Access-Control-Allow-Credentials', true);

  print('Serving at http://${server.address.host}:${server.port}');
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

执行命令,本地代理成功:

 dart ./lib/server/cors.dart
1

页面通过访问http://localhost:5200/todos, 即可访问到 https://jsonplaceholder.typicode.com/todos 的数据。