5.1 HTTP 与网络请求

Flutter 中网络请求的核心是 http 包和 Dio。Dio 功能更强大,支持拦截器、取消请求和文件上传,是生产级项目的首选。


一、http 包(轻量级)

yaml 复制代码
dependencies:
  http: ^1.2.0

1.1 基本请求

dart 复制代码
import 'package:http/http.dart' as http;
import 'dart:convert';

class HttpService {
  static const baseUrl = 'https://api.example.com';

  // GET 请求
  static Future<List<Product>> fetchProducts() async {
    final response = await http.get(
      Uri.parse('$baseUrl/products'),
      headers: {
        'Authorization': 'Bearer ${AuthService.token}',
        'Content-Type': 'application/json',
      },
    );

    if (response.statusCode == 200) {
      final List<dynamic> json = jsonDecode(response.body);
      return json.map((e) => Product.fromJson(e)).toList();
    } else {
      throw HttpException('Failed: ${response.statusCode}');
    }
  }

  // POST 请求
  static Future<Order> createOrder(OrderRequest request) async {
    final response = await http.post(
      Uri.parse('$baseUrl/orders'),
      headers: {'Content-Type': 'application/json'},
      body: jsonEncode(request.toJson()),
    );

    if (response.statusCode == 201) {
      return Order.fromJson(jsonDecode(response.body));
    }
    throw HttpException('Create order failed');
  }
}

二、Dio(生产推荐)

yaml 复制代码
dependencies:
  dio: ^5.4.0

2.1 基础配置

dart 复制代码
import 'package:dio/dio.dart';

class DioClient {
  static DioClient? _instance;
  late final Dio _dio;

  DioClient._() {
    _dio = Dio(BaseOptions(
      baseUrl: AppConfig.apiBaseUrl,
      connectTimeout: const Duration(seconds: 10),
      receiveTimeout: const Duration(seconds: 30),
      headers: {
        'Content-Type': 'application/json',
        'Accept': 'application/json',
      },
    ));
  }

  factory DioClient() => _instance ??= DioClient._();

  Dio get dio => _dio;
}

2.2 拦截器(Interceptors)

dart 复制代码
class AuthInterceptor extends Interceptor {
  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    // 请求前:自动添加 Token
    final token = AuthStorage.getToken();
    if (token != null) {
      options.headers['Authorization'] = 'Bearer $token';
    }
    handler.next(options); // 继续
  }

  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) {
    // 响应后:统一处理业务状态码
    final data = response.data;
    if (data is Map && data['code'] != 0) {
      handler.reject(
        DioException(
          requestOptions: response.requestOptions,
          response: response,
          message: data['message'],
        ),
      );
    } else {
      handler.next(response);
    }
  }

  @override
  void onError(DioException err, ErrorInterceptorHandler handler) {
    // 错误处理:401 自动刷新 token
    if (err.response?.statusCode == 401) {
      _handleTokenExpired(err, handler);
    } else {
      handler.next(err);
    }
  }

  Future<void> _handleTokenExpired(
    DioException err,
    ErrorInterceptorHandler handler,
  ) async {
    try {
      final newToken = await AuthService.refreshToken();
      AuthStorage.saveToken(newToken);
      // 重试原请求
      err.requestOptions.headers['Authorization'] = 'Bearer $newToken';
      final response = await DioClient().dio.fetch(err.requestOptions);
      handler.resolve(response);
    } catch (e) {
      AuthService.logout();
      handler.reject(err);
    }
  }
}

class LogInterceptor extends Interceptor {
  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    debugPrint('→ ${options.method} ${options.path}');
    debugPrint('  Headers: ${options.headers}');
    if (options.data != null) debugPrint('  Body: ${options.data}');
    handler.next(options);
  }

  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) {
    debugPrint('← ${response.statusCode} ${response.requestOptions.path}');
    handler.next(response);
  }

  @override
  void onError(DioException err, ErrorInterceptorHandler handler) {
    debugPrint('✗ Error: ${err.message}');
    handler.next(err);
  }
}

// 注册拦截器
_dio.interceptors.addAll([
  AuthInterceptor(),
  LogInterceptor(),
  RetryInterceptor(dio: _dio, retries: 3), // 自动重试
]);

2.3 取消请求

dart 复制代码
class ProductApiService {
  final _dio = DioClient().dio;
  CancelToken? _searchCancelToken;

  // 搜索时取消上一次请求,避免竞态
  Future<List<Product>> search(String query) async {
    _searchCancelToken?.cancel('New search started');
    _searchCancelToken = CancelToken();

    try {
      final response = await _dio.get(
        '/products/search',
        queryParameters: {'q': query},
        cancelToken: _searchCancelToken,
      );
      return (response.data['items'] as List)
          .map((e) => Product.fromJson(e))
          .toList();
    } on DioException catch (e) {
      if (CancelToken.isCancel(e)) {
        return []; // 请求被取消,返回空列表
      }
      rethrow;
    }
  }
}

2.4 文件上传与下载

dart 复制代码
// 文件上传
Future<String> uploadAvatar(File file) async {
  final formData = FormData.fromMap({
    'file': await MultipartFile.fromFile(
      file.path,
      filename: 'avatar.jpg',
      contentType: MediaType('image', 'jpeg'),
    ),
  });

  final response = await _dio.post(
    '/upload/avatar',
    data: formData,
    onSendProgress: (sent, total) {
      final progress = sent / total;
      debugPrint('Upload: ${(progress * 100).toStringAsFixed(1)}%');
    },
  );
  return response.data['url'] as String;
}

// 文件下载
Future<void> downloadFile(String url, String savePath) async {
  await _dio.download(
    url,
    savePath,
    onReceiveProgress: (received, total) {
      if (total != -1) {
        final progress = received / total;
        // 更新下载进度 UI
      }
    },
  );
}

三、全局异常处理

dart 复制代码
class NetworkException implements Exception {
  final String message;
  final int? statusCode;
  final bool isNetworkError;

  const NetworkException({
    required this.message,
    this.statusCode,
    this.isNetworkError = false,
  });

  factory NetworkException.fromDioException(DioException e) {
    switch (e.type) {
      case DioExceptionType.connectionTimeout:
      case DioExceptionType.receiveTimeout:
        return const NetworkException(
          message: '请求超时,请检查网络连接',
          isNetworkError: true,
        );
      case DioExceptionType.badResponse:
        return NetworkException(
          message: e.response?.data?['message'] ?? '服务器错误',
          statusCode: e.response?.statusCode,
        );
      case DioExceptionType.cancel:
        return const NetworkException(message: '请求已取消');
      default:
        return const NetworkException(
          message: '网络连接失败,请检查网络',
          isNetworkError: true,
        );
    }
  }

  @override
  String toString() => 'NetworkException: $message (status: $statusCode)';
}

小结

功能 http 包 Dio
基本请求 ✅ ✅
拦截器 ❌ ✅
取消请求 ❌ ✅
文件上传 基础 完整(进度、FormData)
超时配置 ✅ ✅(更精细)
推荐场景 简单项目 生产项目

👉 下一节:5.2 JSON 与序列化

相关推荐
西索斯coding3 小时前
Qwen3-Max 与 Qwen-Plus:60 个真实修复任务评测方案
大数据·网络·算法·ai
瓶瓶持续学习中3 小时前
数字孪生与空间智能技术架构解析:五类厂商路线及选型方法
大数据·网络·架构
伤心的梧桐叶3 小时前
商品大图断点续传后打不开,先看是不是把完整响应又接了一遍
http·https
天启HTTP3 小时前
动态IP是如何自动切换的?从分配机制到轮换逻辑
网络
一只旭宝3 小时前
【网络精讲6】muduo 的组件与接口:可视化了解muduo库
网络·数据库
ReStock活库4 小时前
Zephyr异步SPI调用返回后,为什么不能立即复用发送缓冲区?
java·前端·网络
网络小江5 小时前
组网第33课:IP 地址与子网——CIDR、掩码、VLSM、网段规划与可用地址速算
网络协议·tcp/ip·子网掩码·vlsm·网络规划
山雀~5 小时前
WebSocket 断连之后:一次手机采集偶现卡死的排查与修复
网络·websocket·网络协议
Novproxy_Z5 小时前
机房 IP 和住宅 IP 怎么区分?IP ASN、归属信息检测脚本
linux·网络·tcp/ip
黑码哥5 小时前
OpenClaw 网络数据采集实战入门
网络