Flutter网络请求完全指南:http与Dio库深度解析

发布于:2025-06-15 ⋅ 阅读:(19) ⋅ 点赞:(0)

在移动应用开发中,网络请求是与后端服务交互的核心方式。Flutter作为跨平台开发框架,提供了多种网络请求解决方案。本文将全面剖析Flutter中的两种主流网络请求方式:官方http包和功能强大的Dio库,从基础使用到高级技巧,助你掌握Flutter网络编程的精髓。

一、Flutter网络请求概述

1.1 为什么需要专门的网络请求库

在原生开发中,Android使用OkHttp或HttpURLConnection,iOS使用URLSession进行网络请求。Flutter作为跨平台框架,需要统一的网络请求解决方案。Dart语言虽然提供了dart:io库中的HttpClient,但直接使用较为底层,开发效率不高。因此,社区推出了更高级的封装库。

1.2 http与Dio库对比

特性 http包 Dio库
开发者 Flutter团队 社区维护
功能复杂度 简单 丰富
拦截器支持 不支持 支持
文件上传/下载 需要手动实现 内置支持
请求取消 不支持 支持
转换器 不支持 支持
全局配置 有限 全面
学习曲线

二、官方http包详解

2.1 安装与基本配置

在pubspec.yaml中添加依赖:

dependencies:
  http: ^1.1.0

运行flutter pub get安装包。

2.2 核心API解析

http包提供了简洁的API:

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

// GET请求
Future<http.Response> get(Uri url, {Map<String, String>? headers});

// POST请求
Future<http.Response> post(Uri url, 
  {Map<String, String>? headers, Object? body, Encoding? encoding});

// PUT、DELETE等类似

2.3 完整请求示例

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

class HttpService {
  static const String baseUrl = 'https://jsonplaceholder.typicode.com';
  
  // 获取数据
  Future<List<dynamic>> fetchPosts() async {
    final response = await http.get(Uri.parse('$baseUrl/posts'));
    
    if (response.statusCode == 200) {
      return jsonDecode(response.body);
    } else {
      throw Exception('Failed to load posts');
    }
  }
  
  // 创建数据
  Future<Map<String, dynamic>> createPost(Map<String, dynamic> post) async {
    final response = await http.post(
      Uri.parse('$baseUrl/posts'),
      headers: {'Content-Type': 'application/json'},
      body: jsonEncode(post),
    );
    
    if (response.statusCode == 201) {
      return jsonDecode(response.body);
    } else {
      throw Exception('Failed to create post');
    }
  }
  
  // 错误处理增强版
  Future<dynamic> safeRequest(Future<http.Response> request) async {
    try {
      final response = await request;
      if (response.statusCode >= 200 && response.statusCode < 300) {
        return jsonDecode(response.body);
      } else {
        throw HttpException(
          'Request failed with status: ${response.statusCode}',
          uri: response.request?.url,
        );
      }
    } on SocketException {
      throw const SocketException('No Internet connection');
    } on FormatException {
      throw const FormatException('Bad response format');
    }
  }
}

2.4 最佳实践

  1. 封装请求层:将网络请求逻辑集中管理

  2. 统一错误处理:避免在每个请求中重复处理错误

  3. 使用async/await:使异步代码更易读

  4. JSON序列化:考虑使用json_serializable自动生成模型类

三、Dio库深度探索

3.1 Dio的优势特性

  • 拦截器系统:全局处理请求和响应

  • FormData支持:简化文件上传

  • 请求取消:通过CancelToken实现

  • 超时配置:全局和单独请求级别

  • 下载进度:内置进度回调

  • 适配器系统:可自定义底层实现

3.2 高级配置示例

import 'package:dio/dio.dart';

class DioClient {
  final Dio _dio = Dio();
  
  DioClient() {
    // 全局配置
    _dio.options = BaseOptions(
      baseUrl: 'https://jsonplaceholder.typicode.com',
      connectTimeout: const Duration(seconds: 5),
      receiveTimeout: const Duration(seconds: 3),
      responseType: ResponseType.json,
    );
    
    // 拦截器
    _dio.interceptors.add(InterceptorsWrapper(
      onRequest: (options, handler) {
        // 添加认证token
        options.headers['Authorization'] = 'Bearer token';
        return handler.next(options);
      },
      onError: (error, handler) async {
        // 401自动刷新token
        if (error.response?.statusCode == 401) {
          try {
            final newToken = await refreshToken();
            error.requestOptions.headers['Authorization'] = 'Bearer $newToken';
            final response = await _dio.fetch(error.requestOptions);
            return handler.resolve(response);
          } catch (e) {
            return handler.reject(error);
          }
        }
        return handler.next(error);
      },
    ));
    
    // 日志拦截器
    _dio.interceptors.add(LogInterceptor(
      request: true,
      requestHeader: true,
      requestBody: true,
      responseHeader: true,
      responseBody: true,
    ));
  }
  
  Future<String> refreshToken() async {
    // 实现token刷新逻辑
    return 'new_token';
  }
  
  // 封装GET请求
  Future<Response> get(String path, {Map<String, dynamic>? params}) async {
    try {
      return await _dio.get(path, queryParameters: params);
    } on DioException catch (e) {
      throw _handleError(e);
    }
  }
  
  // 文件上传
  Future<Response> uploadFile(String path, String filePath) async {
    FormData formData = FormData.fromMap({
      'file': await MultipartFile.fromFile(filePath),
    });
    
    return await _dio.post(path, data: formData);
  }
  
  // 错误处理
  dynamic _handleError(DioException error) {
    switch (error.type) {
      case DioExceptionType.connectionTimeout:
        throw 'Connection timeout';
      case DioExceptionType.receiveTimeout:
        throw 'Receive timeout';
      // 其他错误类型处理...
      default:
        throw 'Network error';
    }
  }
}

3.3 Dio高级特性实战

3.3.1 文件分块上传
Future<void> uploadLargeFile(String filePath) async {
  final cancelToken = CancelToken();
  final file = File(filePath);
  final fileSize = await file.length();
  const chunkSize = 1024 * 1024; // 1MB
  
  try {
    for (var offset = 0; offset < fileSize; offset += chunkSize) {
      final chunk = await file.openRead(offset, offset + chunkSize);
      final formData = FormData.fromMap({
        'file': MultipartFile(
          chunk,
          chunkSize,
          filename: 'large_file.bin',
          contentType: MediaType('application', 'octet-stream'),
        ),
        'offset': offset,
      });
      
      await _dio.post(
        '/upload',
        data: formData,
        cancelToken: cancelToken,
        onSendProgress: (sent, total) {
          print('Upload progress: ${(sent / total * 100).toStringAsFixed(1)}%');
        },
      );
    }
    print('Upload completed');
  } catch (e) {
    if (CancelToken.isCancel(e)) {
      print('Upload cancelled');
    } else {
      rethrow;
    }
  }
}
3.3.2 并发请求管理
Future<List<dynamic>> fetchMultipleResources() async {
  final responses = await Future.wait([
    _dio.get('/posts'),
    _dio.get('/comments'),
    _dio.get('/users'),
  ]);
  
  return responses.map((response) => response.data).toList();
}

四、性能优化与安全

4.1 网络请求优化策略

  1. 连接复用:Dio默认使用HttpClient的连接池

  2. 数据压缩:添加Accept-Encoding: gzip

  3. 缓存策略:结合dio_cache_interceptor实现

  4. 请求合并:对频繁的小请求进行合并

  5. 分页加载:大数据集分批次请求

4.2 安全最佳实践

  1. HTTPS必须:所有请求使用HTTPS

  2. 证书锁定:实现CertificatePinning

  3. 敏感数据:不在URL中传递敏感参数

  4. Token管理:使用安全存储保存认证token

  5. 输入验证:服务端返回数据必须验证

五、测试与调试

5.1 Mock网络请求

// 使用http包的mock
import 'package:http/http.dart' as http;
import 'package:http/testing.dart';

void main() {
  test('测试获取帖子', () async {
    final client = MockClient((request) async {
      return http.Response(
        jsonEncode([{'id': 1, 'title': 'Test Post'}]),
        200,
        headers: {'content-type': 'application/json'},
      );
    });
    
    final service = PostService(client: client);
    final posts = await service.fetchPosts();
    expect(posts.length, 1);
  });
}

5.2 Dio Mock适配器

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

void main() {
  final dio = Dio();
  
  (dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = 
    (client) => client..findProxy = (uri) => 'DIRECT';
  
  // 或者使用MockAdapter
  dio.httpClientAdapter = MockAdapter()
    ..whenGet('/posts').reply(200, [{'id': 1}]);
}

六、总结与选择建议

经过对http包和Dio库的深度解析,我们可以得出以下结论:

  1. 简单项目:如果只是基础REST API调用,官方http包完全够用

  2. 复杂应用:需要拦截器、文件操作等高级功能时,Dio是更好的选择

  3. 特殊需求:考虑结合两者优势,或在Dio基础上进行二次封装

无论选择哪种方式,良好的架构设计比具体技术选型更重要。建议:

  • 实现统一的网络层抽象

  • 集中处理错误和日志

  • 考虑使用代码生成处理JSON序列化

  • 为网络层编写全面的测试用例

Flutter的网络生态系统仍在不断发展,掌握这些核心技能将帮助你构建更健壮的移动应用。


网站公告

今日签到

点亮在社区的每一天
去签到