《Flutter全栈开发实战指南:从零到高级》- 14 -网络请求与数据解析
网络请求与数据解析
在移动开发中需要与云端服务器进行频繁的数据交互,本节内容讲带你详细了解网络请求与数据解析,让你的应用真正地“活”起来。
一、 为什么网络层如此重要?
举个例子:你正在开发一个新闻App,那些滚动的时事新闻、视频等内容,不可能全部打包在App安装包里,它们需要从服务器实时获取。这个“获取”的过程,就是通过网络请求完成的。
简单来说,流程就是:App 问服务器要数据 -> 服务器返回数据 -> App 把数据展示出来。
在Flutter中,常用的两个网络请求库:官方推荐的 http 库 和 社区维护得 dio 库。我们将从两者入手,带你彻底玩转网络请求。
二、 http库 与 dio库 如何选择?
选择哪个库,就像选择工具,没有绝对的好坏,只有合不合适。
1. http 库
http 库是Flutter团队维护的底层库,它:
- 优点:官方维护,稳定可靠;API简单直接,学习成本低。
- 缺点:功能相对基础,许多高级功能(如拦截器、文件上传/下载进度等)需要自己手动实现。
核心方法:
get(): 向指定URL发起GET请求,用于获取数据。post(): 发起POST请求,用于提交数据。put(),delete(),head()等:对应其他HTTP方法。
2. dio 库
dio 是一个强大的第三方HTTP客户端,它:
- 优点:支持拦截器、全局配置、请求取消、FormData、文件上传/下载、超时设置等。
- 缺点:相对于
http库更重一些。
如何选择?
- 新手入门:可以从
http开始,上手快。 - 中大型项目:强烈推荐
dio,它能帮你节省大量造轮子的时间。
本节内容主要以 dio 为例进行讲解,它更符合项目开发的实际情况。
三、 引入依赖
首先,在你的 pubspec.yaml 文件中声明依赖。
dependencies:flutter:sdk: flutterdio: ^5.0.0 # 用于JSON序列化json_annotation: ^4.8.0dev_dependencies:flutter_test:sdk: flutterbuild_runner: ^2.3.0json_serializable: ^6.5.0
执行 flutter pub get 安装依赖。
四、 http 库
虽然我们推荐使用dio库,但了解http库的基本用法仍是必要的。
以获取一篇博客文章信息为例
import 'package:http/http.dart' as http;
import 'dart:convert'; class HttpExample {static Future<void> fetchPost() async {try {// 1. 发起GET请求final response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/posts/1'),);// 2. 状态码200表示成功if (response.statusCode == 200) {// 3. 使用 dart:convert 解析返回的JSON字符串Map<String, dynamic> jsonData = json.decode(response.body);// 4. 从解析后的Map中取出数据String title = jsonData['title'];String body = jsonData['body'];print('标题: $title');print('内容: $body');} else {// 请求失败print('请求失败,状态码: ${response.statusCode}');print('响应体: ${response.body}');}} catch (e) {// 捕获异常print('请求发生异常: $e');}}
}
代码解读:
async/await:网络请求是耗时操作,必须使用异步。await会等待请求完成,而不会阻塞UI线程。Uri.parse:将字符串URL转换为Uri对象。response.statusCode:响应状态码,200系列表示成功。json.decode():反序列化将JSON串转换为Dart中的Map<String, dynamic>或List;
五、 dio 库
下面我们重点讲解下dio库:
1. Dio-发起请求
我们先创建一个Dio实例并进行全局配置。
import 'package:dio/dio.dart';class DioManager {// 单例static final DioManager _instance = DioManager._internal();factory DioManager() => _instance;DioManager._internal() {_dio = Dio(BaseOptions(baseUrl: 'https://jsonplaceholder.typicode.com', connectTimeout: const Duration(seconds: 5), // 连接超时时间receiveTimeout: const Duration(seconds: 3), // 接收数据超时时间headers: {'Content-Type': 'application/json', },));}late final Dio _dio;Dio get dio => _dio;
}// GET请求
void fetchPostWithDio() async {try {// baseUrl后面拼接路径Response response = await DioManager().dio.get('/posts/1');// dio会自动检查状态码,非200系列会抛异常,所以这里直接处理数据Map<String, dynamic> data = response.data; // 这里dio帮我们自动解析了JSONprint('获取数据: ${data['title']}');} on DioException catch (e) {print('请求异常: $e');if (e.response != null) {// 错误状态码print('错误状态码: ${e.response?.statusCode}');print('错误信息: ${e.response?.data}');} else {// 抛异常print('异常: ${e.message}');}} catch (e) {// 未知异常print('未知异常: $e');}
}
Dio相比Http的优点:
- 自动JSON解析:
response.data直接就是Map或List,无需手动json.decode,太方便了! - 配置清晰:
BaseOptions全局配置一目了然。 - 结构化异常:
DioException包含了丰富的错误信息。
2. Dio-网络请求流程
为了让大家更直观地理解,我们用一个流程图来展示Dio处理请求的完整过程:
3. Dio-拦截器
拦截器允许我们在请求发送前和响应返回后,插入自定义逻辑,对所有经过的请求和响应进行检查和加工。
案例:自动添加认证Token
class AuthInterceptor extends Interceptor {void onRequest(RequestOptions options, RequestInterceptorHandler handler) {// 请求发送前,为每个请求的Header加上Tokenconst String token = 'your_auth_token_here';if (token.isNotEmpty) {options.headers['Authorization'] = 'Bearer $token';}handler.next(options);}void onResponse(Response response, ResponseInterceptorHandler handler) {// 响应成功处理print('请求成功: ${response.requestOptions.path}');handler.next(response);}void onError(DioException err, ErrorInterceptorHandler handler) {// 失败处理// 当Token过期时,自动跳转到登录页if (err.response?.statusCode == 401) {print('Token已过期,请重新登录!');// 这里可以跳转到登录页面// NavigationService.instance.navigateTo('/login');}handler.next(err);}
}// 将拦截器添加到Dio实例中
void main() {final dio = DioManager().dio;dio.interceptors.add(AuthInterceptor());// 这里可以添加其他拦截器dio.interceptors.add(LogInterceptor(responseBody: true));
}
拦截器的添加顺序就是它们的执行顺序。
onRequest正序执行,onResponse和onError倒序执行。
六、 JSON序列化与反序列化
这是新手最容易踩坑的地方。直接从JSON转换成Dart对象(Model),能让我们的代码更安全、更易维护。
1. 为什么要序列化?
- 类型安全:直接操作Map,编译器不知道
data[‘title‘]是String还是int,容易写错; - 代码效率:使用点语法
post.title访问属性,比post[‘title‘]更高效且有代码提示; - 可维护性:当接口字段变更时,你只需要修改一个Model类,而不是分散在各处的字符串key;;
2. 使用 json_serializable自动序列化
通过代码生成的方式,自动创建 fromJson 和 toJson 方法,一劳永逸。
步骤1:创建Model类并使用注解
// post.dart
import 'package:json_annotation/json_annotation.dart';// 运行 `flutter pub run build_runner build` 后,会生成 post.g.dart 文件
part 'post.g.dart';// 这个注解告诉生成器这个类需要生成序列化代码
()
class Post {// 使用@JsonKey可以自定义序列化行为// 例如,如果JSON字段名是`user_id`,而Dart字段是`userId`,可以这样映射:// @JsonKey(name: 'user_id')final int userId;final int id;final String title;final String body;Post({required this.userId,required this.id,required this.title,required this.body,});// 生成的代码会提供这两个方法factory Post.fromJson(Map<String, dynamic> json) => _$PostFromJson(json);Map<String, dynamic> toJson() => _$PostToJson(this);
}
步骤2:运行代码生成命令
在项目根目录下执行:
flutter pub run build_runner build
这个命令会扫描所有带有 @JsonSerializable() 注解的类,并生成对应的 .g.dart 文件(如 post.g.dart)。这个文件里包含了 _$PostFromJson 和 _$PostToJson 的具体实现。
步骤3:自动生成
// 具体的请求方法中使用
void fetchPostModel() async {try {Response response = await DioManager().dio.get('/posts/1');// 将响应数据转换为Post对象Post post = Post.fromJson(response.data);print('文章标题: ${post.title}');} on DioException catch (e) {// ... 错误处理}
}
json_serializable 的优势:
- 自动处理类型转换,避免手误;
- 通过
@JsonKey注解可以处理各种复杂场景;
七、 网络层在MVVM模式中的定位
实际项目中,我们不会直接在UI页面里写网络请求代码。让我们看看网络层在MVVM架构中是如何工作的:
各个分层职责:
- View:只关心数据的展示和用户交互;
- ViewModel:持有业务状态,处理UI逻辑,不关心数据从哪里来;
- Model:决定数据是从网络获取还是本地数据库读取,它调用网络层;
- Dio:纯粹的网络请求执行者,负责API调用、错误初步处理等;
这样分层的好处是:最终目的是解耦,各司其职,修改网络层不会影响业务逻辑,代码结构清晰,同事方便单元测试。
八、 错误处理
一个好的应用,必须支持处理各种异常情况。
1. DioException
DioException 的类型 (type) 帮助我们准确判断错误根源。
void handleDioError(DioException e) {switch (e.type) {case DioExceptionType.connectionTimeout:case DioExceptionType.sendTimeout:case DioExceptionType.receiveTimeout:print('超时错误,请检查网络连接是否稳定。');break;case DioExceptionType.badCertificate:print('证书错误。');break;case DioExceptionType.badResponse:// 服务器返回了错误状态码print('服务器错误: ${e.response?.statusCode}');// 可以根据不同状态码做不同处理if (e.response?.statusCode == 404) {print('请求的资源不存在(404)');} else if (e.response?.statusCode == 500) {print('服务器内部错误(500)');} else if (e.response?.statusCode == 401) {print('未授权,请重新登录(401)');}break;case DioExceptionType.cancel:print('请求被取消。');break;case DioExceptionType.connectionError:print('网络连接错误,请检查网络是否开启。');break;case DioExceptionType.unknown:print('未知错误: ${e.message}');break;}
}
2. 重试机制
对于因网络波动导致的失败,自动重试能大幅提升用户体验。
class RetryInterceptor extends Interceptor {final Dio _dio;RetryInterceptor(this._dio);Future<void> onError(DioException err, ErrorInterceptorHandler handler) async {// 只对超时和网络连接错误进行重试if (err.type == DioExceptionType.connectionTimeout ||err.type == DioExceptionType.receiveTimeout ||err.type == DioExceptionType.connectionError) {final int retryCount = err.requestOptions.extra['retry_count'] ?? 0;const int maxRetryCount = 3;if (retryCount < maxRetryCount) {// 增加重试计数err.requestOptions.extra['retry_count'] = retryCount + 1;print('网络不稳定,正在尝试第${retryCount + 1}次重试...');// 等待一段时间后重试await Future.delayed(Duration(seconds: 1 * (retryCount + 1)));try {// 重新发送请求final Response response = await _dio.fetch(err.requestOptions);// 返回成功responsehandler.resolve(response);return;} catch (retryError) {// 如果失败继续传递错误handler.next(err);return;}}}// 如果不是指定错误或已达最大重试次数,则继续传递错误handler.next(err);}
}
九、 封装一个完整的网络请求库
到这已经把所有的网络请求知识学完了,下面我们用学到的知识封装一个通用的网络请求工具类。
// http_client.dart
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';class HttpClient {static final HttpClient _instance = HttpClient._internal();factory HttpClient() => _instance;late final Dio _dio;HttpClient._internal() {_dio = Dio(BaseOptions(baseUrl: 'https://api.yourserver.com/v1',connectTimeout: const Duration(seconds: 10),receiveTimeout: const Duration(seconds: 10),headers: {'Content-Type': 'application/json'},));// 添加拦截器_dio.interceptors.add(LogInterceptor(requestBody: kDebugMode, responseBody: kDebugMode,));_dio.interceptors.add(AuthInterceptor());_dio.interceptors.add(RetryInterceptor(_dio));}// 封装GET请求Future<Response<T>> get<T>(String path, {Map<String, dynamic>? queryParameters,Map<String, dynamic>? headers,}) async {try {final options = Options(headers: headers);return await _dio.get<T>(path,queryParameters: queryParameters,options: options,);} on DioException {rethrow; }}// 封装POST请求Future<Response<T>> post<T>(String path, {dynamic data,Map<String, dynamic>? queryParameters,Map<String, dynamic>? headers,}) async {try {final options = Options(headers: headers);return await _dio.post<T>(path,data: data,queryParameters: queryParameters,options: options,);} on DioException {rethrow;}}// 获取列表数据Future<List<T>> getList<T>(String path, {Map<String, dynamic>? queryParameters,T Function(Map<String, dynamic>)? fromJson,}) async {final response = await get<List<dynamic>>(path, queryParameters: queryParameters);// 将List<dynamic>转换为List<T>if (fromJson != null) {return response.data!.map<T>((item) => fromJson(item as Map<String, dynamic>)).toList();}return response.data as List<T>;}// 获取单个对象Future<T> getItem<T>(String path, {Map<String, dynamic>? queryParameters,required T Function(Map<String, dynamic>) fromJson,}) async {final response = await get<Map<String, dynamic>>(path, queryParameters: queryParameters);return fromJson(response.data!);}
}//
class PostRepository {final HttpClient _client = HttpClient();Future<Post> getPost(int id) async {final response = await _client.getItem('/posts/$id',fromJson: Post.fromJson, );return response;}Future<List<Post>> getPosts() async {final response = await _client.getList('/posts',fromJson: Post.fromJson,);return response;}Future<Post> createPost(Post post) async {// Model转JSONfinal response = await _client.post('/posts',data: post.toJson(),);return Post.fromJson(response.data);}
}
总结
又到了写总结诶的时候了,让我们用一张表格来回顾所有知识点:
| 知识点 | 核心 | 用途 |
|---|---|---|
| 库选择 | http 轻量,dio 强大 | 中大型项目首选 dio |
| 异步编程 | 使用 async/await 处理耗时操作 | 不能阻塞UI线程 |
| JSON序列化 | 自动生成 | 推荐 json_serializable |
| 错误处理 | 区分网络异常和服务器错误 | 精确捕获 DioException 并分类处理 |
| 拦截器 | 统一处理请求/响应 | 用于添加Token、日志、重试逻辑 |
| 架构分层 | MVVM | 分离解耦 |
| 请求封装 | 统一封装GET/POST等基础方法 | 提供 getItem, getList 等语义化方法 |
网络请求在实际项目中直观重要,没有网络就没有数据,掌握好本章内容,你就能为你Flutter应用注入源源不断的活力。让我们下期见!
