Appearance
第9章:Flutter 网络请求
9.1 网络请求的概念
什么是网络请求?
在 Flutter 中,网络请求是指应用与后端服务器之间的通信过程,用于获取数据、提交数据或执行其他网络操作。
网络请求的作用:
- 获取远程数据(如用户信息、商品列表、新闻资讯)
- 提交数据(如用户注册、登录、表单提交)
- 上传/下载文件
- 与后端 API 交互
常见的网络请求方法
- GET:获取数据,参数附加在 URL 中
- POST:提交数据,参数放在请求体中
- PUT:更新数据
- DELETE:删除数据
- PATCH:部分更新数据
9.2 常用网络请求插件
Dio 插件
Dio 是 Flutter 中最常用的网络请求插件,功能强大,支持多种请求方式、拦截器、请求取消等特性。
安装:
yaml
dependencies:
dio: ^5.4.3+1基本配置:
dart
import 'package:dio/dio.dart';
// 创建 Dio 实例
final dio = Dio(
BaseOptions(
baseUrl: 'https://api.example.com', // 基础 URL
connectTimeout: Duration(seconds: 10), // 连接超时
receiveTimeout: Duration(seconds: 10), // 接收超时
headers: {
'Content-Type': 'application/json',
},
),
);其他网络请求插件
- http:Flutter 官方推荐的基础网络请求库
- retrofit:基于 Dio 的 RESTful API 客户端生成器
- chopper:另一个 RESTful API 客户端
9.3 GET 请求
基本用法
dart
Future<void> fetchData() async {
try {
// 发送 GET 请求
final response = await dio.get('/users', queryParameters: {
'page': 1,
'limit': 10,
});
// 处理响应数据
print('Response: ${response.data}');
} catch (e) {
// 处理错误
print('Error: $e');
}
}带参数的 GET 请求
dart
// 方式 1:使用 queryParameters
final response = await dio.get('/users', queryParameters: {
'id': 123,
'name': 'John',
});
// 方式 2:直接拼接 URL
final response = await dio.get('/users?id=123&name=John');响应数据解析
dart
// 解析 JSON 数据
final data = response.data;
final users = data['users'] as List;
for (var user in users) {
print('User: ${user['name']}');
}9.4 POST 请求
基本用法
dart
Future<void> submitData() async {
try {
// 发送 POST 请求
final response = await dio.post('/users', data: {
'name': 'John Doe',
'email': 'john@example.com',
'password': '123456',
});
// 处理响应数据
print('Response: ${response.data}');
} catch (e) {
// 处理错误
print('Error: $e');
}
}不同格式的请求体
JSON 格式(默认)
dart
final response = await dio.post('/users', data: {
'name': 'John Doe',
'email': 'john@example.com',
});Form-Data 格式
dart
import 'package:dio/dio.dart';
final formData = FormData.fromMap({
'name': 'John Doe',
'email': 'john@example.com',
'avatar': await MultipartFile.fromFile('./avatar.png', filename: 'avatar.png'),
});
final response = await dio.post('/users', data: formData);文本格式
dart
final response = await dio.post('/users',
data: 'name=John Doe&email=john@example.com',
options: Options(
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
),
);9.5 响应数据解析
手动解析 JSON
dart
// 定义用户模型
class User {
final int id;
final String name;
final String email;
User({required this.id, required this.name, required this.email});
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'],
name: json['name'],
email: json['email'],
);
}
}
// 解析响应数据
final response = await dio.get('/users/1');
final user = User.fromJson(response.data);
print('User: ${user.name}, ${user.email}');使用 json_serializable 自动生成代码
安装依赖:
yaml
dependencies:
json_annotation: ^4.8.1
dev_dependencies:
build_runner: ^2.4.6
json_serializable: ^6.7.1定义模型类:
dart
import 'package:json_annotation/json_annotation.dart';
part 'user.g.dart';
@JsonSerializable()
class User {
final int id;
final String name;
final String email;
User({required this.id, required this.name, required this.email});
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
}生成代码:
bash
flutter pub run build_runner build使用生成的代码:
dart
final response = await dio.get('/users/1');
final user = User.fromJson(response.data);
print('User: ${user.name}, ${user.email}');解析复杂数据结构
dart
@JsonSerializable()
class UserList {
final int total;
final List<User> users;
UserList({required this.total, required this.users});
factory UserList.fromJson(Map<String, dynamic> json) => _$UserListFromJson(json);
Map<String, dynamic> toJson() => _$UserListToJson(this);
}
// 解析列表数据
final response = await dio.get('/users');
final userList = UserList.fromJson(response.data);
print('Total users: ${userList.total}');
for (var user in userList.users) {
print('User: ${user.name}');
}9.6 网络请求异常处理
使用 try-catch 捕获异常
dart
Future<void> fetchData() async {
try {
final response = await dio.get('/users');
print('Success: ${response.data}');
} catch (e) {
if (e is DioException) {
// 处理 Dio 异常
switch (e.type) {
case DioExceptionType.connectionTimeout:
print('Connection timeout');
break;
case DioExceptionType.sendTimeout:
print('Send timeout');
break;
case DioExceptionType.receiveTimeout:
print('Receive timeout');
break;
case DioExceptionType.badResponse:
print('Bad response: ${e.response?.statusCode}');
break;
case DioExceptionType.cancel:
print('Request cancelled');
break;
default:
print('Unknown error: $e');
}
} else {
// 处理其他异常
print('Error: $e');
}
}
}使用拦截器处理异常
dart
// 添加拦截器
dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) {
// 请求前处理
print('Request: ${options.uri}');
return handler.next(options);
},
onResponse: (response, handler) {
// 响应后处理
print('Response: ${response.statusCode}');
return handler.next(response);
},
onError: (error, handler) {
// 错误处理
print('Error: ${error.message}');
return handler.next(error);
},
),
);统一错误处理
dart
class ApiException implements Exception {
final String message;
final int? statusCode;
ApiException(this.message, {this.statusCode});
@override
String toString() => 'ApiException: $message (status: $statusCode)';
}
Future<T> apiRequest<T>(Future<T> Function() request) async {
try {
return await request();
} catch (e) {
if (e is DioException) {
throw ApiException(
e.message ?? 'Network error',
statusCode: e.response?.statusCode,
);
}
throw ApiException('Unknown error: $e');
}
}
// 使用
Future<void> fetchData() async {
try {
final response = await apiRequest(() => dio.get('/users'));
print('Success: ${response.data}');
} catch (e) {
if (e is ApiException) {
print('API Error: ${e.message}');
} else {
print('Error: $e');
}
}
}9.7 实操案例:发送 GET/POST 请求
目标
创建一个应用,实现用户列表的获取和用户注册功能。
步骤 1:创建 API 服务类
dart
import 'package:dio/dio.dart';
import 'user.dart';
class ApiService {
final Dio _dio;
ApiService() : _dio = Dio(
BaseOptions(
baseUrl: 'https://jsonplaceholder.typicode.com',
connectTimeout: Duration(seconds: 10),
receiveTimeout: Duration(seconds: 10),
),
) {
// 添加拦截器
_dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) {
print('Request: ${options.uri}');
return handler.next(options);
},
onResponse: (response, handler) {
print('Response: ${response.statusCode}');
return handler.next(response);
},
onError: (error, handler) {
print('Error: ${error.message}');
return handler.next(error);
},
),
);
}
// 获取用户列表
Future<List<User>> getUsers() async {
final response = await _dio.get('/users');
final List<dynamic> data = response.data;
return data.map((json) => User.fromJson(json)).toList();
}
// 获取单个用户
Future<User> getUser(int id) async {
final response = await _dio.get('/users/$id');
return User.fromJson(response.data);
}
// 注册用户
Future<User> registerUser(Map<String, dynamic> userData) async {
final response = await _dio.post('/users', data: userData);
return User.fromJson(response.data);
}
}步骤 2:创建用户模型
dart
import 'package:json_annotation/json_annotation.dart';
part 'user.g.dart';
@JsonSerializable()
class User {
final int id;
final String name;
final String username;
final String email;
final Address address;
final String phone;
final String website;
final Company company;
User({
required this.id,
required this.name,
required this.username,
required this.email,
required this.address,
required this.phone,
required this.website,
required this.company,
});
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
}
@JsonSerializable()
class Address {
final String street;
final String suite;
final String city;
final String zipcode;
final Geo geo;
Address({
required this.street,
required this.suite,
required this.city,
required this.zipcode,
required this.geo,
});
factory Address.fromJson(Map<String, dynamic> json) => _$AddressFromJson(json);
Map<String, dynamic> toJson() => _$AddressToJson(this);
}
@JsonSerializable()
class Geo {
final String lat;
final String lng;
Geo({required this.lat, required this.lng});
factory Geo.fromJson(Map<String, dynamic> json) => _$GeoFromJson(json);
Map<String, dynamic> toJson() => _$GeoToJson(this);
}
@JsonSerializable()
class Company {
final String name;
final String catchPhrase;
final String bs;
Company({
required this.name,
required this.catchPhrase,
required this.bs,
});
factory Company.fromJson(Map<String, dynamic> json) => _$CompanyFromJson(json);
Map<String, dynamic> toJson() => _$CompanyToJson(this);
}步骤 3:生成模型代码
bash
flutter pub run build_runner build步骤 4:创建页面
UsersPage.dart
dart
import 'package:flutter/material.dart';
import 'api_service.dart';
import 'user.dart';
class UsersPage extends StatefulWidget {
const UsersPage({super.key});
@override
State<UsersPage> createState() => _UsersPageState();
}
class _UsersPageState extends State<UsersPage> {
final ApiService _apiService = ApiService();
List<User> _users = [];
bool _isLoading = false;
String? _error;
@override
void initState() {
super.initState();
_fetchUsers();
}
Future<void> _fetchUsers() async {
setState(() {
_isLoading = true;
_error = null;
});
try {
final users = await _apiService.getUsers();
setState(() {
_users = users;
});
} catch (e) {
setState(() {
_error = 'Failed to fetch users: $e';
});
} finally {
setState(() {
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Users')),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: _error != null
? Center(child: Text(_error!))
: ListView.builder(
itemCount: _users.length,
itemBuilder: (context, index) {
final user = _users[index];
return ListTile(
title: Text(user.name),
subtitle: Text(user.email),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => UserDetailPage(userId: user.id),
),
);
},
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const RegisterPage()),
);
},
child: const Icon(Icons.add),
),
);
}
}UserDetailPage.dart
dart
import 'package:flutter/material.dart';
import 'api_service.dart';
import 'user.dart';
class UserDetailPage extends StatefulWidget {
final int userId;
const UserDetailPage({super.key, required this.userId});
@override
State<UserDetailPage> createState() => _UserDetailPageState();
}
class _UserDetailPageState extends State<UserDetailPage> {
final ApiService _apiService = ApiService();
User? _user;
bool _isLoading = false;
String? _error;
@override
void initState() {
super.initState();
_fetchUser();
}
Future<void> _fetchUser() async {
setState(() {
_isLoading = true;
_error = null;
});
try {
final user = await _apiService.getUser(widget.userId);
setState(() {
_user = user;
});
} catch (e) {
setState(() {
_error = 'Failed to fetch user: $e';
});
} finally {
setState(() {
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('User Detail')),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: _error != null
? Center(child: Text(_error!))
: _user != null
? Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Name: ${_user!.name}', style: const TextStyle(fontSize: 18)),
const SizedBox(height: 8),
Text('Username: ${_user!.username}', style: const TextStyle(fontSize: 16)),
const SizedBox(height: 8),
Text('Email: ${_user!.email}', style: const TextStyle(fontSize: 16)),
const SizedBox(height: 8),
Text('Phone: ${_user!.phone}', style: const TextStyle(fontSize: 16)),
const SizedBox(height: 8),
Text('Website: ${_user!.website}', style: const TextStyle(fontSize: 16)),
const SizedBox(height: 16),
const Text('Address:', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Text('${_user!.address.street}, ${_user!.address.suite}', style: const TextStyle(fontSize: 16)),
Text('${_user!.address.city}, ${_user!.address.zipcode}', style: const TextStyle(fontSize: 16)),
const SizedBox(height: 16),
const Text('Company:', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Text(_user!.company.name, style: const TextStyle(fontSize: 16)),
Text(_user!.company.catchPhrase, style: const TextStyle(fontSize: 16)),
],
),
)
: const Center(child: Text('User not found')),
);
}
}RegisterPage.dart
dart
import 'package:flutter/material.dart';
import 'api_service.dart';
import 'user.dart';
class RegisterPage extends StatefulWidget {
const RegisterPage({super.key});
@override
State<RegisterPage> createState() => _RegisterPageState();
}
class _RegisterPageState extends State<RegisterPage> {
final ApiService _apiService = ApiService();
final _formKey = GlobalKey<FormState>();
final _nameController = TextEditingController();
final _emailController = TextEditingController();
bool _isLoading = false;
String? _error;
User? _registeredUser;
@override
void dispose() {
_nameController.dispose();
_emailController.dispose();
super.dispose();
}
Future<void> _registerUser() async {
if (!_formKey.currentState!.validate()) return;
setState(() {
_isLoading = true;
_error = null;
_registeredUser = null;
});
try {
final userData = {
'name': _nameController.text,
'email': _emailController.text,
'username': _nameController.text.toLowerCase().replaceAll(' ', ''),
'phone': '123-456-7890',
'website': 'example.com',
'address': {
'street': '123 Main St',
'suite': 'Apt 1',
'city': 'New York',
'zipcode': '10001',
'geo': {
'lat': '40.7128',
'lng': '-74.0060',
},
},
'company': {
'name': 'Example Company',
'catchPhrase': 'Great products',
'bs': 'IT services',
},
};
final user = await _apiService.registerUser(userData);
setState(() {
_registeredUser = user;
});
} catch (e) {
setState(() {
_error = 'Failed to register user: $e';
});
} finally {
setState(() {
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Register User')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: _isLoading
? const Center(child: CircularProgressIndicator())
: _registeredUser != null
? Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Icons.check_circle, color: Colors.green, size: 64),
const SizedBox(height: 16),
const Text('User registered successfully!', style: TextStyle(fontSize: 18)),
const SizedBox(height: 16),
Text('Name: ${_registeredUser!.name}', style: const TextStyle(fontSize: 16)),
Text('Email: ${_registeredUser!.email}', style: const TextStyle(fontSize: 16)),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text('Back'),
),
],
)
: Form(
key: _formKey,
child: Column(
children: [
if (_error != null)
Text(_error!, style: const TextStyle(color: Colors.red)),
TextFormField(
controller: _nameController,
decoration: const InputDecoration(labelText: 'Name'),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your name';
}
return null;
},
),
TextFormField(
controller: _emailController,
decoration: const InputDecoration(labelText: 'Email'),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your email';
}
if (!RegExp(r'^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$').hasMatch(value)) {
return 'Please enter a valid email';
}
return null;
},
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: _registerUser,
child: const Text('Register'),
),
],
),
),
),
);
}
}步骤 5:配置主应用
dart
import 'package:flutter/material.dart';
import 'pages/users_page.dart';
void main() {
runApp(
MaterialApp(
title: 'Network Demo',
home: const UsersPage(),
),
);
}步骤 6:运行应用
- 启动模拟器或连接真机
- 运行项目
- 测试用户列表获取和用户注册功能
9.8 新手易错点
请求地址错误
错误:
- 基础 URL 配置错误
- 接口路径拼写错误
- 缺少协议(http/https)
解决方案:
- 仔细检查 API 文档
- 使用 Postman 等工具测试接口
- 确保 URL 格式正确
参数格式错误
错误:
- GET 请求参数放在请求体中
- POST 请求参数放在 URL 中
- 参数类型与后端要求不匹配
解决方案:
- 按照 API 文档要求传递参数
- 使用正确的请求方法
- 检查参数类型和格式
JSON 解析失败
错误:
- JSON 格式错误
- 模型类与 JSON 结构不匹配
- 缺少必要字段
解决方案:
- 使用在线 JSON 验证工具检查格式
- 确保模型类与 JSON 结构一致
- 处理 nullable 字段
网络权限问题
错误:
- Android 网络权限未配置
- iOS 网络权限未配置
- 混合内容(http/https)问题
解决方案:
- 在 AndroidManifest.xml 中添加网络权限
- 在 Info.plist 中配置网络权限
- 使用 https 协议
异常处理不当
错误:
- 未捕获网络异常
- 错误信息不友好
- 未处理不同类型的错误
解决方案:
- 使用 try-catch 捕获异常
- 提供友好的错误提示
- 针对不同错误类型进行处理
性能问题
错误:
- 频繁发送网络请求
- 未使用缓存
- 大文件未使用分块上传/下载
解决方案:
- 合理使用缓存
- 批量处理请求
- 大文件使用分块传输
9.9 网络请求最佳实践
1. 使用服务层封装
- 创建 API 服务类封装网络请求
- 集中管理 API 调用
- 便于测试和维护
2. 使用拦截器
- 添加请求/响应拦截器
- 统一处理认证、日志等
- 简化代码
3. 错误处理
- 统一错误处理机制
- 提供友好的错误提示
- 区分网络错误和业务错误
4. 数据模型
- 使用强类型数据模型
- 自动生成 JSON 解析代码
- 确保数据类型安全
5. 性能优化
- 使用缓存减少网络请求
- 批量处理请求
- 合理设置超时时间
- 使用连接池
6. 安全性
- 使用 HTTPS
- 避免明文传输敏感信息
- 合理使用认证机制
9.10 小结
本章介绍了 Flutter 的网络请求,包括网络请求的概念、常用网络请求插件、GET/POST 请求、响应数据解析、网络请求异常处理等内容。网络请求是 Flutter 应用与后端交互的核心,掌握好网络请求对于构建功能完整的应用至关重要。
我们学习了:
- Dio 插件:功能强大的网络请求库
- GET 请求:获取数据的常用方法
- POST 请求:提交数据的常用方法
- JSON 解析:手动解析和自动生成代码解析
- 异常处理:捕获和处理网络错误
- 最佳实践:封装 API 服务、使用拦截器、优化性能
通过实操案例,我们实现了一个包含用户列表获取和用户注册功能的应用,体验了网络请求的完整流程。在实际开发中,你应该根据项目的需求选择合适的网络请求方案,并遵循最佳实践。
至此,我们已经学习了 Flutter 开发的核心内容,包括基础组件、布局、状态管理、路由与导航、网络请求等。这些内容构成了 Flutter 开发的基础,掌握好这些知识,你就可以开始构建自己的 Flutter 应用了。
