网络请求与数据解析
在移动开发中需要与云端服务器进行频繁的数据交互,本节内容讲带你详细了解网络请求与数据解析,让你的应用真正地"活"起来。
一、 为什么网络层如此重要?
举个例子:你正在开发一个新闻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 文件中声明依赖。
yaml
dependencies:
flutter:
sdk: flutter
dio: ^5.0.0
# 用于JSON序列化
json_annotation: ^4.8.0
dev_dependencies:
flutter_test:
sdk: flutter
build_runner: ^2.3.0
json_serializable: ^6.5.0
执行 flutter pub get 安装依赖。
四、 http 库
虽然我们推荐使用dio库,但了解http库的基本用法仍是必要的。
以获取一篇博客文章信息为例
dart
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实例并进行全局配置。
dart
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帮我们自动解析了JSON
print('获取数据: ${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
dart
class AuthInterceptor extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
// 请求发送前,为每个请求的Header加上Token
const String token = 'your_auth_token_here';
if (token.isNotEmpty) {
options.headers['Authorization'] = 'Bearer $token';
}
handler.next(options);
}
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
// 响应成功处理
print('请求成功: ${response.requestOptions.path}');
handler.next(response);
}
@override
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类并使用注解
dart
// post.dart
import 'package:json_annotation/json_annotation.dart';
// 运行 `flutter pub run build_runner build` 后,会生成 post.g.dart 文件
part 'post.g.dart';
// 这个注解告诉生成器这个类需要生成序列化代码
@JsonSerializable()
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:运行代码生成命令
在项目根目录下执行:
bash
flutter pub run build_runner build
这个命令会扫描所有带有 @JsonSerializable() 注解的类,并生成对应的 .g.dart 文件(如 post.g.dart)。这个文件里包含了 _$PostFromJson 和 _$PostToJson 的具体实现。
步骤3:自动生成
dart
// 具体的请求方法中使用
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架构中是如何工作的:
视图层] -->|调用| B[ViewModel
视图模型] B -->|调用| C[Model
模型层] C -->|使用| D[Dio
网络层] D -->|返回JSON| C C -->|转换为Model| B B -->|更新状态| A
各个分层职责:
- View:只关心数据的展示和用户交互;
- ViewModel:持有业务状态,处理UI逻辑,不关心数据从哪里来;
- Model:决定数据是从网络获取还是本地数据库读取,它调用网络层;
- Dio:纯粹的网络请求执行者,负责API调用、错误初步处理等;
这样分层的好处是:最终目的是解耦,各司其职,修改网络层不会影响业务逻辑,代码结构清晰,同事方便单元测试。
八、 错误处理
一个好的应用,必须支持处理各种异常情况。
1. DioException
DioException 的类型 (type) 帮助我们准确判断错误根源。
dart
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. 重试机制
对于因网络波动导致的失败,自动重试能大幅提升用户体验。
dart
class RetryInterceptor extends Interceptor {
final Dio _dio;
RetryInterceptor(this._dio);
@override
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);
// 返回成功response
handler.resolve(response);
return;
} catch (retryError) {
// 如果失败继续传递错误
handler.next(err);
return;
}
}
}
// 如果不是指定错误或已达最大重试次数,则继续传递错误
handler.next(err);
}
}
九、 封装一个完整的网络请求库
到这已经把所有的网络请求知识学完了,下面我们用学到的知识封装一个通用的网络请求工具类。
dart
// 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转JSON
final 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应用注入源源不断的活力。让我们下期见!