Flutter: 网络请求与 REST API

没有网络的应用是座孤岛——API 请求是连接大陆的桥梁。

📋 前置知识:需要先掌握以下内容

1. 你将学到


2. 一个网络请求翻车的真实故事

(1) 痛点:网络错误让应用崩溃

Bob 的 ShopApp 上线第一天,用户在网络不稳定时直接白屏崩溃——没有 try-catch,没有超时,没有重试。更糟糕的是,Token 过期后所有请求返回 401,应用没有自动刷新 Token 的机制,用户被迫重新登录。一天内 2 thousand 个差评。

(2) Dio 拦截器的解法

Dio 的拦截器机制可以在请求发出前统一附加 Token,在响应返回后统一处理 401 自动刷新,网络超时自动重试。

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

// ⚙️ **安装依赖**:flutter pub add dio

// 假设 token 和 refreshToken 已在别处定义
// String token = '...';
// Future<String> refreshToken() async { ... }

// Auth interceptor: auto-attach token + auto-refresh on 401
dio.interceptors.add(InterceptorsWrapper(
  onRequest: (options, handler) {
    options.headers['Authorization'] = 'Bearer $token';
    handler.next(options);
  },
  onError: (error, handler) async {
    if (error.response?.statusCode == 401) {
      token = await refreshToken();
      return handler.resolve(await dio.fetch(error.requestOptions));
    }
    handler.next(error);
  },
));
TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

(3) 收益:网络异常零崩溃

Bob 用 Dio 拦截器后,401 自动刷新、超时重试 3 次、统一错误提示。网络相关崩溃率从 5% 降到 0.01%。


3. HTTP 基础请求

(1) http 包 CRUD 操作

方法 HTTP 动词 用途
http.get GET 获取资源
http.post POST 创建资源
http.put PUT 更新资源
http.delete DELETE 删除资源

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

:http 包基础请求

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

// ⚙️ **安装依赖**:flutter pub add http

// 自定义类定义来源:
// - Product: 见本课第5节 json_serializable 模型
// - ApiException: 见本课第6节统一错误处理

// 假设 token 已在别处定义
// String token = '...';

class ProductApi {
  static const String baseUrl = 'https://api.shopapp.com/v1';

  Future<List<Product>> getProducts({int page = 1}) async {
    final response = await http.get(
      Uri.parse('$baseUrl/products?page=$page'),
      headers: {'Authorization': 'Bearer $token'},
    );
    if (response.statusCode == 200) {
      final data = jsonDecode(response.body);
      return (data['items'] as List).map((j) => Product.fromJson(j)).toList();
    }
    throw ApiException(response.statusCode, response.body);
  }

  Future<Product> createProduct(Product product) async {
    final response = await http.post(
      Uri.parse('$baseUrl/products'),
      headers: {'Content-Type': 'application/json', 'Authorization': 'Bearer $token'},
      body: jsonEncode(product.toJson()),
    );
    if (response.statusCode == 201) {
      return Product.fromJson(jsonDecode(response.body));
    }
    throw ApiException(response.statusCode, response.body);
  }
}
TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

4. Dio 深度应用

(1) http vs Dio 对比

功能 http Dio
基础 CRUD
拦截器
CancelToken
FormData 上传 手动
超时重试 手动
请求日志 手动 ✅ (dio_logger)
全局配置 ✅ BaseOptions

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

:Dio 基础配置

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

// ⚙️ **安装依赖**:flutter pub add dio

// 自定义类定义来源:
// - Product: 见本课第5节 json_serializable 模型
// - Order/Cart: 见第14课 Phase 2 综合练习

final dio = Dio(BaseOptions(
  baseUrl: 'https://api.shopapp.com/v1',
  connectTimeout: const Duration(seconds: 10),
  receiveTimeout: const Duration(seconds: 30),
  headers: {
    'Content-Type': 'application/json',
    'Accept': 'application/json',
  },
));

// GET request
Future<List<Product>> getProducts({int page = 1}) async {
  final response = await dio.get('/products', queryParameters: {'page': page});
  return (response.data['items'] as List).map((j) => Product.fromJson(j)).toList();
}

// POST request
Future<Order> createOrder(Cart cart) async {
  final response = await dio.post('/orders', data: cart.toJson());
  return Order.fromJson(response.data);
}
TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

:Auth 拦截器 + Token 刷新

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

// 自定义类定义来源:
// - SecureStorage: 见第13课 Flutter Secure Storage
// - AuthService: 见第14课 Auth Notifier

class AuthInterceptor extends Interceptor {
  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    final token = SecureStorage.getToken();
    if (token != null) {
      options.headers['Authorization'] = 'Bearer $token';
    }
    handler.next(options);
  }

  @override
  void onError(DioException err, ErrorInterceptorHandler handler) async {
    if (err.response?.statusCode == 401) {
      try {
        final newToken = await _refreshToken();
        SecureStorage.saveToken(newToken);
        // Retry original request with new token
        err.requestOptions.headers['Authorization'] = 'Bearer $newToken';
        final response = await dio.fetch(err.requestOptions);
        return handler.resolve(response);
      } catch (_) {
        // Refresh failed, redirect to login
        AuthService.logout();
      }
    }
    handler.next(err);
  }

  Future<String> _refreshToken() async {
    final refreshToken = SecureStorage.getRefreshToken();
    final response = await Dio().post(
      'https://api.shopapp.com/v1/auth/refresh',
      data: {'refresh_token': refreshToken},
    );
    return response.data['access_token'];
  }
}
TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

:CancelToken 取消请求

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

// ⚙️ **安装依赖**:flutter pub add dio

CancelToken? _cancelToken;

void searchProducts(String query) {
  _cancelToken?.cancel('New search request');
  _cancelToken = CancelToken();
  dio.get('/products/search', queryParameters: {'q': query},
    cancelToken: _cancelToken);
}

// Cancel on dispose
@override
void dispose() {
  _cancelToken?.cancel();
  super.dispose();
}
TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

5. JSON 序列化

(1) 序列化方式对比

方式 优点 缺点 适用场景
手动 fromJson/toJson 无依赖、完全控制 重复代码、易出错 少量简单模型
json_serializable 类型安全、自动生成 需要 build_runner 大量模型
freezed 不可变 + copyWith + union 依赖更多 复杂领域模型

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

:json_serializable 模型

DART
import 'package:json_annotation/json_annotation.dart';

// ⚙️ **安装依赖**:flutter pub add json_annotation
// ⚙️ **开发依赖**:flutter pub add --dev json_serializable build_runner

part 'product.g.dart';

@JsonSerializable()
class Product {
  final int id;
  final String name;
  @JsonKey(name: 'unit_price')
  final double price;
  @JsonKey(name: 'image_url')
  final String? imageUrl;
  final double rating;

  const Product({
    required this.id,
    required this.name,
    required this.price,
    this.imageUrl,
    this.rating = 0.0,
  });

  factory Product.fromJson(Map<String, dynamic> json) => _$ProductFromJson(json);
  Map<String, dynamic> toJson() => _$ProductToJson(this);
}

// Run: dart run build_runner build --delete-conflicting-outputs
TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

6. 统一错误处理

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

:ApiException + 错误拦截器

DART
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';

// ⚙️ **安装依赖**:flutter pub add dio

class ApiException implements Exception {
  final int? statusCode;
  final String message;
  final String? errorCode;

  ApiException(this.statusCode, this.message, {this.errorCode});

  factory ApiException.fromDioError(DioException error) {
    switch (error.type) {
      case DioExceptionType.connectionTimeout:
      case DioExceptionType.sendTimeout:
      case DioExceptionType.receiveTimeout:
        return ApiException(null, 'Connection timeout. Please try again.');
      case DioExceptionType.connectionError:
        return ApiException(null, 'No internet connection.');
      case DioExceptionType.badResponse:
        final status = error.response?.statusCode;
        final msg = error.response?.data['message'] ?? 'Server error';
        if (status == 401) return ApiException(401, 'Session expired. Please login again.');
        if (status == 403) return ApiException(403, 'Access denied.');
        if (status == 404) return ApiException(404, 'Resource not found.');
        if (status == 422) return ApiException(422, msg, errorCode: error.response?.data['code']);
        if (status != null && status >= 500) return ApiException(status, 'Server error. Please try later.');
        return ApiException(status, msg);
      default:
        return ApiException(null, 'Unexpected error.');
    }
  }
}

// Usage in UI
try {
  final products = await productRepo.getProducts();
} on ApiException catch (e) {
  ScaffoldMessenger.of(context).showSnackBar(
    SnackBar(content: Text(e.message)),
  );
}
TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

7. 完整示例:ShopApp Dio Repository

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

// ⚙️ **安装依赖**:flutter pub add dio

// 自定义类定义来源:
// - Product: 见本课第5节 json_serializable 模型
// - CartItem/Address/Order: 见第14课 Phase 2 综合练习
// - AuthInterceptor: 见本课第4节 Auth 拦截器

class ProductRepository {
  final Dio _dio;

  ProductRepository(this._dio);

  Future<List<Product>> getProducts({
    int page = 1,
    int limit = 20,
    String? category,
    String? search,
  }) async {
    final params = {'page': page, 'limit': limit};
    if (category != null) params['category'] = category;
    if (search != null) params['q'] = search;

    final response = await _dio.get('/products', queryParameters: params);
    return (response.data['items'] as List)
        .map((j) => Product.fromJson(j))
        .toList();
  }

  Future<Product> getProductById(int id) async {
    final response = await _dio.get('/products/$id');
    return Product.fromJson(response.data);
  }

  Future<Order> createOrder(List<CartItem> items, Address address) async {
    final response = await _dio.post('/orders', data: {
      'items': items.map((i) => {'product_id': i.product.id, 'quantity': i.quantity}).toList(),
      'shipping_address': address.toJson(),
    });
    return Order.fromJson(response.data);
  }

  Future<String> uploadProductImage(String filePath) async {
    final formData = FormData.fromMap({
      'image': await MultipartFile.fromFile(filePath),
    });
    final response = await _dio.post('/upload', data: formData);
    return response.data['url'];
  }
}

// Dio singleton setup
final dio = Dio(BaseOptions(
  baseUrl: 'https://api.shopapp.com/v1',
  connectTimeout: const Duration(seconds: 10),
  receiveTimeout: const Duration(seconds: 30),
))
  ..interceptors.add(AuthInterceptor())
  ..interceptors.add(LogInterceptor(requestBody: true, responseBody: true));

final productRepo = ProductRepository(dio);
TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

❓ 常见问题

Q Dio 和 http 包怎么选?
A 小型项目用 http 足够;需要拦截器、取消请求、重试、FormData 上传等功能时选 Dio。
Q json_serializable 每次 build_runner 很慢?
A 开发时用 dart run build_runner watch --delete-conflicting-outputs 持续监听文件变化自动生成。
Q Token 刷新时多个请求同时 401 怎么办?
A 加锁机制:第一个 401 触发刷新,其他 401 请求排队等刷新完成。可用 Completer 实现。
Q FormData 上传大文件会 OOM 吗?
A MultipartFile.fromFile 是懒加载,不会一次性读入内存。大文件上传建议分片。
Q 开发环境怎么调试网络请求?
A 添加 LogInterceptor 打印请求/响应,或用 Charles/Proxyman 抓包,Dio 支持设置 proxy。
Q SSL 证书校验失败怎么办?
A 开发环境可用 HttpClientAdapter 跳过验证;生产环境必须验证证书。

📖 小节


📝 作业

  1. 基础题(难度⭐):用 http 包实现 GET 请求,获取商品列表并打印 JSON。
  2. 进阶题(难度⭐⭐):用 Dio 封装 Repository,添加 Auth 拦截器自动附加 Bearer Token,401 时自动刷新。
  3. 挑战题(难度⭐⭐⭐):实现完整的网络层:Dio + Auth 拦截器 + Log 拦截器 + CancelToken 搜索防抖 + ApiException 统一错误处理 + json_serializable 模型。

← 上一课 | 下一课 →

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏