详细介绍:【第四阶段-数据处理与网络】第二章:网络请求—就像发送邮件
文章目录
什么是网络请求?

想象一下网络请求就像是:
- 发送邮件:你写好信件(请求数据),投递到邮箱(发送到服务器)
- 等待回复:邮递员(网络)将信件送达,对方回信(服务器响应)
- 收到回复:你收到回信(获得响应数据),根据内容采取行动
在Flutter中,网络请求让我们的应用能够与远程服务器交换数据,获取最新信息或提交用户数据。
一:HTTP基础 - 网络通信的语言
1.1 基础概念
HTTP就像是网络世界的通用语言,定义了客户端和服务器之间如何交流。
首先,在pubspec.yaml中添加依赖:
dependencies:
http: ^1.1.0
1.2 基础HTTP请求示例
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http; // 导入http库,使用别名http
import 'dart:convert'; // 导入JSON编解码库
void main() => runApp(MaterialApp(home: HttpDemo()));
/// HTTP网络请求演示页面
class HttpDemo extends StatefulWidget {
_HttpDemoState createState() => _HttpDemoState();
}
class _HttpDemoState extends State<HttpDemo> {
// 加载状态标志,用于显示加载动画和禁用按钮
bool _isLoading = false;
// 存储服务器响应的数据,用于显示在界面上
String _responseData = '';
// 存储帖子列表数据,dynamic类型可以存储任意JSON数据
List<dynamic> _posts = [];
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('HTTP 网络请求演示')),
body: Padding(
padding: EdgeInsets.all(16),
child: Column(
children: [
// HTTP方法按钮
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: _isLoading ? null : _performGetRequest,
child: Text('GET'),
),
),
SizedBox(width: 8),
Expanded(
child: ElevatedButton(
onPressed: _isLoading ? null : _performPostRequest,
child: Text('POST'),
),
),
],
),
SizedBox(height: 16),
// 响应数据显示
if (_isLoading)
CircularProgressIndicator()
else if (_responseData.isNotEmpty)
Expanded(
child: Container(
width: double.infinity,
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(8),
),
child: SingleChildScrollView(
child: Text(_responseData, style: TextStyle(fontSize: 12)),
),
),
),
SizedBox(height: 16),
// 帖子列表
ElevatedButton(
onPressed: _isLoading ? null : _loadPosts,
child: Text('加载帖子列表'),
),
if (_posts.isNotEmpty)
Expanded(
child: ListView.builder(
itemCount: _posts.length > 5 ? 5 : _posts.length,
itemBuilder: (context, index) {
final post = _posts[index];
return ListTile(
title: Text(post['title']),
subtitle: Text(post['body'], maxLines: 2),
);
},
),
),
],
),
),
);
}
/// GET请求示例
/// GET用于从服务器获取数据,不会修改服务器数据
Future<void> _performGetRequest() async {
// 更新UI状态:开始加载,清空之前的响应数据
setState(() {
_isLoading = true;
_responseData = '';
});
try {
// 发送GET请求
// Uri.parse: 将字符串URL转换为Uri对象
// await: 等待异步操作完成
final response = await http.get(
Uri.parse('https://jsonplaceholder.typicode.com/posts/1'),
headers: {'Content-Type': 'application/json'}, // 设置请求头,告诉服务器我们发送/接收JSON格式
);
// 检查HTTP状态码
// 200表示成功,其他状态码表示不同的错误情况
if (response.statusCode == 200) {
// json.decode: 将JSON字符串解析为Dart对象(Map或List)
final data = json.decode(response.body);
setState(() {
// JsonEncoder.withIndent: 格式化JSON输出,使其更易读
// ' '表示使用2个空格作为缩进
_responseData = JsonEncoder.withIndent(' ').convert(data);
});
} else {
// 如果状态码不是200,抛出异常
throw Exception('请求失败: ${response.statusCode}');
}
} catch (e) {
// 捕获所有异常(网络错误、解析错误等)
setState(() => _responseData = '请求失败: $e');
} finally {
// finally块总是会执行,无论是否发生异常
// 用于清理工作,这里用于停止加载动画
setState(() => _isLoading = false);
}
}
/// POST请求示例
/// POST用于向服务器提交数据,通常用于创建新资源
Future<void> _performPostRequest() async {
setState(() {
_isLoading = true;
_responseData = '';
});
try {
// 准备要发送的数据
// 这是一个Map对象,将被转换为JSON格式发送
final requestData = {
'title': 'Flutter学习笔记',
'body': '这是一个POST请求示例',
'userId': 1,
};
// 发送POST请求
final response = await http.post(
Uri.parse('https://jsonplaceholder.typicode.com/posts'),
headers: {'Content-Type': 'application/json'},
// json.encode: 将Dart对象转换为JSON字符串
// POST请求需要将数据放在body中发送
body: json.encode(requestData),
);
// 201 Created: 表示成功创建了新资源
// 这是POST请求成功的标准状态码
if (response.statusCode == 201) {
final data = json.decode(response.body);
setState(() {
_responseData = JsonEncoder.withIndent(' ').convert(data);
});
} else {
throw Exception('请求失败: ${response.statusCode}');
}
} catch (e) {
setState(() => _responseData = '请求失败: $e');
} finally {
setState(() => _isLoading = false);
}
}
/// 加载帖子列表示例
/// 演示如何获取并显示列表数据
Future<void> _loadPosts() async {
setState(() => _isLoading = true);
try {
// 获取帖子列表
// 这个API返回的是一个JSON数组
final response = await http.get(
Uri.parse('https://jsonplaceholder.typicode.com/posts'),
);
if (response.statusCode == 200) {
// json.decode会将JSON数组解析为List<dynamic>
// 每个元素是一个Map<String, dynamic>
setState(() => _posts = json.decode(response.body));
}
} catch (e) {
// 这里使用print输出错误,实际项目中应该显示给用户
print('加载失败: $e');
} finally {
setState(() => _isLoading = false);
}
}
}
核心知识点:
http.get(): 发送GET请求http.post(): 发送POST请求Uri.parse(): 解析URLjson.encode()/json.decode(): JSON编码/解码headers: 设置请求头response.statusCode: 响应状态码response.body: 响应体
二:Dio高级网络库
2.1 基础概念
Dio是一个功能更强大的网络库,提供拦截器、并发请求、文件上传下载等高级功能。
首先添加依赖:
dependencies:
dio: ^5.3.2
2.2 Dio使用示例
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'dart:convert';
/// 网络服务类 - 使用单例模式
/// 单例模式确保整个应用只有一个ApiService实例
/// 这样可以共享配置和拦截器,避免重复创建
class ApiService {
// 静态私有实例变量,保存唯一的ApiService实例
static final ApiService _instance = ApiService._internal();
// 工厂构造函数,总是返回同一个实例
factory ApiService() => _instance;
// 私有命名构造函数,防止外部直接创建实例
ApiService._internal();
// Dio实例,late表示延迟初始化
late Dio _dio;
/// 初始化Dio配置
void initialize() {
// 创建Dio实例并配置基础选项
_dio = Dio(BaseOptions(
// 基础URL,所有请求都会以此为前缀
baseUrl: 'https://jsonplaceholder.typicode.com',
// 连接超时时间:建立连接的最长等待时间
connectTimeout: Duration(seconds: 10),
// 接收超时时间:接收数据的最长等待时间
receiveTimeout: Duration(seconds: 10),
// 默认请求头,所有请求都会带上这些头
headers: {'Content-Type': 'application/json'},
));
// 添加拦截器 - 可以在请求/响应前后执行自定义逻辑
_dio.interceptors.add(InterceptorsWrapper(
// 请求拦截器:在请求发送前执行
onRequest: (options, handler) {
// 打印请求信息,方便调试
print('请求: ${options.method} ${options.path}');
// 可以在这里添加token等认证信息
// options.headers['Authorization'] = 'Bearer $token';
// 继续执行请求
handler.next(options);
},
// 响应拦截器:在收到响应后执行
onResponse: (response, handler) {
// 打印响应状态码
print('响应: ${response.statusCode}');
// 可以在这里处理通用响应逻辑
// 继续传递响应
handler.next(response);
},
// 错误拦截器:当请求发生错误时执行
onError: (error, handler) {
// 打印错误信息
print('错误: ${error.message}');
// 可以在这里统一处理错误(如401未授权跳转登录)
// 继续传递错误
handler.next(error);
},
));
}
/// 发送GET请求
/// [path] 请求路径,会自动拼接baseUrl
/// [params] 查询参数,会被转换为URL参数 (?key=value)
Future<Response> get(String path, {Map<String, dynamic>? params}) async {
return await _dio.get(path, queryParameters: params);
}
/// 发送POST请求
/// [path] 请求路径
/// [data] 请求体数据,会自动转换为JSON
Future<Response> post(String path, {dynamic data}) async {
return await _dio.post(path, data: data);
}
}
// Dio演示页面
class DioDemo extends StatefulWidget {
_DioDemoState createState() => _DioDemoState();
}
class _DioDemoState extends State<DioDemo> {
final _apiService = ApiService();
bool _isLoading = false;
String _responseData = '';
void initState() {
super.initState();
_apiService.initialize();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Dio 网络请求')),
body: Padding(
padding: EdgeInsets.all(16),
child: Column(
children: [
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: _isLoading ? null : _dioGetRequest,
child: Text('GET请求'),
),
),
SizedBox(width: 8),
Expanded(
child: ElevatedButton(
onPressed: _isLoading ? null : _dioPostRequest,
child: Text('POST请求'),
),
),
],
),
SizedBox(height: 16),
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: _isLoading ? null : _dioConcurrentRequests,
child: Text('并发请求'),
),
),
SizedBox(width: 8),
Expanded(
child: ElevatedButton(
onPressed: _isLoading ? null : _dioWithParams,
child: Text('带参数'),
),
),
],
),
SizedBox(height: 16),
if (_isLoading)
CircularProgressIndicator()
else if (_responseData.isNotEmpty)
Expanded(
child: Container(
width: double.infinity,
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(8),
),
child: SingleChildScrollView(
child: Text(_responseData, style: TextStyle(fontSize: 12)),
),
),
),
],
),
),
);
}
// Dio GET请求
Future<void> _dioGetRequest() async {
setState(() {
_isLoading = true;
_responseData = '';
});
try {
final response = await _apiService.get('/posts/1');
setState(() {
_responseData = JsonEncoder.withIndent(' ').convert(response.data);
});
} catch (e) {
setState(() => _responseData = '请求失败: $e');
} finally {
setState(() => _isLoading = false);
}
}
// Dio POST请求
Future<void> _dioPostRequest() async {
setState(() {
_isLoading = true;
_responseData = '';
});
try {
final response = await _apiService.post('/posts', data: {
'title': 'Dio POST示例',
'body': '这是Dio POST请求',
'userId': 1,
});
setState(() {
_responseData = JsonEncoder.withIndent(' ').convert(response.data);
});
} catch (e) {
setState(() => _responseData = '请求失败: $e');
} finally {
setState(() => _isLoading = false);
}
}
/// 并发请求示例
/// 演示如何同时发送多个请求并等待所有结果
Future<void> _dioConcurrentRequests() async {
setState(() {
_isLoading = true;
_responseData = '';
});
try {
// 创建多个Future对象,每个代表一个异步请求
// 这些请求会并行执行,而不是顺序执行
final futures = [
_apiService.get('/posts/1'),
_apiService.get('/posts/2'),
_apiService.get('/posts/3'),
];
// Future.wait: 等待所有Future完成
// 如果任何一个失败,整个操作就会失败
final responses = await Future.wait(futures);
// 提取所有响应的数据部分
final results = responses.map((r) => r.data).toList();
setState(() {
//JsonEncoder.withIndent(' ')将多个响应结果格式化为美观的JSON字符串,便于查看.(两个空格)表示每一层嵌套使用两个空格作为缩进
_responseData = JsonEncoder.withIndent(' ').convert(results);
});
} catch (e) {
setState(() => _responseData = '并发请求失败: $e');
} finally {
setState(() => _isLoading = false);
}
}
/// 带查询参数的请求示例
/// 演示如何在GET请求中传递参数
Future<void> _dioWithParams() async {
setState(() {
_isLoading = true;
_responseData = '';
});
//JsonEncoder.withIndent(' ')将多个响应结果格式化为美观的JSON字符串,便于查看.(两个空格)表示每一层嵌套使用两个空格作为缩进
try {
// 查询参数会被转换为URL参数
// 最终URL: /posts?userId=1&_limit=3
final response = await _apiService.get('/posts', params: {
'userId': 1, // 筛选userId为1的帖子
'_limit': 3, // 限制返回3条数据
});
setState(() {
_responseData = JsonEncoder.withIndent(' ').convert(response.data);
});
} catch (e) {
setState(() => _responseData = '请求失败: $e');
} finally {
setState(() => _isLoading = false);
}
}
}
核心知识点:
Dio(): 创建Dio实例BaseOptions: 配置基础选项(baseUrl、超时等)interceptors: 拦截器(请求/响应/错误拦截)queryParameters: 查询参数Future.wait(): 并发请求
三:JSON数据处理
3.1 基础概念
JSON是一种轻量级的数据交换格式,需要在Dart对象和JSON字符串之间进行转换。
3.2 数据模型和JSON转换
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
/// 用户数据模型类
/// 将JSON数据映射为强类型的Dart对象,提供类型安全和代码提示
class User {
// final表示这些字段在创建后不可修改(不可变对象)
final int id;
final String name;
final String email;
// 构造函数,使用命名参数并标记为required(必需)
User({required this.id, required this.name, required this.email});
/// 工厂构造函数:从JSON Map创建User对象
/// 这个过程叫做"反序列化"或"解析"
/// [json] 是从服务器返回的JSON数据,已被json.decode解析为Map
factory User.fromJson(Map<String, dynamic> json) {
return User(
// 从Map中提取对应的字段值
// json['id']会返回dynamic类型,Dart会自动转换为int
id: json['id'],
name: json['name'],
email: json['email'],
);
}
/// 将User对象转换为JSON Map
/// 这个过程叫做"序列化"
/// 返回的Map可以被json.encode转换为JSON字符串发送给服务器
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'email': email,
};
}
}
// JSON处理演示
class JsonDemo extends StatefulWidget {
_JsonDemoState createState() => _JsonDemoState();
}
class _JsonDemoState extends State<JsonDemo> {
List<User> _users = [];
String _jsonString = '';
bool _isLoading = false;
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('JSON 数据处理')),
body: Padding(
padding: EdgeInsets.all(16),
child: Column(
children: [
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: _isLoading ? null : _fetchAndParseJson,
child: Text('获取JSON'),
),
),
SizedBox(width: 8),
Expanded(
child: ElevatedButton(
onPressed: _users.isEmpty ? null : _convertToJson,
child: Text('转换JSON'),
),
),
],
),
SizedBox(height: 16),
// JSON字符串显示
if (_jsonString.isNotEmpty)
Container(
height: 150,
width: double.infinity,
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(8),
),
child: SingleChildScrollView(
child: Text(_jsonString, style: TextStyle(fontSize: 12)),
),
),
SizedBox(height: 16),
// 用户列表
if (_users.isNotEmpty)
Expanded(
child: ListView.builder(
itemCount: _users.length,
itemBuilder: (context, index) {
final user = _users[index];
return ListTile(
title: Text(user.name),
subtitle: Text(user.email),
leading: CircleAvatar(child: Text('${user.id}')),
);
},
),
),
],
),
),
);
}
/// 获取并解析JSON数据
/// 演示完整的JSON处理流程:请求 → 解析 → 转换为对象
Future<void> _fetchAndParseJson() async {
setState(() => _isLoading = true);
try {
// 1. 发送HTTP请求获取数据
final response = await http.get(
Uri.parse('https://jsonplaceholder.typicode.com/users'),
);
if (response.statusCode == 200) {
// 2. 解析JSON字符串为Dart对象
// response.body是字符串,json.decode将其解析为List<dynamic>
// 因为这个API返回的是JSON数组
final List<dynamic> jsonList = json.decode(response.body);
// 3. 将动态类型的Map列表转换为强类型的User对象列表
// map函数遍历每个元素,调用User.fromJson进行转换
// toList()将Iterable转换为List
final users = jsonList.map((json) => User.fromJson(json)).toList();
setState(() {
// 保存User对象列表,供UI使用
_users = users;
// 格式化JSON字符串用于显示
_jsonString = JsonEncoder.withIndent(' ').convert(jsonList);
});
}
} catch (e) {
print('获取失败: $e');
} finally {
setState(() => _isLoading = false);
}
}
/// 将对象转换为JSON
/// 演示序列化过程:对象 → Map → JSON字符串
void _convertToJson() {
// 1. 将User对象列表转换为Map列表
// 每个User对象调用toJson()方法转换为Map<String, dynamic>
final jsonList = _users.map((user) => user.toJson()).toList();
// 2. 将Map列表转换为格式化的JSON字符串
// JsonEncoder.withIndent添加缩进,使输出更易读
final jsonString = JsonEncoder.withIndent(' ').convert(jsonList);
// 3. 更新UI显示
setState(() => _jsonString = jsonString);
}
}
核心知识点:
fromJson(): 从JSON Map创建对象toJson(): 将对象转换为JSON Mapjson.decode(): 解析JSON字符串json.encode(): 转换为JSON字符串JsonEncoder.withIndent(): 格式化JSON输出.map(): 批量转换列表
四:进阶知识点
4.1 HTTP状态码详解
理解HTTP状态码对于调试网络请求非常重要:
2xx 成功状态码:
200 OK: GET请求成功201 Created: POST请求成功创建资源204 No Content: 请求成功但无返回内容(常用于DELETE)
3xx 重定向状态码:
301 Moved Permanently: 永久重定向302 Found: 临时重定向304 Not Modified: 资源未修改,可使用缓存
4xx 客户端错误:
400 Bad Request: 请求参数错误401 Unauthorized: 未授权,需要登录403 Forbidden: 禁止访问,权限不足404 Not Found: 资源不存在422 Unprocessable Entity: 请求格式正确但语义错误
5xx 服务器错误:
500 Internal Server Error: 服务器内部错误502 Bad Gateway: 网关错误503 Service Unavailable: 服务不可用
4.2 错误处理最佳实践
/// 统一的网络请求错误处理
class NetworkException implements Exception {
final String message;
final int? statusCode;
NetworkException(this.message, {this.statusCode});
String toString() => 'NetworkException: $message (状态码: $statusCode)';
}
/// 封装的网络请求方法,包含完整的错误处理
Future<T> safeRequest<T>({
required Future<Response> Function() request,
required T Function(dynamic data) parser,
}) async {
try {
final response = await request();
// 根据状态码判断请求结果
if (response.statusCode >= 200 && response.statusCode < 300) {
return parser(response.data);
} else if (response.statusCode == 401) {
throw NetworkException('未授权,请先登录', statusCode: 401);
} else if (response.statusCode == 404) {
throw NetworkException('请求的资源不存在', statusCode: 404);
} else if (response.statusCode >= 500) {
throw NetworkException('服务器错误,请稍后重试', statusCode: response.statusCode);
} else {
throw NetworkException('请求失败', statusCode: response.statusCode);
}
} on DioException catch (e) {
// 处理Dio特定的异常
if (e.type == DioExceptionType.connectionTimeout) {
throw NetworkException('连接超时,请检查网络');
} else if (e.type == DioExceptionType.receiveTimeout) {
throw NetworkException('接收数据超时');
} else if (e.type == DioExceptionType.badResponse) {
throw NetworkException('服务器响应错误', statusCode: e.response?.statusCode);
} else {
throw NetworkException('网络请求失败: ${e.message}');
}
} catch (e) {
throw NetworkException('未知错误: $e');
}
}
// 使用示例
Future<List<User>> fetchUsers() async {
return await safeRequest(
request: () => ApiService().get('/users'),
parser: (data) => (data as List).map((json) => User.fromJson(json)).toList(),
);
}
4.3 请求取消和超时控制
/// 支持取消的ApiService实现
class ApiService {
static final ApiService _instance = ApiService._internal();
factory ApiService() => _instance;
ApiService._internal();
late Dio _dio;
void initialize() {
_dio = Dio(BaseOptions(
baseUrl: 'https://jsonplaceholder.typicode.com',
connectTimeout: Duration(seconds: 10),
receiveTimeout: Duration(seconds: 10),
));
}
/// 支持CancelToken的GET请求
/// [path] 请求路径
/// [params] 查询参数
/// [cancelToken] 用于取消请求的token
Future<Response> get(
String path, {
Map<String, dynamic>? params,
CancelToken? cancelToken,
}) async {
return await _dio.get(
path,
queryParameters: params,
cancelToken: cancelToken, // 传递CancelToken
);
}
/// 支持CancelToken的POST请求
Future<Response> post(
String path, {
dynamic data,
CancelToken? cancelToken,
}) async {
return await _dio.post(
path,
data: data,
cancelToken: cancelToken,
);
}
}
/// 使用CancelToken取消请求
class RequestManager {
final _cancelTokens = <String, CancelToken>{};
/// 发送可取消的请求
Future<Response> cancelableRequest(
String key,
Future<Response> Function(CancelToken) request,
) async {
// 取消之前的同名请求
_cancelTokens[key]?.cancel('新请求已发起');
// 创建新的CancelToken
final cancelToken = CancelToken();
_cancelTokens[key] = cancelToken;
try {
final response = await request(cancelToken);
_cancelTokens.remove(key);
return response;
} catch (e) {
_cancelTokens.remove(key);
rethrow;
}
}
/// 取消指定请求
void cancel(String key) {
_cancelTokens[key]?.cancel('用户取消');
_cancelTokens.remove(key);
}
/// 取消所有请求
void cancelAll() {
_cancelTokens.forEach((key, token) => token.cancel('取消所有请求'));
_cancelTokens.clear();
}
}
// 使用示例
final requestManager = RequestManager();
// 发送可取消的搜索请求
Future<void> searchUsers(String keyword) async {
try {
final response = await requestManager.cancelableRequest(
'search',
(cancelToken) => ApiService().get(
'/users',
params: {'q': keyword},
cancelToken: cancelToken,
),
);
// 处理响应...
} catch (e) {
if (e is DioException && CancelToken.isCancel(e)) {
print('请求已取消');
} else {
print('请求失败: $e');
}
}
}
4.4 请求重试机制
/// 带重试机制的请求拦截器
class RetryInterceptor extends Interceptor {
final int maxRetries;
final Duration retryDelay;
RetryInterceptor({
this.maxRetries = 3,
this.retryDelay = const Duration(seconds: 1),
});
void onError(DioException err, ErrorInterceptorHandler handler) async {
// 只对特定错误进行重试
if (_shouldRetry(err)) {
final retryCount = err.requestOptions.extra['retryCount'] ?? 0;
if (retryCount < maxRetries) {
print('请求失败,${retryDelay.inSeconds}秒后进行第${retryCount + 1}次重试...');
// 等待一段时间后重试
await Future.delayed(retryDelay);
// 更新重试次数
err.requestOptions.extra['retryCount'] = retryCount + 1;
// 重新发送请求
try {
final response = await Dio().fetch(err.requestOptions);
handler.resolve(response);
return;
} catch (e) {
// 重试失败,继续错误处理
}
}
}
// 不重试或重试次数已用完
handler.next(err);
}
bool _shouldRetry(DioException err) {
// 只对网络错误和5xx服务器错误进行重试
return err.type == DioExceptionType.connectionTimeout ||
err.type == DioExceptionType.receiveTimeout ||
(err.response?.statusCode ?? 0) >= 500;
}
}
// 使用示例
void initializeDioWithRetry() {
final dio = Dio();
dio.interceptors.add(RetryInterceptor(maxRetries: 3));
}
4.5 请求缓存策略
/// 简单的内存缓存实现
class CacheInterceptor extends Interceptor {
final _cache = <String, CacheEntry>{};
final Duration cacheDuration;
CacheInterceptor({this.cacheDuration = const Duration(minutes: 5)});
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
// 只缓存GET请求
if (options.method == 'GET') {
final key = _getCacheKey(options);
final cached = _cache[key];
// 如果缓存存在且未过期,直接返回缓存数据
if (cached != null && !cached.isExpired) {
print('使用缓存数据: $key');
handler.resolve(cached.response);
return;
}
}
handler.next(options);
}
void onResponse(Response response, ResponseInterceptorHandler handler) {
// 缓存成功的GET请求响应
if (response.requestOptions.method == 'GET' &&
response.statusCode == 200) {
final key = _getCacheKey(response.requestOptions);
_cache[key] = CacheEntry(
response: response,
expireTime: DateTime.now().add(cacheDuration),
);
print('缓存数据: $key');
}
handler.next(response);
}
String _getCacheKey(RequestOptions options) {
return '${options.uri}';
}
/// 清除所有缓存
void clearCache() {
_cache.clear();
}
/// 清除过期缓存
void clearExpiredCache() {
_cache.removeWhere((key, entry) => entry.isExpired);
}
}
class CacheEntry {
final Response response;
final DateTime expireTime;
CacheEntry({required this.response, required this.expireTime});
bool get isExpired => DateTime.now().isAfter(expireTime);
}
4.6 文件上传和下载
/// 文件上传示例
Future<void> uploadFile(String filePath) async {
try {
// 创建FormData
final formData = FormData.fromMap({
'file': await MultipartFile.fromFile(
filePath,
filename: filePath.split('/').last,
),
'description': '文件描述',
});
// 发送上传请求,带进度回调
final response = await Dio().post(
'https://api.example.com/upload',
data: formData,
onSendProgress: (sent, total) {
final progress = (sent / total * 100).toStringAsFixed(1);
print('上传进度: $progress%');
},
);
print('上传成功: ${response.data}');
} catch (e) {
print('上传失败: $e');
}
}
/// 文件下载示例
Future<void> downloadFile(String url, String savePath) async {
try {
await Dio().download(
url,
savePath,
onReceiveProgress: (received, total) {
if (total != -1) {
final progress = (received / total * 100).toStringAsFixed(1);
print('下载进度: $progress%');
}
},
);
print('下载完成: $savePath');
} catch (e) {
print('下载失败: $e');
}
}
学习总结
通过这一章的学习,你已经掌握了:
网络请求技术
| 库 | 特点 | 适用场景 |
|---|---|---|
| http | 简单轻量 | 基础HTTP请求 |
| Dio | 功能强大 | 复杂网络需求 |
HTTP方法:
- GET: 获取数据
- POST: 创建数据
- PUT: 更新数据
- DELETE: 删除数据
️ 实践技能
- 发送请求: 使用http/Dio发送各种HTTP请求
- 处理响应: 解析响应数据和状态码
- 错误处理: 使用try-catch处理网络异常
- JSON转换: 在JSON和Dart对象之间转换
最佳实践
- 封装服务类: 将网络请求封装为服务类
- 统一错误处理: 使用拦截器统一处理错误
- 数据模型: 使用强类型模型而非Map
- 异步处理: 正确使用async/await
- 超时设置: 设置合理的超时时间
- 并发控制: 合理使用并发请求
常见问题
1. 如何处理超时?
Dio(BaseOptions(
connectTimeout: Duration(seconds: 10),
receiveTimeout: Duration(seconds: 10),
));
2. 如何添加请求头?
headers: {
'Authorization': 'Bearer token',
'Content-Type': 'application/json',
}
3. 如何处理错误?
try {
final response = await http.get(url);
if (response.statusCode == 200) {
// 成功处理
} else {
// 错误处理
}
} catch (e) {
// 异常处理
}
恭喜你完成了Flutter网络请求与API的学习! 现在你已经掌握了与服务器交互的核心技能!
浙公网安备 33010602011771号