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

相关推荐
果汁华5 小时前
Typer:基于类型提示的现代Python CLI框架
开发语言·网络·python
提子拌饭1335 小时前
生命组学架构下的细胞分化与基因突变生存模拟器:基于鸿蒙Flutter的情景树渲染与状态溢出防御
flutter·华为·架构·开源·harmonyos
小比特_蓝光5 小时前
深入解析Linux进程:PCB到状态流转
网络
Flash.kkl6 小时前
应用层协议HTTP
网络·网络协议·http
齐潇宇6 小时前
文件共享服务器
linux·运维·网络·文件共享
@insist1236 小时前
网络工程师-虚拟专用网技术(二):高级架构详解
网络·网络工程师·软考·软件水平考试
添砖java‘’6 小时前
数据链路层
服务器·网络·数据链路层
xiaoyaohou116 小时前
012、骨干网络改进(三):CSPNet与跨阶段局部网络的深度优化
网络
空中海6 小时前
6.1 主题与暗色模式
运维·服务器·前端·flutter