这节课是 Flutter 实现前后端交互的核心也是从「本地模拟数据」到「真实业务开发」的关键一步。我们会系统学习 Flutter 最主流的网络请求库Dio掌握GET/POST 基础请求、请求 / 响应拦截器统一处理请求头、错误、加载状态、JSON 数据解析手动解析 json_serializable自动解析企业级标准并结合上节课的列表知识实现真实接口驱动的下拉刷新 / 上拉加载列表完成从接口请求到 UI 渲染的全流程闭环。课前回顾滚动列表ListView.builder/GridView.builder懒加载、下拉刷新RefreshIndicator、上拉加载ScrollController状态管理StatefulWidget维护列表数据 / 加载状态异步操作判断mounted前置准备需要一个测试接口本节课使用免费公开的 REST APIhttps://jsonplaceholder.typicode.com提供用户、帖子、相册等模拟接口无需后端开发直接调用。一、网络请求基础为什么选择 DioFlutter 原生提供了HttpClient用于网络请求但原生 API 繁琐不支持拦截器、请求取消、FormData、文件上传 / 下载等高级功能实际开发中几乎不会直接使用。Dio是 Flutter 生态中最主流、功能最完善的网络请求库支持GET/POST/PUT/DELETE 等所有 HTTP 请求方法请求 / 响应拦截器统一处理 token、请求头、错误、日志FormData、文件上传 / 下载、断点续传超时设置、请求取消、缓存控制全局配置、多实例配置适配复杂业务场景。第一步集成 Dio在项目的pubspec.yaml文件中添加 Dio 依赖然后执行flutter pub get安装yamldependencies: flutter: sdk: flutter dio: ^5.4.0 # 推荐使用最新稳定版二、Dio 核心使用全局配置 基础 GET/POST 请求实际开发中会对 Dio 进行全局初始化配置基础 URL、超时时间、请求头避免重复代码然后封装通用的请求方法接下来从基础配置到实际请求逐步实现。1. Dio 全局初始化配置企业级规范创建lib/utils/network_utils.dart文件封装 Dio 全局实例统一配置基础参数这是企业开发的标准写法便于后续统一维护和修改。dartimport package:dio/dio.dart; // 全局Dio实例所有网络请求都使用该实例 final Dio dio Dio(); /// 初始化Dio配置在APP启动时执行main函数中 void initDio() { // 1. 配置基础URL接口域名后续请求只需写接口路径 dio.options.baseUrl https://jsonplaceholder.typicode.com/; // 2. 配置超时时间连接超时、接收超时各5秒 dio.options.connectTimeout const Duration(seconds: 5); dio.options.receiveTimeout const Duration(seconds: 5); // 3. 配置默认请求头如Content-Type、Accept dio.options.headers { Content-Type: application/json;charsetutf-8, Accept: application/json, }; // 4. 配置响应类型默认JSON自动解析为MapString, dynamic dio.options.responseType ResponseType.json; // 可选添加日志拦截器开发环境使用生产环境注释 dio.interceptors.add(LogInterceptor( request: true, // 打印请求信息 requestBody: true, // 打印请求体 responseBody: true, // 打印响应体 responseHeader: false, // 不打印响应头 error: true, // 打印错误信息 )); }关键配置说明baseUrl接口基础域名后续请求只需传接口路径如/postsDio 会自动拼接为https://jsonplaceholder.typicode.com/poststimeout超时时间建议设置 5 秒避免请求长时间阻塞headers默认 JSON 请求头适配绝大多数后端接口LogInterceptorDio 内置的日志拦截器开发时开启方便调试接口生产环境需关闭避免泄露敏感信息。2. 在 main 函数中初始化 Dio修改项目根目录的main.dart在运行 APP 前执行initDio保证全局 Dio 实例已配置完成dartimport package:flutter/material.dart; import package:xxx/utils/network_utils.dart; // 替换为你的项目包名 import pages/network_list_page.dart; // 后续创建的网络请求列表页 void main() { // 初始化Dio全局配置 initDio(); 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), debugShowCheckedModeBanner: false, home: const NetworkListPage(), // 网络请求列表页 ); } }3. 基础 GET 请求获取列表数据GET 请求适用于查询数据如获取列表、详情无请求体参数通过URL 拼接?key1value1key2value2Dio 会自动处理参数拼接无需手动写。实战GET 请求获取帖子列表接口/postsdart// 封装GET请求获取帖子列表 FutureListMapString, dynamic getPostList({int page 1, int size 10}) async { try { // Dio的get方法第一个参数为接口路径queryParameters为GET请求参数 Response response await dio.get( /posts, queryParameters: { page: page, limit: size, }, ); // 接口返回成功状态码200返回数据 if (response.statusCode 200) { return ListMapString, dynamic.from(response.data); } else { throw Exception(请求失败状态码${response.statusCode}); } } on DioException catch (e) { // 捕获Dio专属异常网络错误、超时、接口错误等 throw Exception(网络请求失败${e.message}); } catch (e) { // 捕获其他未知异常 throw Exception(未知错误$e); } }关键说明queryParametersGET 请求的参数集合Dio 会自动拼接为/posts?page1limit10DioExceptionDio 的专属异常类包含网络错误、超时、请求取消、接口返回错误等所有网络相关异常必须优先捕获统一异常抛出所有异常统一抛出为Exception上层调用时通过try/catch捕获统一处理错误提示。4. 基础 POST 请求提交 / 修改数据POST 请求适用于提交 / 修改数据如登录、注册、提交表单参数通过请求体传递JSON 格式Dio 会自动将data参数转为 JSON 字符串。实战POST 请求创建帖子接口/postsdart// 封装POST请求创建帖子 FutureMapString, dynamic createPost({ required String title, required String body, required int userId, }) async { try { // Dio的post方法data为POST请求体JSON参数 Response response await dio.post( /posts, data: { title: title, body: body, userId: userId, }, ); if (response.statusCode 201 || response.statusCode 200) { return MapString, dynamic.from(response.data); } else { throw Exception(创建失败状态码${response.statusCode}); } } on DioException catch (e) { throw Exception(网络请求失败${e.message}); } catch (e) { throw Exception(未知错误$e); } }关键说明dataPOST 请求的请求体支持Map/ListDio 会自动根据请求头转为 JSON 字符串状态码创建数据成功的状态码通常为201 Created查询 / 修改为 200需根据后端接口规范判断。三、Dio 拦截器统一处理请求 / 响应企业级核心拦截器是 Dio 的核心功能也是企业开发中必须使用的功能用于统一处理所有请求的请求头、token、加载状态、错误提示、响应数据格式化避免在每个请求中重复写相同代码。Dio 支持 4 种拦截器开发中最常用请求拦截器OnRequest和响应拦截器OnResponse/OnError三者结合可实现全流程统一处理。实战添加全局统一拦截器修改network_utils.dart的initDio方法添加自定义拦截器实现请求拦截统一添加 token、刷新 token响应成功拦截统一格式化响应数据只返回业务数据响应错误拦截统一处理网络错误、超时、接口错误给出友好提示。dartvoid initDio() { // ... 省略之前的基础配置 // 添加自定义全局拦截器 dio.interceptors.add(InterceptorsWrapper( // 1. 请求拦截请求发送前执行 onRequest: (RequestOptions options, RequestInterceptorHandler handler) { // 示例统一添加token从本地缓存中获取如SharedPreferences String? token your_token_from_local; // 实际开发中替换为真实token if (token ! null token.isNotEmpty) { options.headers[Authorization] Bearer $token; } // 继续执行请求 handler.next(options); }, // 2. 响应成功拦截接口返回成功后执行 onResponse: (Response response, ResponseInterceptorHandler handler) { // 示例统一格式化响应数据假设后端返回格式为 {code:0, msg:成功, data:{...}} // 可在此处统一判断code只返回data给上层 handler.next(response); }, // 3. 响应错误拦截请求失败网络/超时/接口错误时执行 onError: (DioException e, ErrorInterceptorHandler handler) { // 统一处理错误信息 String errorMsg _handleDioError(e); // 示例全局弹出错误提示可结合FlutterToast/Dialog print(全局网络错误$errorMsg); // 抛出错误上层可继续捕获 handler.reject(DioException(requestOptions: e.requestOptions, message: errorMsg)); }, )); // 日志拦截器... } /// 统一处理Dio异常返回友好的错误提示 String _handleDioError(DioException e) { switch (e.type) { case DioExceptionType.connectionTimeout: return 网络连接超时请检查网络; case DioExceptionType.sendTimeout: return 请求发送超时请稍后重试; case DioExceptionType.receiveTimeout: return 响应接收超时请稍后重试; case DioExceptionType.connectionError: return 网络连接错误请检查网络; case DioExceptionType.cancel: return 请求已取消; case DioExceptionType.badResponse: // 接口返回错误状态码404/500等 return 接口请求失败状态码${e.response?.statusCode}; default: return e.message ?? 未知网络错误; } }拦截器核心要点执行顺序请求拦截→发送请求→响应成功 / 错误拦截→上层业务代码handler.next()继续执行后续流程必须调用否则请求会被阻塞handler.reject()中断请求抛出错误上层可通过try/catch捕获token 处理请求拦截中统一添加 token无需在每个请求中单独写token 过期可在此处统一刷新错误统一处理_handleDioError将 Dio 的异常类型转为用户易懂的提示语全局统一使用提升用户体验。四、JSON 数据解析手动解析 自动解析json_serializable后端接口返回的是JSON 字符串Dio 已自动将其解析为MapString, dynamic/ListMapString, dynamic但直接使用 Map 会有类型不安全、代码繁琐、易出错的问题实际开发中会将 JSON 数据转为实体类Model通过实体类操作数据这是企业开发的强制规范。Flutter 中有两种 JSON 解析方式手动解析简单场景、自动解析json_serializable复杂场景 / 企业级接下来分别实现。1. 手动解析适合简单实体类创建实体类提供从 Map 构造对象的方法fromJson和转为 Map的方法toJson手动映射 JSON 字段和实体类属性适合字段较少的简单实体。实战创建帖子实体类PostModel创建lib/model/post_model.dart文件封装帖子实体类对应接口返回的 JSON 字段dart// 帖子实体类手动解析 class PostModel { // 实体类属性与接口JSON字段一一对应 final int id; final String title; final String body; final int userId; // 构造函数 PostModel({ required this.id, required this.title, required this.body, required this.userId, }); /// 从MapString, dynamic解析为PostModel核心方法 factory PostModel.fromJson(MapString, dynamic json) { return PostModel( id: json[id] ?? 0, // 判空赋予默认值避免空指针 title: json[title] ?? , body: json[body] ?? , userId: json[userId] ?? 0, ); } /// 将PostModel转为MapString, dynamic可选用于POST请求 MapString, dynamic toJson() { return { id: id, title: title, body: body, userId: userId, }; } }手动解析使用将接口返回的 Map 转为实体类列表dart// 改造之前的getPostList方法返回PostModel列表 FutureListPostModel getPostList({int page 1, int size 10}) async { try { Response response await dio.get(/posts, queryParameters: {page: page, limit: size}); if (response.statusCode 200) { // 将ListMap转为ListPostModel return (response.data as List).map((e) PostModel.fromJson(e as MapString, dynamic)).toList(); } else { throw Exception(请求失败状态码${response.statusCode}); } } on DioException catch (e) { throw Exception(_handleDioError(e)); } catch (e) { throw Exception(未知错误$e); } }手动解析优缺点优点无需依赖第三方库灵活适合简单实体缺点字段较多时手动写fromJson/toJson非常繁琐易出错维护成本高。2. 自动解析json_serializable企业级标准json_serializable是 Flutter 官方推荐的 JSON 自动解析库通过代码生成自动生成fromJson/toJson方法无需手动编写支持复杂实体类、嵌套实体类解决手动解析的繁琐问题是企业开发的标准选择。步骤 1集成依赖json_serializable需要 3 个依赖添加到pubspec.yaml中然后执行flutter pub getyamldependencies: flutter: sdk: flutter dio: ^5.4.0 json_annotation: ^4.8.1 # 实体类注解 dev_dependencies: flutter_test: sdk: flutter build_runner: ^2.4.6 # 代码生成工具 json_serializable: ^6.7.1 # 自动生成JSON解析代码json_annotation提供实体类的注解如JsonSerializablebuild_runner代码生成的命令行工具json_serializable自动生成解析代码的核心库。步骤 2创建实体类并添加注解PostModel改造post_model.dart使用注解标记实体类和属性无需手动写fromJson/toJsondartimport package:json_annotation/json_annotation.dart; // 生成的解析代码会放在这个文件中格式实体类文件名.g.dart part post_model.g.dart; /// 帖子实体类json_serializable自动解析 JsonSerializable() // 标记为可序列化的实体类 class PostModel { final int id; final String title; final String body; final int userId; // 构造函数 PostModel({ required this.id, required this.title, required this.body, required this.userId, }); /// 从Json解析为PostModel由json_serializable自动生成 factory PostModel.fromJson(MapString, dynamic json) _$PostModelFromJson(json); /// 将PostModel转为Json由json_serializable自动生成 MapString, dynamic toJson() _$PostModelToJson(this); }关键注解说明part post_model.g.dart指定生成的解析代码的文件路径必须与实体类文件名一致后缀为.g.dartJsonSerializable()标记该类为 JSON 可序列化类触发代码生成_$PostModelFromJson/_$PostModelToJson自动生成的方法命名规则为_$类名FromJson/_$类名ToJson无需手动实现。步骤 3执行命令生成解析代码在项目根目录的终端中执行以下命令自动生成post_model.g.dart文件bash运行# 一次性生成代码开发时使用 flutter pub run build_runner build # 监听文件变化自动重新生成代码推荐开发时一直运行 flutter pub run build_runner watch执行成功后会在post_model.dart同目录下生成post_model.g.dart里面包含自动生成的fromJson/toJson方法无需修改该文件。步骤 4使用自动解析的实体类与手动解析的使用方式完全一致直接调用PostModel.fromJson即可底层由自动生成的代码实现dart// 调用方式不变底层已为自动解析 FutureListPostModel getPostList({int page 1, int size 10}) async { try { Response response await dio.get(/posts, queryParameters: {page: page, limit: size}); if (response.statusCode 200) { return (response.data as List).map((e) PostModel.fromJson(e as MapString, dynamic)).toList(); } else { throw Exception(请求失败状态码${response.statusCode}); } } on DioException catch (e) { throw Exception(_handleDioError(e)); } catch (e) { throw Exception(未知错误$e); } }自动解析核心优势无需手动编写解析代码字段再多也只需添加注解自动生成避免繁琐和错误类型安全编译期检查字段类型避免运行时类型错误支持嵌套实体类如实体类中包含另一个实体类只需标记JsonSerializable()即可自动解析支持自定义字段映射如 JSON 字段为user_id实体类属性为userId可通过JsonKey(name: user_id)映射。五、综合实战真实接口驱动的下拉刷新 上拉加载列表结合本节课的 Dio 网络请求、JSON 自动解析以及上节课的列表知识实现企业级标准的网络列表页基于真实接口/posts获取数据下拉刷新重新请求第一页数据重置列表上拉加载请求下一页数据追加到列表统一处理加载状态、错误状态、无数据状态规范的资源管理和异常处理。实战代码NetworkListPage完整可运行创建lib/pages/network_list_page.dart实现全流程功能dartimport package:flutter/material.dart; import package:xxx/model/post_model.dart; import package:xxx/utils/network_utils.dart; class NetworkListPage extends StatefulWidget { const NetworkListPage({super.key}); override StateNetworkListPage createState() _NetworkListPageState(); } class _NetworkListPageState extends StateNetworkListPage { late ListPostModel _postList; // 帖子列表数据实体类列表 late ScrollController _scrollController; // 滚动控制器 int _page 1; // 当前页码 final int _pageSize 10; // 每页10条 bool _isLoading false; // 是否正在加载 bool _hasMore true; // 是否有更多数据 String _errorMsg ; // 错误信息 override void initState() { super.initState(); _postList []; _scrollController ScrollController(); _scrollController.addListener(_scrollListener); // 初始化加载第一页数据 _loadData(isRefresh: true); } // 滚动监听上拉加载 void _scrollListener() { if (_scrollController.position.pixels _scrollController.position.maxScrollExtent - 50 !_isLoading _hasMore _errorMsg.isEmpty) { _loadData(isRefresh: false); } } // 加载数据isRefreshtrue下拉刷新false上拉加载 Futurevoid _loadData({required bool isRefresh}) async { if (_isLoading) return; setState(() { _isLoading true; if (isRefresh) { _page 1; _errorMsg ; // 刷新清空错误信息 } }); try { // 调用真实接口获取实体类列表 ListPostModel newData await getPostList(page: _page, size: _pageSize); if (mounted) { setState(() { if (isRefresh) { _postList newData; // 刷新重置数据 } else { _postList.addAll(newData); // 加载追加数据 } _page; _hasMore newData.length _pageSize; // 数据量等于页大小说明有更多数据 _isLoading false; }); } } catch (e) { if (mounted) { setState(() { _errorMsg e.toString().replaceAll(Exception: , ); _isLoading false; }); } } } // 下拉刷新回调 Futurevoid _onRefresh() async { _hasMore true; await _loadData(isRefresh: true); } // 构建列表项 Widget _buildPostItem(PostModel post) { return ListTile( leading: CircleAvatar(child: Text(${post.userId})), title: Text( post.title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500), maxLines: 1, overflow: TextOverflow.ellipsis, ), subtitle: Text( post.body, style: const TextStyle(fontSize: 12, color: Colors.grey), maxLines: 2, overflow: TextOverflow.ellipsis, ), trailing: Text(ID: ${post.id}), onTap: () { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(点击了帖子${post.title})), ); }, ); } // 构建加载/错误/无数据提示 Widget _buildStatusWidget() { if (_errorMsg.isNotEmpty) { // 错误状态显示错误信息重新加载按钮 return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text(_errorMsg, style: const TextStyle(color: Colors.red, fontSize: 14)), const SizedBox(height: 20), ElevatedButton( onPressed: () _loadData(isRefresh: true), child: const Text(重新加载), ), ], ), ); } else if (_postList.isEmpty !_isLoading) { // 无数据状态 return const Center(child: Text(暂无帖子数据)); } else if (_isLoading _postList.isEmpty) { // 初始化加载状态 return const Center(child: CircularProgressIndicator()); } else { // 有数据构建底部加载提示 return _hasMore ? const Center( child: Padding( padding: EdgeInsets.symmetric(vertical: 16), child: CircularProgressIndicator(strokeWidth: 2), ), ) : const Center( child: Padding( padding: EdgeInsets.symmetric(vertical: 16), child: Text(没有更多数据了, style: TextStyle(color: Colors.grey)), ), ); } } override void dispose() { _scrollController.removeListener(_scrollListener); _scrollController.dispose(); super.dispose(); } override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text(真实接口列表实战), centerTitle: true, ), body: RefreshIndicator( color: Colors.blue, onRefresh: _onRefresh, child: ListView.builder( controller: _scrollController, itemCount: _postList.length 1, // 数据状态提示 itemBuilder: (context, index) { if (index _postList.length) { return _buildPostItem(_postList[index]); } else { return _buildStatusWidget(); } }, itemExtent: 100, // 固定高度提升性能 separatorBuilder: (context, index) const Divider(height: 1, color: Color(0xFFF0F0F0)), ), ), ); } }核心功能亮点实体类驱动列表数据为ListPostModel类型安全代码可读性高全状态覆盖初始化加载、加载中、错误、无数据、有更多、无更多6 种状态全覆盖用户体验友好异常安全所有异步操作判断mounted避免组件销毁后更新状态性能优化itemExtent固定高度懒加载构建规范的资源销毁交互友好错误状态提供「重新加载」按钮下拉刷新触发重新请求上拉提前 50px 加载。六、本节课核心总结必背前后端交互全考点1. Dio 网络请求核心全局配置初始化baseUrl、超时时间、请求头封装全局 Dio 实例企业开发标准基础请求GET 用queryParameters传参POST 用data传参统一捕获DioException拦截器请求拦截加 token、响应成功拦截格式化数据、响应错误拦截统一处理错误必用功能日志拦截器开发时开启生产时关闭方便调试。2. JSON 数据解析核心企业级规范必须将 JSON 转为实体类Model禁止直接使用 Map保证类型安全手动解析适合简单实体实现fromJson/toJson方法手动映射字段自动解析json_serializable添加注解 执行命令生成代码支持复杂 / 嵌套实体企业开发首选核心方法fromJsonJSON→实体类、toJson实体类→JSON。3. 真实接口列表实战核心状态三要素_isLoading防重复加载、_hasMore是否有更多数据、_page页码全状态覆盖初始化加载、加载中、错误、无数据、有更多、无更多提升用户体验异步安全所有异步操作后判断mounted避免组件销毁后更新状态交互友好错误状态提供重新加载下拉刷新、上拉提前加载。4. 企业开发最佳实践文件划分网络工具network_utils.dart、实体类model/、页面pages/分层管理便于维护异常统一处理底层拦截器统一处理网络错误上层捕获业务错误给出友好提示资源规范管理ScrollController/Dio 实例等及时销毁避免内存泄漏接口封装将所有接口封装为独立的方法放在network_utils.dart页面只调用方法不直接写请求。七、课后练习前后端交互必备必敲代码基础练习基于本节课的 PostModel实现帖子详情页点击列表项跳转到详情页传递PostModel并展示所有字段进阶练习在 Dio 拦截器中集成SharedPreferences实现 token 的本地缓存和自动添加模拟登录后 token 的持久化实战练习实现一个登录页调用模拟登录接口可自定义 POST 接口登录成功后保存 token 到本地跳转到首页首页通过拦截器自动携带 token 请求数据。下一节课预告我们会学习 Flutter 的本地存储开发必备包括SharedPreferences轻量键值对存储如 token、用户信息、配置项、文件存储本地文件 / 图片存储、SQLite本地数据库适合大量结构化数据如本地列表、历史记录学会后能实现数据的本地持久化让 APP 在重启后仍能保留用户数据完成从「网络请求」到「本地存储」的全流程数据管理。本地存储是 APP 的基础功能结合之前的网络请求能实现绝大多数业务场景如登录状态持久化、离线缓存、历史记录重点掌握SharedPreferences和SQLite的使用我可以帮你把本节课的Dio 全局配置、实体类、网络列表页代码整合为一个可直接运行的 Flutter 项目模板需要吗