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

相关推荐
神奇的程序员12 小时前
开发了一个管理本地开发环境的软件
前端·flutter
qq_5895681012 小时前
springbootweb案例,出现访问 http://localhost:8080/list 一直处于浏览器运转阶段
java·网络协议·http·list·springboot
代码中介商13 小时前
Linux TCP 网络编程完全指南:从三次握手到高并发服务器
服务器·网络·tcp/ip
xmdy586613 小时前
Flutter+开源鸿蒙实战|智联邻里Day9 系统权限适配+应用全局分享+缓存深度优化+版本更新弹窗
flutter·开源·harmonyos
咖喱o13 小时前
QinQ/VLAN Stacking
linux·运维·服务器·网络
AI周红伟15 小时前
周红伟:运营商一季度净利集体下滑 Token运营提速
大数据·网络·人工智能
marsh020615 小时前
43 openclaw熔断与降级:保障系统在异常情况下的可用性
java·运维·网络·ai·编程·技术
汽车仪器仪表相关领域16 小时前
Kvaser Memorator Professional 5xHS CB:五通道CAN FD裸板记录仪,赋能多总线系统集成测试的旗舰级核心装备
大数据·网络·人工智能·单元测试·汽车·集成测试
初学者,亦行者17 小时前
计算机网络必考:一文吃透 TCP/IP 体系结构(附高清思维导图)
网络·tcp/ip
段一凡-华北理工大学17 小时前
【高炉炼铁领域炉温监测、预警、调控智能体设计与应用】~系列文章10:实时预警机制:跑在问题前面!
网络·人工智能·python·知识图谱·高炉炼铁·工业智能体