MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

利用 dio 插件实现 Flutter 应用的复杂网络请求

2023-03-193.2k 阅读

一、Flutter 网络请求基础与 dio 插件介绍

1.1 Flutter 网络请求概述

在现代移动应用开发中,网络请求是不可或缺的一部分。Flutter作为一款流行的跨平台移动应用开发框架,提供了多种方式来处理网络请求。网络请求的常见场景包括从服务器获取数据,如 JSON 格式的用户信息、商品列表等;向服务器发送数据,例如用户注册信息、订单提交等。Flutter 的网络请求实现需要考虑到跨平台兼容性、性能优化以及错误处理等多方面因素。

在 Flutter 中,原生的 dart:io 库提供了基础的网络请求功能,例如 HttpClient 类可以用于发送 HTTP 请求。然而,使用原生库进行复杂网络请求时,代码编写相对繁琐,需要处理诸如连接管理、请求头设置、响应解析等多个细节。为了简化网络请求的开发流程,Flutter 社区提供了许多优秀的第三方插件,其中 dio 插件尤为突出。

1.2 dio 插件简介

dio 是一个强大的 Flutter HTTP 客户端,它基于 http 库进行封装,为开发者提供了简洁易用的 API,同时具备许多高级特性,使其非常适合处理复杂的网络请求。dio 插件的主要特点如下:

  • 简洁易用的 APIdio 的 API 设计直观,开发者可以轻松地发送各种类型的 HTTP 请求,如 GET、POST、PUT、DELETE 等,并且可以方便地设置请求参数、请求头和处理响应数据。
  • 拦截器支持dio 提供了拦截器机制,允许开发者在请求发送前和响应接收后对数据进行统一处理。这对于添加通用的请求头(如身份验证令牌)、日志记录、错误处理等操作非常有用。
  • 请求取消:在某些情况下,例如用户在请求过程中切换页面,可能需要取消正在进行的网络请求。dio 支持请求取消功能,避免不必要的资源浪费。
  • 支持多种响应类型dio 可以处理多种类型的响应数据,包括 JSON、字符串、字节流等,方便开发者根据实际需求进行解析。
  • Cookie 管理:对于需要处理 Cookie 的场景,dio 提供了内置的 Cookie 管理功能,开发者可以轻松地设置、获取和管理 Cookie。

二、dio 插件的安装与基本使用

2.1 安装 dio 插件

要在 Flutter 项目中使用 dio 插件,首先需要在 pubspec.yaml 文件中添加依赖。打开项目的 pubspec.yaml 文件,在 dependencies 部分添加如下内容:

dependencies:
  dio: ^[latest_version]

[latest_version] 替换为 dio 的最新版本号。可以在 pub.dev 上查看当前的最新版本。添加依赖后,在项目根目录下运行 flutter pub get 命令,Flutter 会自动下载并安装 dio 插件及其依赖。

2.2 发送简单的 GET 请求

安装完成后,就可以在 Dart 代码中引入 dio 库并开始使用。以下是一个发送简单 GET 请求的示例:

import 'package:dio/dio.dart';

void main() async {
  try {
    Dio dio = Dio();
    Response response = await dio.get('https://jsonplaceholder.typicode.com/posts/1');
    print(response.data);
  } catch (e) {
    print('Error: $e');
  }
}

在上述代码中:

  1. 首先导入 package:dio/dio.dart 库,这是使用 dio 插件的基础。
  2. 创建一个 Dio 实例,Dio 类是 dio 插件的核心,用于发送各种网络请求。
  3. 使用 dio.get 方法发送一个 GET 请求到 https://jsonplaceholder.typicode.com/posts/1,这是一个提供示例数据的 API 地址。await 关键字用于等待请求完成并获取响应。
  4. 打印 response.data,这里 response.data 包含了服务器返回的响应数据。如果请求过程中发生错误,catch 块会捕获异常并打印错误信息。

2.3 发送带参数的 GET 请求

很多时候,GET 请求需要携带参数。dio 提供了简洁的方式来设置请求参数。以下是一个示例:

import 'package:dio/dio.dart';

void main() async {
  try {
    Dio dio = Dio();
    Map<String, dynamic> queryParameters = {
      'userId': 1,
      '_limit': 10
    };
    Response response = await dio.get('https://jsonplaceholder.typicode.com/posts',
        queryParameters: queryParameters);
    print(response.data);
  } catch (e) {
    print('Error: $e');
  }
}

在这个示例中,定义了一个 queryParameters 映射,包含了 userId_limit 两个参数。在调用 dio.get 方法时,通过 queryParameters 参数将这些参数传递给服务器。最终请求的 URL 会类似于 https://jsonplaceholder.typicode.com/posts?userId=1&_limit=10

2.4 发送 POST 请求

发送 POST 请求通常用于向服务器提交数据。以下是一个发送 POST 请求的示例,假设服务器期望接收 JSON 格式的数据:

import 'package:dio/dio.dart';

void main() async {
  try {
    Dio dio = Dio();
    Map<String, dynamic> data = {
      'title': 'New Post',
      'body': 'This is a new post',
      'userId': 1
    };
    Response response = await dio.post('https://jsonplaceholder.typicode.com/posts',
        data: data);
    print(response.data);
  } catch (e) {
    print('Error: $e');
  }
}

在上述代码中:

  1. 定义了一个 data 映射,包含了要提交给服务器的数据。
  2. 使用 dio.post 方法发送 POST 请求到 https://jsonplaceholder.typicode.com/posts,并通过 data 参数将数据传递给服务器。服务器接收到数据后,会根据其 API 逻辑进行处理,并返回相应的响应。

三、处理复杂网络请求

3.1 设置请求头

在实际开发中,很多 API 要求在请求头中携带特定的信息,例如身份验证令牌、内容类型等。dio 允许开发者轻松设置请求头。以下是一个设置请求头的示例:

import 'package:dio/dio.dart';

void main() async {
  try {
    Dio dio = Dio();
    dio.options.headers = {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer your_token_here'
    };
    Response response = await dio.get('https://example.com/api/data');
    print(response.data);
  } catch (e) {
    print('Error: $e');
  }
}

在这个示例中,通过 dio.options.headers 来设置请求头。Content-Type 设置为 application/json,表示请求的数据格式为 JSON;Authorization 设置为包含身份验证令牌的 Bearer 类型。这样,每次通过这个 dio 实例发送请求时,都会带上这些请求头。

3.2 处理响应数据

dio 支持多种响应数据类型的处理。常见的响应类型有 JSON、字符串和字节流。

3.2.1 处理 JSON 响应

如果服务器返回的是 JSON 格式的数据,dio 会自动将其解析为 Dart 的 MapList 类型(取决于 JSON 结构)。前面的示例中已经展示了如何直接打印 JSON 响应数据。例如,如果服务器返回的 JSON 数据是一个包含多个对象的数组,代码如下:

import 'package:dio/dio.dart';

void main() async {
  try {
    Dio dio = Dio();
    Response response = await dio.get('https://jsonplaceholder.typicode.com/posts');
    List<dynamic> posts = response.data;
    for (var post in posts) {
      print('Title: ${post['title']}, Body: ${post['body']}');
    }
  } catch (e) {
    print('Error: $e');
  }
}

在这个示例中,response.data 被赋值给 posts,它是一个 List<dynamic> 类型。通过遍历 posts,可以访问每个 JSON 对象中的属性。

3.2.2 处理字符串响应

有些情况下,服务器可能返回纯文本字符串。可以通过 response.data.toString() 将响应数据转换为字符串。例如:

import 'package:dio/dio.dart';

void main() async {
  try {
    Dio dio = Dio();
    Response response = await dio.get('https://example.com/api/plaintext');
    String text = response.data.toString();
    print('Received text: $text');
  } catch (e) {
    print('Error: $e');
  }
}

3.2.3 处理字节流响应

对于下载文件等场景,服务器可能返回字节流数据。可以通过 response.data 直接获取字节数组,并进行后续处理,例如保存文件。以下是一个简单的下载文件示例:

import 'package:dio/dio.dart';
import 'dart:io';

void main() async {
  try {
    Dio dio = Dio();
    Response response = await dio.get('https://example.com/api/file',
        options: Options(responseType: ResponseType.bytes));
    File file = File('downloaded_file.pdf');
    await file.writeAsBytes(response.data);
    print('File downloaded successfully');
  } catch (e) {
    print('Error: $e');
  }
}

在这个示例中,通过 Options(responseType: ResponseType.bytes) 设置响应类型为字节流。然后将字节数组写入文件,完成文件下载。

3.3 错误处理

网络请求过程中可能会发生各种错误,如网络连接失败、服务器响应错误等。dio 提供了详细的错误处理机制。

3.3.1 捕获网络错误

当网络连接失败时,会抛出 DioError 异常,并且 DioError.typeDioErrorType.connectTimeoutDioErrorType.receiveTimeout 等。以下是捕获网络错误的示例:

import 'package:dio/dio.dart';

void main() async {
  try {
    Dio dio = Dio();
    Response response = await dio.get('https://nonexistent.example.com/api/data');
    print(response.data);
  } on DioError catch (e) {
    if (e.type == DioErrorType.connectTimeout) {
      print('Connection timeout occurred');
    } else if (e.type == DioErrorType.receiveTimeout) {
      print('Receive timeout occurred');
    } else {
      print('Other network error: ${e.message}');
    }
  }
}

3.3.2 处理服务器响应错误

如果服务器返回的状态码不是 2xx,也会抛出 DioError 异常,并且 DioError.typeDioErrorType.response。可以通过 e.response 获取服务器的响应信息,包括状态码、响应头和响应体等。以下是处理服务器响应错误的示例:

import 'package:dio/dio.dart';

void main() async {
  try {
    Dio dio = Dio();
    Response response = await dio.get('https://jsonplaceholder.typicode.com/nonexistent');
    print(response.data);
  } on DioError catch (e) {
    if (e.type == DioErrorType.response) {
      print('Server error: ${e.response?.statusCode}');
      print('Response data: ${e.response?.data}');
    }
  }
}

在这个示例中,如果请求的资源不存在,服务器会返回 404 状态码,通过 e.response?.statusCode 可以获取到这个状态码,通过 e.response?.data 可以获取到服务器返回的错误信息。

四、dio 拦截器的使用

4.1 拦截器概述

dio 的拦截器是其强大功能之一,它允许开发者在请求发送前和响应接收后对数据进行统一处理。拦截器可以分为请求拦截器和响应拦截器。请求拦截器在请求发送之前被调用,可以用于添加通用的请求头、处理请求参数等;响应拦截器在响应被接收之后被调用,可以用于处理响应数据、统一错误处理等。

4.2 添加请求拦截器

以下是一个添加请求拦截器的示例,用于在每个请求头中添加身份验证令牌:

import 'package:dio/dio.dart';

void main() async {
  Dio dio = Dio();
  dio.interceptors.add(InterceptorsWrapper(
    onRequest: (Options options, RequestInterceptorHandler handler) {
      options.headers['Authorization'] = 'Bearer your_token_here';
      handler.next(options);
    },
  ));
  try {
    Response response = await dio.get('https://example.com/api/data');
    print(response.data);
  } catch (e) {
    print('Error: $e');
  }
}

在这个示例中:

  1. 创建一个 InterceptorsWrapper 实例,并实现 onRequest 方法。
  2. onRequest 方法中,为 options.headers 添加 Authorization 头。
  3. 调用 handler.next(options) 将请求继续传递下去。如果不调用 handler.next(options),请求将不会被发送。

4.3 添加响应拦截器

以下是一个添加响应拦截器的示例,用于统一处理服务器返回的错误信息:

import 'package:dio/dio.dart';

void main() async {
  Dio dio = Dio();
  dio.interceptors.add(InterceptorsWrapper(
    onResponse: (Response response, ResponseInterceptorHandler handler) {
      if (response.statusCode! >= 400) {
        print('Server error: ${response.statusCode}');
        // 可以在这里进行统一的错误处理,如显示错误弹窗等
      }
      handler.next(response);
    },
  ));
  try {
    Response response = await dio.get('https://jsonplaceholder.typicode.com/nonexistent');
    print(response.data);
  } catch (e) {
    print('Error: $e');
  }
}

在这个示例中:

  1. 创建一个 InterceptorsWrapper 实例,并实现 onResponse 方法。
  2. onResponse 方法中,检查 response.statusCode 是否大于或等于 400,如果是,则表示服务器返回了错误状态码,打印错误信息。
  3. 调用 handler.next(response) 将响应继续传递下去。如果不调用 handler.next(response),响应将不会被传递到后续的处理逻辑。

4.4 移除拦截器

在某些情况下,可能需要移除已经添加的拦截器。可以通过 dio.interceptors.remove 方法来移除拦截器。以下是一个示例:

import 'package:dio/dio.dart';

void main() async {
  Dio dio = Dio();
  InterceptorsWrapper interceptor = InterceptorsWrapper(
    onRequest: (Options options, RequestInterceptorHandler handler) {
      options.headers['Authorization'] = 'Bearer your_token_here';
      handler.next(options);
    },
  );
  dio.interceptors.add(interceptor);
  // 移除拦截器
  dio.interceptors.remove(interceptor);
  try {
    Response response = await dio.get('https://example.com/api/data');
    print(response.data);
  } catch (e) {
    print('Error: $e');
  }
}

在这个示例中,先添加了一个请求拦截器,然后通过 dio.interceptors.remove(interceptor) 将其移除。这样后续的请求将不会再经过这个拦截器的处理。

五、请求取消与并发请求

5.1 请求取消

在 Flutter 应用中,有时候需要取消正在进行的网络请求,例如用户在请求过程中切换页面。dio 提供了方便的请求取消功能。以下是一个示例:

import 'package:dio/dio.dart';

void main() async {
  Dio dio = Dio();
  CancelToken cancelToken = CancelToken();
  try {
    Response response = await dio.get('https://example.com/api/data',
        cancelToken: cancelToken);
    print(response.data);
  } on DioError catch (e) {
    if (e.type == DioErrorType.cancel) {
      print('Request was cancelled');
    } else {
      print('Error: $e');
    }
  }
  // 取消请求
  cancelToken.cancel('Cancelled by user');
}

在这个示例中:

  1. 创建一个 CancelToken 实例。
  2. 在发送请求时,通过 cancelToken 参数将其传递给 dio.get 方法。
  3. 如果需要取消请求,调用 cancelToken.cancel 方法,并可以传入一个取消原因的字符串。
  4. catch 块中,通过检查 e.type 是否为 DioErrorType.cancel 来判断请求是否被取消。

5.2 并发请求

在某些场景下,可能需要同时发送多个网络请求,并在所有请求完成后进行统一处理。dio 结合 Dart 的 Future 类可以轻松实现并发请求。以下是一个示例,同时发送两个 GET 请求,并在两个请求都完成后处理响应数据:

import 'package:dio/dio.dart';

void main() async {
  Dio dio = Dio();
  Future<Response> future1 = dio.get('https://jsonplaceholder.typicode.com/posts/1');
  Future<Response> future2 = dio.get('https://jsonplaceholder.typicode.com/posts/2');
  List<Response> responses = await Future.wait([future1, future2]);
  for (Response response in responses) {
    print(response.data);
  }
}

在这个示例中:

  1. 创建两个 Future<Response>,分别发送两个不同的 GET 请求。
  2. 使用 Future.wait 方法等待这两个 Future 都完成,并返回一个包含所有响应的列表。
  3. 遍历 responses 列表,处理每个请求的响应数据。

通过这种方式,可以有效地管理并发网络请求,提高应用的性能和响应速度。

六、与 Flutter 状态管理结合

6.1 为什么要结合状态管理

在 Flutter 应用中,网络请求的结果通常需要反映在 UI 上。例如,从服务器获取用户信息后,需要在 UI 中显示用户的姓名、头像等。为了实现这种数据与 UI 的同步更新,就需要结合状态管理。状态管理可以帮助我们更好地组织和管理应用的状态,使得网络请求的结果能够准确、高效地更新到 UI 上。

6.2 与 Provider 状态管理结合

Provider 是 Flutter 中常用的状态管理库。以下是一个结合 Providerdio 进行网络请求并更新 UI 的示例。

首先,定义一个数据模型类:

class User {
  final int id;
  final String name;

  User({required this.id, required this.name});

  factory User.fromJson(Map<String, dynamic> json) {
    return User(
      id: json['id'],
      name: json['name'],
    );
  }
}

然后,创建一个 UserProvider 类,用于管理用户数据的状态:

import 'package:dio/dio.dart';
import 'package:flutter/material.dart';

class UserProvider with ChangeNotifier {
  User? _user;
  User? get user => _user;

  Future<void> fetchUser() async {
    try {
      Dio dio = Dio();
      Response response = await dio.get('https://jsonplaceholder.typicode.com/users/1');
      _user = User.fromJson(response.data);
      notifyListeners();
    } catch (e) {
      print('Error: $e');
    }
  }
}

UserProvider 类中:

  1. 定义了一个 _user 变量来存储用户数据,并提供了一个 user getter 方法。
  2. fetchUser 方法使用 dio 发送网络请求获取用户数据,并在获取成功后将数据转换为 User 对象,更新 _user 变量,并调用 notifyListeners() 通知依赖该状态的 Widget 进行更新。

在 Flutter 应用中使用 UserProvider

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => UserProvider(),
      child: MaterialApp(
        home: Scaffold(
          appBar: AppBar(
            title: Text('User Example'),
          ),
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton(
                  onPressed: () async {
                    await context.read<UserProvider>().fetchUser();
                  },
                  child: Text('Fetch User'),
                ),
                Consumer<UserProvider>(
                  builder: (context, provider, child) {
                    if (provider.user == null) {
                      return Text('No user data');
                    } else {
                      return Text('User: ${provider.user?.name}');
                    }
                  },
                )
              ],
            ),
          ),
        ),
      ),
    );
  }
}

在这个示例中:

  1. 使用 ChangeNotifierProviderUserProvider 提供给整个应用。
  2. ElevatedButtononPressed 回调中,调用 context.read<UserProvider>().fetchUser() 发起网络请求。
  3. 使用 Consumer<UserProvider> 监听 UserProvider 的状态变化,并根据 user 是否为空显示相应的 UI。

通过这种方式,将 dio 的网络请求与 Provider 状态管理相结合,实现了网络请求结果在 UI 上的动态更新。

七、性能优化与最佳实践

7.1 缓存策略

在网络请求中,缓存是提高性能的重要手段。dio 本身没有内置的缓存机制,但可以结合其他库来实现缓存。例如,可以使用 dio_cache_interceptor 库。以下是一个简单的使用示例:

  1. 首先在 pubspec.yaml 中添加依赖:
dependencies:
  dio_cache_interceptor: ^[latest_version]
  1. 然后在代码中使用:
import 'package:dio/dio.dart';
import 'package:dio_cache_interceptor/dio_cache_interceptor.dart';

void main() async {
  Dio dio = Dio();
  dio.interceptors.add(
    DioCacheInterceptor(
      options: CacheOptions(
        store: MemCacheStore(),
        policy: CachePolicy.requestIfNetworkFailed,
        hitCacheOnErrorExcept: [401, 403],
      ),
    ),
  );
  try {
    Response response = await dio.get('https://example.com/api/data');
    print(response.data);
  } catch (e) {
    print('Error: $e');
  }
}

在这个示例中:

  1. 创建一个 DioCacheInterceptor 实例,并设置 CacheOptions
  2. CacheOptions 中的 store 定义了缓存存储的方式,这里使用 MemCacheStore 表示内存缓存。
  3. policy 定义了缓存策略,CachePolicy.requestIfNetworkFailed 表示如果网络请求失败,尝试从缓存中获取数据。
  4. hitCacheOnErrorExcept 表示除了 401(未授权)和 403(禁止访问)错误外,其他错误情况下都尝试从缓存中获取数据。

7.2 批量请求与合并

在某些情况下,可能有多个相似的网络请求。为了减少网络开销,可以将这些请求合并为一个批量请求。例如,如果需要获取多个用户的信息,可以设计一个 API 支持批量获取用户信息,然后在 Flutter 中通过 dio 发送这个批量请求。假设服务器提供了一个 /users?ids=1,2,3 的 API 来批量获取用户信息,代码如下:

import 'package:dio/dio.dart';

void main() async {
  Dio dio = Dio();
  List<int> userIds = [1, 2, 3];
  String idsString = userIds.join(',');
  Response response = await dio.get('https://example.com/api/users?ids=$idsString');
  print(response.data);
}

在这个示例中,将多个用户 ID 拼接成字符串,作为参数发送到服务器,从而实现了批量请求,减少了网络请求次数。

7.3 避免频繁请求

在应用中,要避免在不必要的时候频繁发送网络请求。例如,可以通过防抖(Debounce)或节流(Throttle)机制来控制请求频率。假设在搜索框输入时需要发送搜索请求,可以使用防抖机制,避免用户每次输入都立即发送请求。以下是一个简单的防抖实现示例:

import 'package:dio/dio.dart';
import 'dart:async';

class Debouncer {
  final int milliseconds;
  Timer? _timer;

  Debouncer({required this.milliseconds});

  void run(VoidCallback action) {
    _timer?.cancel();
    _timer = Timer(Duration(milliseconds: milliseconds), action);
  }
}

void main() async {
  Dio dio = Dio();
  Debouncer debouncer = Debouncer(milliseconds: 500);
  String searchQuery = '';
  // 模拟搜索框输入变化
  void onSearchChanged(String query) {
    searchQuery = query;
    debouncer.run(() async {
      try {
        Response response = await dio.get('https://example.com/api/search?q=$searchQuery');
        print(response.data);
      } catch (e) {
        print('Error: $e');
      }
    });
  }
  // 模拟用户输入
  onSearchChanged('apple');
  await Future.delayed(Duration(milliseconds: 300));
  onSearchChanged('apples');
}

在这个示例中:

  1. 创建一个 Debouncer 类,通过 Timer 实现防抖功能。
  2. onSearchChanged 方法中,每次输入变化时取消之前的 Timer,并重新启动一个新的 Timer。只有在 milliseconds 时间内没有再次触发时,才会执行 action,即发送网络请求。

通过以上性能优化和最佳实践,可以提高 Flutter 应用中网络请求的效率和用户体验。