Flutter网络请求实战指南:从基础到高级封装
Flutter网络请求实战指南:从基础到高级封装
网络请求是Flutter应用与后端交互的核心环节,几乎所有商业应用都离不开数据的获取与提交。从简单的GET请求获取列表数据,到复杂的POST表单提交、文件上传/下载,再到请求拦截、异常统一处理、缓存策略,Flutter提供了多种实现方案,但在实际开发中需解决请求复用、状态同步、异常兼容等关键问题。本文从基础网络请求用法入手,逐步深入请求封装、状态管理结合、异常处理、高级场景(上传/下载、WebSocket)及性能优化,帮助开发者构建稳定、高效、可维护的网络请求体系。
一、基础网络请求:常用库与原生实现
Flutter中实现网络请求主要有两种方式:一是使用Dart原生的HttpClient,二是使用第三方库(如dio、http)。第三方库封装了更多实用功能(如拦截器、表单提交、取消请求),是实际开发的首选。
1. 核心库对比与选型
-
HttpClient:Dart原生库,无需依赖第三方包,轻量但功能简陋,需手动处理编码、拦截、异常等,适合简单场景或学习;
-
http:官方推荐的轻量级网络库,API简洁,支持基本的GET/POST请求,需配合其他库实现复杂功能(如拦截、缓存),适合中小型应用;
-
dio:功能全面的网络库,支持拦截器、取消请求、FormData提交、文件上传/下载、超时设置、请求重试等,生态完善,是大型应用的首选。
本文以最常用的dio库为例展开(需在pubspec.yaml中添加依赖:dio: ^5.0.0)。
2. 基础请求示例
(1)GET请求:获取列表数据
适用于获取数据列表(如商品列表、新闻列表),请求参数通常拼接在URL后。
import 'package:dio/dio.dart';
// 初始化Dio实例
final Dio _dio = Dio();
// GET请求获取商品列表
Future<List<Product>> fetchProductList() async {
try {
// 1. 发起GET请求
final response = await _dio.get(
'https://api.example.com/products',
queryParameters: { // URL参数
'page': 1,
'limit': 20,
},
options: Options(
headers: { // 请求头
'Content-Type': 'application/json',
'Authorization': 'Bearer your_token',
},
timeout: Duration(seconds: 10), // 超时时间
),
);
// 2. 解析响应数据(假设后端返回格式:{ "code": 200, "msg": "success", "data": [...] })
if (response.data['code'] == 200) {
List<dynamic> dataList = response.data['data'];
return dataList.map((json) => Product.fromJson(json)).toList();
} else {
throw Exception('请求失败:${response.data['msg']}');
}
} catch (e) {
// 3. 异常处理
throw Exception('获取商品列表失败:$e');
}
}
// 商品模型类
class Product {
final int id;
final String name;
final double price;
Product({required this.id, required this.name, required this.price});
// 从JSON解析模型
factory Product.fromJson(Map<String, dynamic> json) {
return Product(
id: json['id'],
name: json['name'],
price: json['price'].toDouble(),
);
}
}
(2)POST请求:提交表单数据
适用于用户登录、数据提交等场景,支持JSON格式提交和FormData表单提交。
// 1. JSON格式提交(登录请求)
Future<User> loginWithJson() async {
try {
final response = await _dio.post(
'https://api.example.com/login',
data: { // JSON数据
'username': 'admin',
'password': '123456',
},
options: Options(
headers: {'Content-Type': 'application/json'},
),
);
if (response.data['code'] == 200) {
return User.fromJson(response.data['data']);
} else {
throw Exception(response.data['msg']);
}
} catch (e) {
throw Exception('登录失败:$e');
}
}
// 2. FormData表单提交(上传用户信息)
Future<void> submitUserInfo() async {
try {
// 构建FormData
FormData formData = FormData.fromMap({
'name': '张三',
'age': 25,
'avatar': await MultipartFile.fromFile(
'/path/to/avatar.jpg', // 本地文件路径
filename: 'avatar.jpg',
),
});
final response = await _dio.post(
'https://api.example.com/user/update',
data: formData,
options: Options(
headers: {'Authorization': 'Bearer your_token'},
),
);
if (response.data['code'] != 200) {
throw Exception(response.data['msg']);
}
} catch (e) {
throw Exception('提交用户信息失败:$e');
}
}
// 用户模型类
class User {
final int id;
final String username;
final String token;
User({required this.id, required this.username, required this.token});
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'],
username: json['username'],
token: json['token'],
);
}
}
二、网络请求封装:提升复用性与可维护性
在实际开发中,直接使用原生dio请求会导致代码冗余(如重复的请求头、超时设置、异常处理)。通过封装网络请求,可以统一管理请求配置、拦截器、异常处理,提升代码复用性与可维护性。
1. 封装核心思路
-
单例模式管理Dio实例,避免重复创建;
-
统一配置基础参数(基础URL、超时时间、请求头);
-
添加请求/响应拦截器,处理token添加、日志打印、响应统一解析;
-
封装通用的GET/POST方法,简化请求调用;
-
统一异常处理,区分网络异常、服务器异常、业务异常。
2. 完整封装实现
import 'package:dio/dio.dart';
// 网络请求工具类(单例模式)
class HttpUtil {
// 单例实例
static final HttpUtil _instance = HttpUtil._internal();
factory HttpUtil() => _instance;
// Dio实例
late Dio _dio;
// 私有构造函数:初始化Dio配置
HttpUtil._internal() {
_dio = Dio();
// 1. 基础配置
_dio.options = BaseOptions(
baseUrl: 'https://api.example.com/', // 基础URL
connectTimeout: Duration(seconds: 10), // 连接超时
receiveTimeout: Duration(seconds: 10), // 接收超时
contentType: 'application/json; charset=utf-8', // 默认请求头
);
// 2. 添加请求拦截器
_dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) {
// 示例:添加Token
String? token = _getToken();
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
// 打印请求日志
print('请求URL:${options.uri}');
print('请求参数:${options.data}');
handler.next(options); // 继续请求
},
onResponse: (response, handler) {
// 打印响应日志
print('响应数据:${response.data}');
handler.next(response); // 继续处理响应
},
onError: (DioException e, handler) {
// 打印错误日志
print('请求错误:${e.message}');
handler.next(e); // 继续处理错误
},
),
);
// 可选:添加日志拦截器(更详细的日志)
_dio.interceptors.add(LogInterceptor(responseBody: true));
}
// 从本地获取Token(示例:实际可从SharedPreferences获取)
String? _getToken() {
// 这里简化处理,实际项目中需结合本地存储
return 'your_saved_token';
}
// 通用GET请求
Future<T> get<T>(
String path, {
Map<String, dynamic>? queryParameters,
Options? options,
}) async {
try {
Response response = await _dio.get(
path,
queryParameters: queryParameters,
options: options,
);
return _handleResponse<T>(response);
} catch (e) {
throw _handleError(e);
}
}
// 通用POST请求
Future<T> post<T>(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
Options? options,
}) async {
try {
Response response = await _dio.post(
path,
data: data,
queryParameters: queryParameters,
options: options,
);
return _handleResponse<T>(response);
} catch (e) {
throw _handleError(e);
}
}
// 响应统一处理
T _handleResponse<T>(Response response) {
Map<String, dynamic> data = response.data;
// 假设后端统一响应格式:{ "code": 200, "msg": "success", "data": ... }
if (data['code'] == 200) {
return data['data'] as T;
} else {
// 业务异常:抛出具体错误信息
throw BusinessException(
code: data['code'],
message: data['msg'] ?? '请求失败',
);
}
}
// 异常统一处理
Exception _handleError(dynamic e) {
if (e is DioException) {
// Dio异常:网络错误、超时等
switch (e.type) {
case DioExceptionType.connectionTimeout:
return NetworkException('网络连接超时');
case DioExceptionType.sendTimeout:
return NetworkException('请求发送超时');
case DioExceptionType.receiveTimeout:
return NetworkException('响应接收超时');
case DioExceptionType.connectionError:
return NetworkException('网络连接错误');
case DioExceptionType.cancel:
return NetworkException('请求已取消');
default:
return NetworkException('网络请求错误:${e.message}');
}
} else if (e is BusinessException) {
// 业务异常:直接抛出
return e;
} else {
// 其他异常
return Exception('未知错误:$e');
}
}
// 取消请求(需要传入CancelToken)
void cancelRequest(CancelToken cancelToken) {
if (!cancelToken.isCancelled) {
cancelToken.cancel('请求已取消');
}
}
}
// 自定义异常:业务异常(后端返回的错误)
class BusinessException extends Exception {
final int code;
final String message;
BusinessException({required this.code, required this.message});
@override
String toString() => 'BusinessException: $code - $message';
}
// 自定义异常:网络异常
class NetworkException extends Exception {
final String message;
NetworkException(this.message);
@override
String toString() => 'NetworkException: $message';
}
3. 封装后使用示例
// 1. 获取商品列表
Future<List<Product>> fetchProductList() async {
try {
List<dynamic> data = await HttpUtil().get(
'products',
queryParameters: {'page': 1, 'limit': 20},
);
return data.map((json) => Product.fromJson(json)).toList();
} on BusinessException catch (e) {
print('业务错误:${e.code} - ${e.message}');
rethrow;
} on NetworkException catch (e) {
print('网络错误:${e.message}');
rethrow;
} catch (e) {
print('其他错误:$e');
rethrow;
}
}
// 2. 登录请求
Future<User> login(String username, String password) async {
try {
Map<String, dynamic> data = await HttpUtil().post(
'login',
data: {'username': username, 'password': password},
);
return User.fromJson(data);
} catch (e) {
throw Exception('登录失败:$e');
}
}
// 3. 取消请求示例
void testCancelRequest() {
CancelToken cancelToken = CancelToken();
// 发起请求
HttpUtil().get('products', cancelToken: cancelToken).then((data) {
print('请求成功:$data');
}).catchError((e) {
print('请求失败:$e');
});
// 取消请求(如页面销毁时)
HttpUtil().cancelRequest(cancelToken);
}
三、网络请求与状态管理结合:实现数据与UI同步
网络请求的结果需要同步到UI界面,而复杂应用中需管理“加载中”“加载成功”“加载失败”等状态。结合状态管理方案(如Provider、Bloc、Riverpod),可实现请求状态的统一管理与UI的自动更新。
1. 结合Provider实现请求状态管理
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
// 1. 定义请求状态模型
class ProductListProvider extends ChangeNotifier {
List<Product> _productList = [];
bool _isLoading = false;
String? _errorMessage;
// getter
List<Product> get productList => _productList;
bool get isLoading => _isLoading;
String? get errorMessage => _errorMessage;
// 获取商品列表(带状态管理)
Future<void> fetchProductList() async {
// 1. 开始加载:更新状态为加载中
_isLoading = true;
_errorMessage = null;
notifyListeners();
try {
// 2. 发起网络请求
List<dynamic> data = await HttpUtil().get(
'products',
queryParameters: {'page': 1, 'limit': 20},
);
_productList = data.map((json) => Product.fromJson(json)).toList();
} catch (e) {
// 3. 加载失败:保存错误信息
_errorMessage = e.toString();
} finally {
// 4. 加载完成:更新状态为非加载中
_isLoading = false;
notifyListeners();
}
}
}
// 2. UI组件:展示商品列表(消费状态)
class ProductListPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => ProductListProvider()..fetchProductList(),
child: Scaffold(
appBar: AppBar(title: Text('商品列表')),
body: Consumer<ProductListProvider>(
builder: (context, provider, child) {
// 加载中
if (provider.isLoading) {
return Center(child: CircularProgressIndicator());
}
// 加载失败
if (provider.errorMessage != null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(provider.errorMessage!),
SizedBox(height: 20),
ElevatedButton(
onPressed: () => provider.fetchProductList(),
child: Text('重试'),
),
],
),
);
}
// 加载成功:展示列表
return ListView.builder(
itemCount: provider.productList.length,
itemBuilder: (context, index) {
Product product = provider.productList[index];
return ListTile(
leading: Text('${product.id}'),
title: Text(product.name),
subtitle: Text('¥${product.price}'),
);
},
);
},
),
),
);
}
}
2. 结合Bloc实现请求状态管理(进阶)
对于复杂业务场景,使用Bloc可实现更清晰的状态流转,便于测试与多人协作。
import 'package:bloc/bloc.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
// 1. 定义事件
enum ProductListEvent { fetch }
// 2. 定义状态
abstract class ProductListState {}
class ProductListLoading extends ProductListState {}
class ProductListSuccess extends ProductListState {
final List<Product> productList;
ProductListSuccess(this.productList);
}
class ProductListFailure extends ProductListState {
final String errorMessage;
ProductListFailure(this.errorMessage);
}
// 3. 定义Bloc
class ProductListBloc extends Bloc<ProductListEvent, ProductListState> {
ProductListBloc() : super(ProductListLoading()) {
on<ProductListEvent>((event, emit) async {
if (event == ProductListEvent.fetch) {
emit(ProductListLoading()); // 发射加载中状态
try {
List<dynamic> data = await HttpUtil().get(
'products',
queryParameters: {'page': 1, 'limit': 20},
);
List<Product> productList =
data.map((json) => Product.fromJson(json)).toList();
emit(ProductListSuccess(productList)); // 发射成功状态
} catch (e) {
emit(ProductListFailure(e.toString())); // 发射失败状态
}
}
});
}
}
// 4. UI组件:消费Bloc状态
class ProductListPageWithBloc extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => ProductListBloc()..add(ProductListEvent.fetch),
child: Scaffold(
appBar: AppBar(title: Text('商品列表(Bloc)')),
body: BlocBuilder<ProductListBloc, ProductListState>(
builder: (context, state) {
if (state is ProductListLoading) {
return Center(child: CircularProgressIndicator());
} else if (state is ProductListFailure) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(state.errorMessage),
SizedBox(height: 20),
ElevatedButton(
onPressed: () =>
context.read<ProductListBloc>().add(ProductListEvent.fetch),
child: Text('重试'),
),
],
),
);
} else if (state is ProductListSuccess) {
return ListView.builder(
itemCount: state.productList.length,
itemBuilder: (context, index) {
Product product = state.productList[index];
return ListTile(
leading: Text('${product.id}'),
title: Text(product.name),
subtitle: Text('¥${product.price}'),
);
},
);
} else {
return Center(child: Text('未知状态'));
}
},
),
),
);
}
}
四、高级网络场景:上传/下载与WebSocket
除了基础的GET/POST请求,实际开发中还会遇到文件上传/下载、实时通信(WebSocket)等高级场景,以下是基于dio和Flutter原生API的实现方案。
1. 文件上传(基于dio)
支持单文件上传、多文件上传,可监听上传进度。
// 单文件上传(上传头像)
Future<String> uploadAvatar(String filePath) async {
try {
FormData formData = FormData.fromMap({
'avatar': await MultipartFile.fromFile(
filePath,
filename: 'avatar.jpg',
contentType: MediaType('image', 'jpeg'), // 指定文件类型
),
});
Response response = await HttpUtil()._dio.post(
'user/upload-avatar',
data: formData,
onSendProgress: (int sent, int total) {
// 监听上传进度
double progress = sent / total;
print('上传进度:${(progress * 100).toStringAsFixed(2)}%');
},
);
if (response.data['code'] == 200) {
return response.data['data']['avatarUrl']; // 返回上传后的图片URL
} else {
throw BusinessException(
code: response.data['code'],
message: response.data['msg'],
);
}
} catch (e) {
throw Exception('上传头像失败:$e');
}
}
// 多文件上传(上传多张图片)
Future<List<String>> uploadImages(List<String> filePaths) async {
try {
List<MultipartFile> files = [];
for (String path in filePaths) {
files.add(
await MultipartFile.fromFile(
path,
filename: path.split('/').last,
),
);
}
FormData formData = FormData.fromMap({
'images': files, // 多文件用列表
});
Response response = await HttpUtil()._dio.post(
'common/upload-images',
data: formData,
onSendProgress: (int sent, int total) {
double progress = sent / total;
print('上传进度:${(progress * 100).toStringAsFixed(2)}%');
},
);
if (response.data['code'] == 200) {
List<dynamic> data = response.data['data'];
return data.map((item) => item['imageUrl'] as String).toList();
} else {
throw BusinessException(
code: response.data['code'],
message: response.data['msg'],
);
}
} catch (e) {
throw Exception('上传图片失败:$e');
}
}
2. 文件下载(基于dio)
支持断点续传、下载进度监听,需指定本地保存路径。
// 文件下载(支持进度监听)
Future<void> downloadFile(String url, String savePath) async {
try {
await HttpUtil()._dio.download(
url,
savePath,
onReceiveProgress: (int received, int total) {
// 监听下载进度
if (total != -1) {
double progress = received / total;
print('下载进度:${(progress * 100).toStringAsFixed(2)}%');
}
},
options: Options(
responseType: ResponseType.stream, // 响应类型为流
),
);
print('文件下载完成:$savePath');
} catch (e) {
throw Exception('文件下载失败:$e');
}
}
// 断点续传(基于Range请求头)
Future<void> resumeDownload(String url, String savePath) async {
try {
// 1. 检查本地已下载的文件长度
File file = File(savePath);
int downloadedLength = 0;
if (await file.exists()) {
downloadedLength = await file.length();
}
// 2. 发起带Range的请求(从已下载位置继续下载)
await HttpUtil()._dio.download(
url,
savePath,
options: Options(
headers: {
'Range': 'bytes=$downloadedLength-', // 指定续传起始位置
},
responseType: ResponseType.stream,
),
onReceiveProgress: (int received, int total) {
// 总长度 = 已下载长度 + 本次下载长度
int totalLength = downloadedLength + total;
double progress = (downloadedLength + received) / totalLength;
print('续传进度:${(progress * 100).toStringAsFixed(2)}%');
},
// 追加写入文件(断点续传核心)
deleteOnError: false, // 下载错误时不删除已下载文件
);
print('断点续传完成:$savePath');
} catch (e) {
throw Exception('断点续传失败:$e');
}
}
3. WebSocket实时通信(基于Flutter原生)
适用于实时聊天、实时数据推送等场景,Flutter原生提供web_socket_channel库(需添加依赖:web_socket_channel: ^2.0.0)。
import 'package:web_socket_channel/io.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
class WebSocketManager {
late WebSocketChannel _channel;
bool _isConnected = false;
// 连接WebSocket
Future<void> connect(String url) async {
try {
_channel = IOWebSocketChannel.connect(url);
_isConnected = true;
print('WebSocket连接成功');
// 监听消息
_channel.stream.listen(
(message) {
print('收到消息:$message');
// 处理消息(如解析JSON、更新UI)
_handleMessage(message);
},
onError: (error) {
print('WebSocket错误:$error');
_isConnected = false;
// 自动重连
_reconnect(url);
},
onDone: () {
print('WebSocket连接关闭');
_isConnected = false;
// 自动重连
_reconnect(url);
},
);
} catch (e) {
print('WebSocket连接失败:$e');
_isConnected = false;
// 自动重连
_reconnect(url);
}
}
// 发送消息
void sendMessage(String message) {
if (_isConnected) {
_channel.sink.add(message);
} else {
print('WebSocket未连接,无法发送消息');
}
}
// 关闭连接
void close() {
if (_isConnected) {
_channel.sink.close();
_isConnected = false;
print('WebSocket手动关闭');
}
}
// 处理收到的消息
void _handleMessage(dynamic message) {
// 示例:解析JSON消息
// Map<String, dynamic> data = json.decode(message);
// 根据消息类型处理不同业务...
}
// 自动重连
void _reconnect(String url) {
Future.delayed(Duration(seconds: 3), () {
if (!_isConnected) {
print('尝试重连WebSocket...');
connect(url);
}
});
}
}
// 使用示例
void testWebSocket() {
WebSocketManager manager = WebSocketManager();
// 连接WebSocket服务端
manager.connect('ws://api.example.com/chat');
// 发送消息
manager.sendMessage('Hello, WebSocket!');
// 页面销毁时关闭连接
// manager.close();
}
五、网络请求优化与最佳实践
在实际开发中,除了功能实现,还需关注网络请求的性能优化、用户体验优化与稳定性保障,以下是核心优化点与最佳实践。
1. 性能优化技巧
-
请求缓存策略:对高频获取且不常变化的数据(如商品分类、地区列表)实现缓存,优先从本地缓存获取,缓存过期后再请求网络;可使用
dio_cache_interceptor库实现缓存; -
请求合并与节流:对于频繁触发的请求(如搜索输入框实时联想),使用节流(如300ms内只发起一次请求)或防抖,减少无效请求;
-
图片懒加载与压缩:图片请求时指定尺寸(如后端支持),减少图片体积;实现图片懒加载,避免一次性加载过多图片;
-
断点续传与分片上传:大文件上传/下载时使用断点续传或分片上传,提升传输稳定性,避免网络中断后重新传输;
-
并行请求与串行请求合理使用:多个独立请求(如获取用户信息+商品列表)使用并行请求(
Future.wait)提升效率;依赖关系的请求(如先登录再获取数据)使用串行请求。
2. 稳定性保障最佳实践
-
统一异常处理:通过拦截器统一处理网络异常、业务异常,避免重复代码,提升代码可维护性;
-
请求重试机制:对临时网络错误(如连接超时、网络波动)实现自动重试,可通过
dio_retry库实现,设置合理的重试次数与间隔; -
请求取消:页面销毁时取消未完成的请求,避免内存泄漏与不必要的资源消耗;
-
超时设置:根据业务场景设置合理的超时时间(如普通接口10秒,文件上传30秒),避免长时间等待;
-
数据加密与安全:敏感数据(如登录密码)传输时使用HTTPS加密;必要时对请求参数进行签名,防止数据篡改。
3. 用户体验优化
-
加载状态反馈:请求过程中显示加载动画(如CircularProgressIndicator、骨架屏),避免用户误以为应用无响应;
-
错误提示友好:网络错误、业务错误时显示清晰的提示信息(如“网络连接失败,请检查网络”),并提供重试按钮;
-
弱网/离线提示:检测到弱网或离线状态时,给出相应提示,引导用户检查网络;
-
后台请求处理:后台请求(如下载文件)在应用退到后台时继续执行,前台显示进度通知。
六、结语:构建可靠的Flutter网络层
Flutter网络请求开发的核心是“稳定、高效、可维护”,从基础的请求实现到高级的封装与优化,需围绕“功能完整性、状态可管理、异常可兼容、体验友好”四个核心目标展开。
在实际开发中,建议优先使用成熟的第三方库(如dio),并基于项目需求封装统一的网络工具类,结合状态管理方案实现请求状态与UI的同步;同时,注重性能优化与稳定性保障,如缓存策略、重试机制、请求取消等,提升应用的整体体验。
通过本文介绍的基础用法、封装技巧、高级场景实现与最佳实践,开发者可快速构建可靠的Flutter网络层,满足各类业务场景的需求。同时,需结合具体业务场景灵活调整方案,持续优化网络请求的性能与用户体验,为应用的稳定运行提供核心保障。
欢迎大家加入开源鸿蒙跨平台开发者社区,一起共建开源鸿蒙跨平台生态。
更多推荐

所有评论(0)