欢迎大家加入[开源鸿蒙跨平台开发者社区](https://openharmonycrossplatform.csdn.net),一起共建开源鸿蒙跨平台生态。

网络请求是 Flutter 应用与后端交互的 “桥梁”—— 登录、数据展示、文件上传下载等核心功能都离不开网络请求。但很多开发者仅停留在 “能请求” 的层面,忽略了异常处理、请求拦截、数据缓存、断点续传等关键场景。本文将从基础的 GET/POST 请求入手,逐步实现拦截器、请求缓存、文件断点续传等进阶功能,既有严谨的代码规范,又有生动的场景拆解,让你彻底掌握 Flutter 网络请求的精髓。

一、Flutter 网络请求核心认知:为什么选择 Dio?

先理清 Flutter 网络请求的核心逻辑,明确技术选型的底层逻辑:

  • 原生 HttpClient 的痛点:原生HttpClientAPI 繁琐,不支持拦截器、FormData、文件上传等高级功能,需手动处理序列化 / 反序列化;
  • Dio 的优势:Dio 是 Flutter 生态中最主流的网络请求库,支持拦截器、FormData、文件上传下载、超时控制、取消请求等,API 简洁且扩展性强;
  • 核心设计原则:网络请求需遵循 “异常兜底、数据解析、状态管理” 三位一体的原则,避免崩溃和数据错乱。

本文所有代码基于:

plaintext

Flutter 3.26.0
Dart 3.6.0
dio: ^5.5.0
shared_preferences: ^2.2.2 # 缓存存储
path_provider: ^2.1.2 # 本地文件路径

二、入门:基础 GET/POST 请求 + 数据解析

先实现最基础的 GET/POST 请求,掌握 Dio 的核心用法和 JSON 数据解析,这是网络请求的基础模板。

2.1 第一步:封装基础 Dio 实例

创建utils/dio_client.dart,封装全局 Dio 实例,统一配置基础参数:

dart

import 'package:dio/dio.dart';

// 全局Dio实例封装
class DioClient {
  static late Dio _dio;

  // 初始化Dio配置
  static void init() {
    _dio = Dio(
      BaseOptions(
        // 基础URL(实际开发中替换为真实接口地址)
        baseUrl: 'https://api.example.com/v1',
        // 连接超时时间
        connectTimeout: const Duration(seconds: 5),
        // 接收超时时间
        receiveTimeout: const Duration(seconds: 3),
        // 响应数据类型
        responseType: ResponseType.json,
        // 请求头
        headers: {
          'Content-Type': 'application/json',
          'Accept': 'application/json',
        },
      ),
    );
  }

  // 获取Dio实例
  static Dio get instance => _dio;

  // 简化GET请求
  static Future<T> get<T>(
    String path, {
    Map<String, dynamic>? queryParameters,
    Options? options,
    CancelToken? cancelToken,
  }) async {
    try {
      final response = await _dio.get(
        path,
        queryParameters: queryParameters,
        options: options,
        cancelToken: cancelToken,
      );
      return response.data as T;
    } on DioException catch (e) {
      throw _handleDioError(e);
    }
  }

  // 简化POST请求
  static Future<T> post<T>(
    String path, {
    dynamic data,
    Map<String, dynamic>? queryParameters,
    Options? options,
    CancelToken? cancelToken,
  }) async {
    try {
      final response = await _dio.post(
        path,
        data: data,
        queryParameters: queryParameters,
        options: options,
        cancelToken: cancelToken,
      );
      return response.data as T;
    } on DioException catch (e) {
      throw _handleDioError(e);
    }
  }

  // 统一异常处理
  static String _handleDioError(DioException e) {
    switch (e.type) {
      case DioExceptionType.connectionTimeout:
        return '网络连接超时,请检查网络';
      case DioExceptionType.receiveTimeout:
        return '数据接收超时,请稍后重试';
      case DioExceptionType.badResponse:
        return '接口返回错误:${e.response?.statusCode}';
      case DioExceptionType.cancel:
        return '请求已取消';
      case DioExceptionType.connectionError:
        return '网络连接失败,请检查网络';
      default:
        return '请求失败:${e.message ?? '未知错误'}';
    }
  }
}

代码解析

  • 全局单例 Dio:通过静态方法init初始化 Dio,避免重复创建实例;
  • BaseOptions 配置:统一设置基础 URL、超时时间、请求头等,减少重复代码;
  • 简化 GET/POST 方法:封装通用请求方法,自动处理异常并抛出友好提示;
  • 统一异常处理:将 Dio 的各类异常转换为用户友好的提示文字,避免直接抛出技术异常。

2.2 第二步:初始化 Dio 并实现基础请求

修改main.dart,在应用启动时初始化 Dio:

dart

import 'package:flutter/material.dart';
import 'utils/dio_client.dart';
import 'pages/network_demo_page.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  // 初始化Dio
  DioClient.init();
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter网络请求实战',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: const NetworkDemoPage(),
    );
  }
}

2.3 第三步:实现 GET/POST 请求与数据解析

创建pages/network_demo_page.dart,实现用户列表 GET 请求和用户创建 POST 请求:

dart

import 'package:flutter/material.dart';
import 'utils/dio_client.dart';

// 数据模型:用户
class User {
  final int id;
  final String name;
  final String email;

  User({required this.id, required this.name, required this.email});

  // 从JSON解析模型
  factory User.fromJson(Map<String, dynamic> json) {
    return User(
      id: json['id'],
      name: json['name'],
      email: json['email'],
    );
  }

  // 转换为JSON
  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'name': name,
      'email': email,
    };
  }
}

class NetworkDemoPage extends StatefulWidget {
  const NetworkDemoPage({super.key});

  @override
  State<NetworkDemoPage> createState() => _NetworkDemoPageState();
}

class _NetworkDemoPageState extends State<NetworkDemoPage> {
  // 加载状态
  bool _isLoading = false;
  // 用户列表
  List<User> _userList = [];
  // 错误提示
  String? _errorMsg;

  // 获取用户列表(GET请求)
  Future<void> _fetchUserList() async {
    setState(() {
      _isLoading = true;
      _errorMsg = null;
    });

    try {
      // 发起GET请求(模拟接口:/users)
      final response = await DioClient.get<Map<String, dynamic>>(
        '/users',
        queryParameters: {'page': 1, 'size': 10}, // 请求参数
      );

      // 解析JSON数据
      final List<dynamic> dataList = response['data'];
      setState(() {
        _userList = dataList.map((json) => User.fromJson(json)).toList();
      });
    } catch (e) {
      setState(() {
        _errorMsg = e.toString();
      });
    } finally {
      setState(() {
        _isLoading = false;
      });
    }
  }

  // 创建用户(POST请求)
  Future<void> _createUser() async {
    setState(() {
      _isLoading = true;
      _errorMsg = null;
    });

    try {
      // 构建请求体
      final userData = User(
        id: 0, // 后端生成ID
        name: 'Flutter测试用户',
        email: 'flutter_test@example.com',
      ).toJson();

      // 发起POST请求
      await DioClient.post(
        '/users',
        data: userData,
      );

      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('用户创建成功!')),
        );
        // 重新获取用户列表
        _fetchUserList();
      }
    } catch (e) {
      setState(() {
        _errorMsg = e.toString();
      });
    } finally {
      setState(() {
        _isLoading = false;
      });
    }
  }

  @override
  void initState() {
    super.initState();
    // 页面加载时获取用户列表
    _fetchUserList();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('基础网络请求')),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            // 操作按钮
            Row(
              children: [
                ElevatedButton(
                  onPressed: _isLoading ? null : _fetchUserList,
                  child: const Text('刷新用户列表'),
                ),
                const SizedBox(width: 16),
                ElevatedButton(
                  onPressed: _isLoading ? null : _createUser,
                  child: const Text('创建测试用户'),
                ),
              ],
            ),
            const SizedBox(height: 20),

            // 加载状态/错误提示/用户列表
            _isLoading
                ? const Center(child: CircularProgressIndicator())
                : _errorMsg != null
                    ? Center(
                        child: Text(
                          _errorMsg!,
                          style: const TextStyle(color: Colors.red, fontSize: 16),
                        ),
                      )
                    : Expanded(
                        child: ListView.builder(
                          itemCount: _userList.length,
                          itemBuilder: (context, index) {
                            final user = _userList[index];
                            return ListTile(
                              key: ValueKey(user.id),
                              title: Text(user.name),
                              subtitle: Text(user.email),
                            );
                          },
                        ),
                      ),
          ],
        ),
      ),
    );
  }
}

核心解析

  1. 数据模型设计

    • 定义User模型类,通过fromJson/toJson方法实现 JSON 与模型的互转,避免直接操作 Map,提升代码可读性;
    • 模型类的字段与接口返回字段一一对应,减少解析错误。
  2. 请求状态管理

    • 通过_isLoading控制加载状态,避免重复请求;
    • _errorMsg存储错误提示,友好展示给用户;
    • 所有异步操作包裹在try-catch中,防止崩溃。
  3. 请求参数处理

    • GET 请求通过queryParameters传递 URL 参数;
    • POST 请求通过data传递 JSON 请求体;
    • 所有请求都关联页面生命周期,通过mounted判断页面是否挂载。

三、进阶:拦截器实现 + 请求 / 响应统一处理

拦截器是 Dio 的核心特性,可实现请求头自动添加、响应数据统一解析、Token 过期自动刷新等功能,是企业级开发的必备技能。

3.1 实现通用拦截器

修改utils/dio_client.dart,添加请求拦截器和响应拦截器:

dart

import 'package:dio/dio.dart';

class DioClient {
  static late Dio _dio;

  static void init() {
    _dio = Dio(
      BaseOptions(
        baseUrl: 'https://api.example.com/v1',
        connectTimeout: const Duration(seconds: 5),
        receiveTimeout: const Duration(seconds: 3),
        responseType: ResponseType.json,
        headers: {
          'Content-Type': 'application/json',
          'Accept': 'application/json',
        },
      ),
    );

    // 添加请求拦截器
    _dio.interceptors.add(
      InterceptorsWrapper(
        // 请求发送前拦截
        onRequest: (RequestOptions options, RequestInterceptorHandler handler) {
          // 1. 自动添加Token(实际开发中从本地存储获取)
          final token = 'your_token_here';
          if (token.isNotEmpty) {
            options.headers['Authorization'] = 'Bearer $token';
          }

          // 2. 打印请求日志(调试用)
          debugPrint('请求URL:${options.uri}');
          debugPrint('请求参数:${options.data}');

          // 继续执行请求
          handler.next(options);
        },

        // 响应返回后拦截
        onResponse: (Response response, ResponseInterceptorHandler handler) {
          // 1. 统一解析响应数据(假设接口返回格式:{code: 200, data: ..., msg: ''})
          final Map<String, dynamic> responseData = response.data;
          if (responseData['code'] == 200) {
            // 只返回data部分,简化上层解析
            response.data = responseData['data'];
          } else {
            // 非200状态码抛出异常
            handler.reject(
              DioException(
                requestOptions: response.requestOptions,
                response: response,
                type: DioExceptionType.badResponse,
                message: responseData['msg'] ?? '接口返回错误',
              ),
              true,
            );
            return;
          }

          // 2. 打印响应日志
          debugPrint('响应数据:${response.data}');

          // 继续处理响应
          handler.next(response);
        },

        // 请求失败时拦截
        onError: (DioException e, ErrorInterceptorHandler handler) {
          // 1. Token过期自动刷新(示例逻辑)
          if (e.response?.statusCode == 401) {
            // 这里可实现Token刷新逻辑,刷新后重新发起请求
            debugPrint('Token过期,准备刷新');
            // 简化处理:直接抛出未登录异常
            handler.reject(
              DioException(
                requestOptions: e.requestOptions,
                message: '登录状态已过期,请重新登录',
                type: DioExceptionType.badResponse,
              ),
              true,
            );
            return;
          }

          // 2. 统一错误处理
          handler.next(e);
        },
      ),
    );
  }

  // 其余代码不变...
}

拦截器核心逻辑解析

  1. 请求拦截器(onRequest)

    • 自动添加 Authorization 请求头,无需在每个请求中手动设置;
    • 打印请求日志,便于调试;
    • 可扩展:添加请求加密、参数签名等逻辑。
  2. 响应拦截器(onResponse)

    • 统一解析接口返回格式,假设接口返回{code: 200, data: ..., msg: ''},只向上层返回data部分,简化解析逻辑;
    • 非 200 状态码直接抛出异常,上层只需处理业务逻辑。
  3. 错误拦截器(onError)

    • 捕获 401 状态码,实现 Token 过期自动刷新(示例中简化为提示重新登录);
    • 可扩展:添加重试机制、错误日志上报等。

3.2 拦截器使用效果

添加拦截器后,上层代码无需关心:

  • Token 的添加:拦截器自动注入;
  • 响应数据的统一解析:直接获取data部分;
  • Token 过期处理:拦截器统一捕获并提示。

四、高阶 1:请求缓存实现(避免重复请求)

实际开发中,对于不常变化的数据(如首页分类、用户信息),可添加缓存逻辑,减少网络请求,提升页面加载速度。

4.1 封装缓存工具类

创建utils/cache_manager.dart

dart

import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';

// 缓存管理工具
class CacheManager {
  static late SharedPreferences _prefs;

  // 初始化
  static Future<void> init() async {
    _prefs = await SharedPreferences.getInstance();
  }

  // 保存缓存
  static Future<void> setCache(String key, dynamic data, {Duration? expireTime}) {
    final cacheData = {
      'data': data,
      'timestamp': DateTime.now().millisecondsSinceEpoch,
      'expireTime': expireTime?.inMilliseconds ?? 0,
    };
    return _prefs.setString(key, json.encode(cacheData));
  }

  // 获取缓存
  static dynamic getCache(String key) {
    final cacheStr = _prefs.getString(key);
    if (cacheStr == null) return null;

    final cacheData = json.decode(cacheStr);
    final timestamp = cacheData['timestamp'] as int;
    final expireTime = cacheData['expireTime'] as int;

    // 判断是否过期
    if (expireTime > 0) {
      final now = DateTime.now().millisecondsSinceEpoch;
      if (now - timestamp > expireTime) {
        // 过期则删除缓存
        removeCache(key);
        return null;
      }
    }

    return cacheData['data'];
  }

  // 删除缓存
  static Future<void> removeCache(String key) {
    return _prefs.remove(key);
  }

  // 清空所有缓存
  static Future<void> clearAllCache() {
    return _prefs.clear();
  }
}

4.2 实现带缓存的 GET 请求

修改utils/dio_client.dart,添加带缓存的 GET 请求方法:

dart

// 带缓存的GET请求
static Future<T> getWithCache<T>(
  String path, {
  Map<String, dynamic>? queryParameters,
  Options? options,
  CancelToken? cancelToken,
  Duration? cacheDuration = const Duration(minutes: 10), // 默认缓存10分钟
}) async {
  // 构建缓存key
  final cacheKey = '$path-${json.encode(queryParameters ?? {})}';
  
  // 先从缓存获取
  final cacheData = CacheManager.getCache(cacheKey);
  if (cacheData != null) {
    return cacheData as T;
  }

  // 缓存未命中,发起网络请求
  try {
    final response = await _dio.get(
      path,
      queryParameters: queryParameters,
      options: options,
      cancelToken: cancelToken,
    );
    // 保存缓存
    await CacheManager.setCache(cacheKey, response.data, expireTime: cacheDuration);
    return response.data as T;
  } on DioException catch (e) {
    throw _handleDioError(e);
  }
}

4.3 使用带缓存的请求

NetworkDemoPage中替换_fetchUserList方法:

dart

Future<void> _fetchUserList() async {
  setState(() {
    _isLoading = true;
    _errorMsg = null;
  });

  try {
    // 使用带缓存的GET请求
    final response = await DioClient.getWithCache<Map<String, dynamic>>(
      '/users',
      queryParameters: {'page': 1, 'size': 10},
      cacheDuration: const Duration(minutes: 5), // 缓存5分钟
    );

    final List<dynamic> dataList = response['data'];
    setState(() {
      _userList = dataList.map((json) => User.fromJson(json)).toList();
    });
  } catch (e) {
    setState(() {
      _errorMsg = e.toString();
    });
  } finally {
    setState(() {
      _isLoading = false;
    });
  }
}

缓存逻辑解析

  • 缓存 Key 由请求路径 + 参数组成,避免不同参数的请求缓存冲突;
  • 优先读取缓存,缓存未命中或过期时才发起网络请求;
  • 缓存时长可自定义,灵活适配不同业务场景。

五、高阶 2:文件断点续传下载

文件下载是高频场景,断点续传能避免网络中断后重新下载,提升用户体验。

5.1 封装断点续传下载工具

创建utils/download_manager.dart

dart

import 'package:dio/dio.dart';
import 'package:path_provider/path_provider.dart';
import 'dart:io';

// 下载进度回调
typedef DownloadProgressCallback = void Function(int count, int total);

// 断点续传下载管理器
class DownloadManager {
  // 开始下载
  static Future<void> downloadFile(
    String url,
    String saveFileName, {
    required DownloadProgressCallback onProgress,
    required Function(String path) onSuccess,
    required Function(String error) onError,
  }) async {
    try {
      // 获取本地存储路径
      final directory = await getExternalStorageDirectory();
      if (directory == null) {
        onError('无法获取存储路径');
        return;
      }

      final savePath = '${directory.path}/$saveFileName';
      final file = File(savePath);
      int start = 0;

      // 如果文件已存在,获取文件大小作为起始位置
      if (file.existsSync()) {
        start = file.lengthSync();
      }

      // 发起断点续传请求
      final dio = Dio();
      await dio.download(
        url,
        savePath,
        options: Options(
          headers: {
            'Range': 'bytes=$start-', // 断点续传核心:指定起始字节
          },
        ),
        onReceiveProgress: onProgress,
        // 追加写入文件
        deleteOnError: false,
      );

      onSuccess(savePath);
    } catch (e) {
      onError(e.toString());
    }
  }
}

5.2 实现文件下载页面

创建pages/download_page.dart

dart

import 'package:flutter/material.dart';
import 'utils/download_manager.dart';

class DownloadPage extends StatefulWidget {
  const DownloadPage({super.key});

  @override
  State<DownloadPage> createState() => _DownloadPageState();
}

class _DownloadPageState extends State<DownloadPage> {
  // 下载进度
  double _progress = 0.0;
  // 下载状态
  bool _isDownloading = false;
  // 下载提示
  String _downloadMsg = '未开始下载';

  // 开始下载
  void _startDownload() {
    setState(() {
      _isDownloading = true;
      _progress = 0.0;
      _downloadMsg = '下载中...';
    });

    // 模拟下载地址(实际替换为真实文件地址)
    const downloadUrl = 'https://example.com/file/test.pdf';
    const fileName = 'test.pdf';

    DownloadManager.downloadFile(
      downloadUrl,
      fileName,
      onProgress: (count, total) {
        if (total <= 0) return;
        setState(() {
          _progress = count / total;
          _downloadMsg = '下载中:${(count / total * 100).toStringAsFixed(1)}%';
        });
      },
      onSuccess: (path) {
        setState(() {
          _isDownloading = false;
          _downloadMsg = '下载完成:$path';
        });
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('文件下载成功!')),
        );
      },
      onError: (error) {
        setState(() {
          _isDownloading = false;
          _downloadMsg = '下载失败:$error';
        });
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('下载失败:$error')),
        );
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('断点续传下载')),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            // 下载进度条
            LinearProgressIndicator(
              value: _progress,
              minHeight: 10,
              backgroundColor: Colors.grey[200],
              valueColor: const AlwaysStoppedAnimation<Color>(Colors.blue),
            ),
            const SizedBox(height: 20),
            // 下载状态提示
            Text(
              _downloadMsg,
              style: const TextStyle(fontSize: 16),
            ),
            const SizedBox(height: 40),
            // 下载按钮
            ElevatedButton(
              onPressed: _isDownloading ? null : _startDownload,
              style: ElevatedButton.styleFrom(
                padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 12),
              ),
              child: const Text('开始下载文件'),
            ),
          ],
        ),
      ),
    );
  }
}

断点续传核心解析

  • Range请求头:指定bytes=$start-,告诉服务器从指定字节开始传输,实现断点续传;
  • 文件追加写入:deleteOnError: false确保下载中断后文件不被删除,下次可继续下载;
  • 进度回调:onReceiveProgress实时获取下载进度,更新 UI 展示。

六、网络请求避坑指南

  1. 异常处理
    • 所有网络请求必须包裹try-catch,避免未捕获异常导致崩溃;
    • 区分网络异常、接口异常、业务异常,分别给出友好提示;
  2. 内存泄漏
    • 使用CancelToken取消未完成的请求(如页面销毁时);
    • 异步回调中必须判断mounted,避免操作已销毁的页面;
  3. 缓存策略
    • 缓存只适用于非敏感、不常变化的数据(如分类、配置);
    • 敏感数据(如用户信息)禁止缓存,或设置极短的缓存时长;
  4. 断点续传
    • 确保服务器支持Range请求头,否则断点续传无效;
    • 大文件下载建议分块下载,避免内存溢出;
  5. 调试技巧
    • 使用 Dio 的日志拦截器(LogInterceptor)打印完整的请求 / 响应日志;
    • 测试时模拟各种异常场景(断网、超时、401/404/500)。

七、总结

Flutter 网络请求的学习路径是 “基础请求→拦截器→缓存→高级功能”,核心原则是 “统一封装、异常兜底、体验优先”:

  1. 基础请求:封装全局 Dio 实例,统一处理请求参数和数据解析;
  2. 拦截器:实现请求头自动添加、响应统一解析、Token 过期处理;
  3. 缓存策略:减少重复请求,提升页面加载速度;
  4. 高级功能:断点续传下载、文件上传、取消请求等,适配复杂业务场景。

网络请求是 Flutter 应用的 “生命线”,写得规范与否直接影响应用的稳定性和用户体验。比如统一的异常处理能避免崩溃,缓存策略能提升加载速度,断点续传能优化大文件下载体验。希望本文的实战案例和原理解析,能让你避开网络请求的 “坑”,写出既严谨又高性能的 Flutter 网络请求代码。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐