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 与序列化

相关推荐
恋猫de小郭13 小时前
Android 限制侧载新进展,谷歌联合国内厂商推验证计划
android·前端·flutter
恋猫de小郭13 小时前
解读 Android 17 全新内存限制,有没有“豁免”后门?
android·前端·flutter
程序员老刘3 天前
跨平台开发地图 | 2026年6月
flutter·ai编程·客户端
悟空瞎说4 天前
Flutter 架构详解:新手必懂底层原理
flutter
霜落长河4 天前
抛弃TCP改用UDP,HTTP3怎么了?
http
SoaringHeart4 天前
Flutter最佳实践:IM聊天文字链接自动识别跳转
前端·flutter
恋猫de小郭4 天前
KMP / CMP 鸿蒙版本 Beta 发布,他有什么特别之处?
android·前端·flutter
网络研究院5 天前
2026年网络安全
网络·安全·法律·法规·趋势·发展
酣大智5 天前
ARP代理--工作原理
运维·网络·arp·arp代理
treesforest5 天前
AI安全系统如何识别异常访问?IP风险识别正在成为关键能力
网络·人工智能·tcp/ip·安全·web安全