在现代移动应用开发中,几乎所有应用都需要与后端服务器进行数据交互,获取远程数据并展示给用户。Flutter 提供了多种方式来处理网络请求和数据解析,本节课将详细介绍如何在 Flutter 中进行网络请求、处理响应数据以及解析 JSON 格式的数据。
一、HTTP 库选择:dio 安装与基本使用
Flutter 官方提供了 http
包用于网络请求,但在实际开发中,dio
库因其更强大的功能和更简洁的 API 而被广泛使用。dio
是一个强大的 Dart HTTP 客户端,支持拦截器、FormData、请求取消、超时设置等高级功能。
1. 安装 dio
在 pubspec.yaml
文件中添加依赖:
yaml
dependencies:
flutter:
sdk: flutter
dio: ^5.9.0 # 使用最新版本
运行 flutter pub get
安装依赖。
2. dio 基本使用
首先导入 dio 包:
dart
import 'package:dio/dio.dart';
创建 dio 实例:
dart
// 创建默认实例
Dio dio = Dio();
// 也可以通过 BaseOptions 配置实例
BaseOptions options = BaseOptions(
baseUrl: 'https://api.example.com',
connectTimeout: const Duration(milliseconds: 5000),
receiveTimeout: const Duration(milliseconds: 3000),
headers: {
'Content-Type': 'application/json',
},
);
Dio dio = Dio(options);
二、发起 GET/POST 请求与参数处理
1. 发起 GET 请求
GET 请求通常用于从服务器获取数据:
dart
// 简单的 GET 请求
Future<void> fetchData() async {
try {
Response response = await dio.get('/users');
print('Response data: ${response.data}');
print('Status code: ${response.statusCode}');
} catch (e) {
print('Error: $e');
}
}
// 带查询参数的 GET 请求
Future<void> fetchUserData() async {
try {
// 方式一:直接在 URL 中添加参数
Response response1 = await dio.get('/users?userId=123&name=John');
// 方式二:使用 queryParameters
Response response2 = await dio.get(
'/users',
queryParameters: {
'userId': 123,
'name': 'John',
},
);
print('Response data: ${response2.data}');
} catch (e) {
print('Error: $e');
}
}
2. 发起 POST 请求
POST 请求通常用于向服务器提交数据:
dart
// 提交 JSON 数据
Future<void> submitData() async {
try {
Response response = await dio.post(
'/users',
data: {'name': 'John Doe', 'email': 'john@example.com', 'age': 30},
);
print('Response data: ${response.data}');
} catch (e) {
print('Error: $e');
}
}
// 提交 FormData(表单数据)
Future<void> uploadForm() async {
try {
FormData formData = FormData.fromMap({
'name': 'John Doe',
'avatar': await MultipartFile.fromFile(
'/path/to/avatar.jpg',
filename: 'avatar.jpg',
),
'hobbies': ['reading', 'sports'],
});
Response response = await dio.post('/user/profile', data: formData);
print('Response data: ${response.data}');
} catch (e) {
print('Error: $e');
}
}
3. 自定义请求头
可以为单个请求设置自定义请求头:
dart
Future<void> fetchWithHeaders() async {
try {
Response response = await dio.get(
'/protected/data',
options: Options(
headers: {
'Authorization': 'Bearer your_token_here',
'Custom-Header': 'custom_value',
},
),
);
print('Response data: ${response.data}');
} catch (e) {
print('Error: $e');
}
}
4. 处理请求超时
可以为单个请求设置超时时间:
dart
Future<void> fetchWithTimeout() async {
try {
Response response = await dio.get(
'/slow/endpoint',
options: Options(
sendTimeout: const Duration(seconds: 2),
receiveTimeout: const Duration(seconds: 5),
),
);
print('Response data: ${response.data}');
} on DioException catch (e) {
if (e.type == DioExceptionType.connectionTimeout) {
print('Connection timeout');
} else if (e.type == DioExceptionType.receiveTimeout) {
print('Receive timeout');
} else {
print('Other error: $e');
}
}
}
三、JSON 数据解析
服务器返回的数据通常是 JSON 格式,Flutter 提供了多种方式来解析 JSON 数据。
1. 手动解析 JSON
Dart 内置了 dart:convert
库,可以手动解析 JSON 数据:
dart
import 'dart:convert';
// 假设服务器返回的 JSON 数据如下:
// {
// "id": 1,
// "name": "John Doe",
// "email": "john@example.com",
// "age": 30,
// "hobbies": ["reading", "sports"]
// }
// 创建模型类
class User {
final int id;
final String name;
final String email;
final int age;
final List<String> hobbies;
User({
required this.id,
required this.name,
required this.email,
required this.age,
required this.hobbies,
});
// 从 JSON 映射创建 User 实例
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'],
name: json['name'],
email: json['email'],
age: json['age'],
hobbies: List<String>.from(json['hobbies']),
);
}
// 转换为 JSON 映射
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'email': email,
'age': age,
'hobbies': hobbies,
};
}
}
// 解析 JSON 数据
Future<void> parseUserJson() async {
try {
Response response = await dio.get('/users/1');
// 将 JSON 字符串转换为 Map
Map<String, dynamic> jsonData = response.data;
// 转换为 User 对象
User user = User.fromJson(jsonData);
print('User name: ${user.name}');
print('User email: ${user.email}');
} catch (e) {
print('Error: $e');
}
}
// 解析 JSON 数组
Future<void> parseUsersJson() async {
try {
Response response = await dio.get('/users');
// 将 JSON 数组转换为 List
List<dynamic> jsonList = response.data;
// 转换为 User 对象列表
List<User> users = jsonList.map((json) => User.fromJson(json)).toList();
print('Number of users: ${users.length}');
print('First user: ${users[0].name}');
} catch (e) {
print('Error: $e');
}
}
手动解析的优点是简单直接,不需要额外依赖,但缺点是当 JSON 结构复杂或字段较多时,编写解析代码繁琐且容易出错。
2. 使用 json_serializable 自动生成代码
json_serializable
是一个自动化的源代码生成器,可以为 JSON 序列化和反序列化生成代码,减少手动编写解析代码的工作量。
安装依赖
在 pubspec.yaml
中添加依赖:
yaml
dependencies:
# ... 其他依赖
json_annotation: ^4.8.1 # 注解包
dev_dependencies:
# ... 其他开发依赖
build_runner: ^2.4.4 # 构建工具
json_serializable: ^6.7.1 # 代码生成器
运行 flutter pub get
安装依赖。
创建模型类
dart
import 'package:json_annotation/json_annotation.dart';
// 生成的代码将在 user.g.dart 文件中
part 'user.g.dart';
@JsonSerializable()
class User {
final int id;
final String name;
// 使用 @JsonKey 注解指定 JSON 字段名与类属性名不同的情况
@JsonKey(name: 'email_address')
final String email;
final int age;
// 忽略该字段,不参与序列化和反序列化
@JsonKey(ignore: true)
final String? token;
final List<String> hobbies;
User({
required this.id,
required this.name,
required this.email,
required this.age,
this.token,
required this.hobbies,
});
// 从 JSON 映射创建 User 实例
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
// 转换为 JSON 映射
Map<String, dynamic> toJson() => _$UserToJson(this);
}
生成代码
在项目根目录运行以下命令生成序列化代码:
bash
flutter pub run build_runner build
如果需要在开发过程中自动生成代码(当模型类变化时),可以使用 watch 模式:
bash
flutter pub run build_runner watch
运行成功后,会生成 user.g.dart
文件,包含自动生成的序列化和反序列化代码。
使用自动生成的代码
dart
Future<void> useGeneratedCode() async {
try {
Response response = await dio.get('/users/1');
// 使用自动生成的 fromJson 方法解析
User user = User.fromJson(response.data);
print('User name: ${user.name}');
print('User email: ${user.email}');
// 序列化示例
Map<String, dynamic> userJson = user.toJson();
print('Serialized user: $userJson');
} catch (e) {
print('Error: $e');
}
}
json_serializable
的优点是减少手动编写解析代码的工作量,提高代码的可靠性和可维护性,特别适合处理复杂的 JSON 结构。
四、网络状态处理
在实际应用中,网络请求通常有几种状态:加载中、成功、失败。我们需要根据不同的状态展示不同的 UI。
1. 创建网络状态管理类
dart
enum NetworkStatus { initial, loading, success, error }
class NetworkResult<T> {
final NetworkStatus status;
final T? data;
final String? errorMessage;
NetworkResult.initial()
: status = NetworkStatus.initial,
data = null,
errorMessage = null;
NetworkResult.loading()
: status = NetworkStatus.loading,
data = null,
errorMessage = null;
NetworkResult.success(this.data)
: status = NetworkStatus.success,
errorMessage = null;
NetworkResult.error(this.errorMessage)
: status = NetworkStatus.error,
data = null;
}
2. 基于状态展示不同 UI
dart
class DataScreen extends StatefulWidget {
const DataScreen({super.key});
@override
State<DataScreen> createState() => _DataScreenState();
}
class _DataScreenState extends State<DataScreen> {
final Dio _dio = Dio();
NetworkResult<List<User>> _result = NetworkResult.initial();
@override
void initState() {
super.initState();
fetchUsers();
}
Future<void> fetchUsers() async {
setState(() {
_result = NetworkResult.loading();
});
try {
Response response = await _dio.get('https://api.example.com/users');
List<User> users = (response.data as List)
.map((json) => User.fromJson(json))
.toList();
setState(() {
_result = NetworkResult.success(users);
});
} catch (e) {
setState(() {
_result = NetworkResult.error(e.toString());
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('User List')),
body: _buildBody(),
);
}
Widget _buildBody() {
switch (_result.status) {
case NetworkStatus.initial:
return const Center(child: Text('Tap to load data'));
case NetworkStatus.loading:
return const Center(child: CircularProgressIndicator());
case NetworkStatus.success:
return _buildUserList(_result.data!);
case NetworkStatus.error:
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Error: ${_result.errorMessage}'),
const SizedBox(height: 16),
ElevatedButton(
onPressed: fetchUsers,
child: const Text('Retry'),
),
],
),
);
}
}
Widget _buildUserList(List<User> users) {
return ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
User user = users[index];
return ListTile(
title: Text(user.name),
subtitle: Text(user.email),
trailing: Text('Age: ${user.age}'),
);
},
);
}
}
五、dio 拦截器
dio 提供了拦截器功能,可以在请求发送前或响应返回后进行一些统一处理,如添加认证 token、处理错误等。
1. 请求拦截器
dart
// 添加请求拦截器
dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) {
// 在请求发送前做一些处理
print('Request: ${options.method} ${options.uri}');
// 添加认证 token
options.headers['Authorization'] = 'Bearer your_token_here';
// 继续发送请求
return handler.next(options);
// 如果想终止请求,可以调用 handler.reject()
// return handler.reject(DioException(requestOptions: options, type: DioExceptionType.cancel));
},
),
);
2. 响应拦截器
dart
// 添加响应拦截器
dio.interceptors.add(
InterceptorsWrapper(
onResponse: (response, handler) {
// 在响应返回后做一些处理
print('Response: ${response.statusCode} ${response.data}');
// 继续处理响应
return handler.next(response);
},
),
);
3. 错误拦截器
dart
// 添加错误拦截器
dio.interceptors.add(
InterceptorsWrapper(
onError: (DioException e, handler) {
// 处理错误
print('Error: ${e.message}');
// 统一处理 401 未授权错误
if (e.response?.statusCode == 401) {
// 可以在这里跳转到登录页面
print('Unauthorized, redirecting to login');
}
// 继续处理错误
return handler.next(e);
// 如果想掩盖错误,可以返回一个成功的响应
// return handler.resolve(Response(requestOptions: e.requestOptions, data: {}));
},
),
);
4. 日志拦截器
dio 提供了一个内置的日志拦截器,方便调试:
dart
import 'package:dio/io.dart';
dio.interceptors.add(LogInterceptor(
request: true, // 打印请求信息
requestHeader: true, // 打印请求头
requestBody: true, // 打印请求体
responseHeader: true, // 打印响应头
responseBody: true, // 打印响应体
error: true, // 打印错误信息
logPrint: (object) {
print('Dio Log: $object');
},
));
六、实例:请求开源 API 展示新闻列表
下面我们将实现一个完整的示例,使用公开的新闻 API 获取新闻列表并展示。
1. 创建新闻模型类
dart
import 'package:json_annotation/json_annotation.dart';
part 'news.g.dart';
@JsonSerializable()
class NewsArticle {
@JsonKey(name: 'source')
final NewsSource source;
@JsonKey(name: 'author')
final String? author;
@JsonKey(name: 'title')
final String title;
@JsonKey(name: 'description')
final String? description;
@JsonKey(name: 'url')
final String url;
@JsonKey(name: 'urlToImage')
final String? urlToImage;
@JsonKey(name: 'publishedAt')
final String publishedAt;
@JsonKey(name: 'content')
final String? content;
NewsArticle({
required this.source,
this.author,
required this.title,
this.description,
required this.url,
this.urlToImage,
required this.publishedAt,
this.content,
});
factory NewsArticle.fromJson(Map<String, dynamic> json) =>
_$NewsArticleFromJson(json);
Map<String, dynamic> toJson() => _$NewsArticleToJson(this);
}
@JsonSerializable()
class NewsSource {
@JsonKey(name: 'id')
final String? id;
@JsonKey(name: 'name')
final String name;
NewsSource({
this.id,
required this.name,
});
factory NewsSource.fromJson(Map<String, dynamic> json) =>
_$NewsSourceFromJson(json);
Map<String, dynamic> toJson() => _$NewsSourceToJson(this);
}
@JsonSerializable()
class NewsResponse {
@JsonKey(name: 'status')
final String status;
@JsonKey(name: 'totalResults')
final int totalResults;
@JsonKey(name: 'articles')
final List<NewsArticle> articles;
NewsResponse({
required this.status,
required this.totalResults,
required this.articles,
});
factory NewsResponse.fromJson(Map<String, dynamic> json) =>
_$NewsResponseFromJson(json);
Map<String, dynamic> toJson() => _$NewsResponseToJson(this);
}
运行代码生成命令:
bash
flutter pub run build_runner build
2. 创建新闻服务类
dart
import 'package:dio/dio.dart';
class NewsService {
final Dio _dio = Dio();
final String _apiKey = 'your_news_api_key'; // 替换为你的 API Key
final String _baseUrl = 'https://newsapi.org/v2';
NewsService() {
// 配置 dio
_dio.options.baseUrl = _baseUrl;
_dio.options.connectTimeout = const Duration(seconds: 5);
_dio.options.receiveTimeout = const Duration(seconds: 3);
// 添加日志拦截器
_dio.interceptors.add(LogInterceptor(responseBody: true));
}
// 获取头条新闻
Future<NewsResponse> getTopHeadlines({String country = 'us'}) async {
try {
Response response = await _dio.get(
'/top-headlines',
queryParameters: {
'country': country,
'apiKey': _apiKey,
},
);
return NewsResponse.fromJson(response.data);
} on DioException catch (e) {
print('News API error: ${e.message}');
throw Exception('Failed to fetch news: ${e.message}');
}
}
// 搜索新闻
Future<NewsResponse> searchNews(String query) async {
try {
Response response = await _dio.get(
'/everything',
queryParameters: {
'q': query,
'apiKey': _apiKey,
},
);
return NewsResponse.fromJson(response.data);
} on DioException catch (e) {
print('News API error: ${e.message}');
throw Exception('Failed to search news: ${e.message}');
}
}
}
注意:需要在 News API 网站注册获取 API Key。
3. 实现新闻列表页面
dart
class NewsListScreen extends StatefulWidget {
const NewsListScreen({super.key});
@override
State<NewsListScreen> createState() => _NewsListScreenState();
}
class _NewsListScreenState extends State<NewsListScreen> {
final NewsService _newsService = NewsService();
NetworkResult<List<NewsArticle>> _newsResult = NetworkResult.initial();
final TextEditingController _searchController = TextEditingController();
@override
void initState() {
super.initState();
_fetchTopHeadlines();
}
Future<void> _fetchTopHeadlines() async {
setState(() {
_newsResult = NetworkResult.loading();
});
try {
NewsResponse response = await _newsService.getTopHeadlines();
setState(() {
_newsResult = NetworkResult.success(response.articles);
});
} catch (e) {
setState(() {
_newsResult = NetworkResult.error(e.toString());
});
}
}
Future<void> _searchNews() async {
String query = _searchController.text.trim();
if (query.isEmpty) return;
setState(() {
_newsResult = NetworkResult.loading();
});
try {
NewsResponse response = await _newsService.searchNews(query);
setState(() {
_newsResult = NetworkResult.success(response.articles);
});
} catch (e) {
setState(() {
_newsResult = NetworkResult.error(e.toString());
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Latest News'),
),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: 'Search news...',
suffixIcon: IconButton(
icon: const Icon(Icons.search),
onPressed: _searchNews,
),
border: const OutlineInputBorder(),
),
onSubmitted: (value) => _searchNews(),
),
),
Expanded(
child: _buildNewsContent(),
),
],
),
);
}
Widget _buildNewsContent() {
switch (_newsResult.status) {
case NetworkStatus.initial:
return const Center(child: Text('Loading news...'));
case NetworkStatus.loading:
return const Center(child: CircularProgressIndicator());
case NetworkStatus.success:
return _buildNewsList(_newsResult.data!);
case NetworkStatus.error:
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Error: ${_newsResult.errorMessage}'),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _fetchTopHeadlines,
child: const Text('Retry'),
),
],
),
);
}
}
Widget _buildNewsList(List<NewsArticle> articles) {
if (articles.isEmpty) {
return const Center(child: Text('No news found'));
}
return ListView.builder(
itemCount: articles.length,
itemBuilder: (context, index) {
NewsArticle article = articles[index];
return Card(
margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
child: Column(
children: [
if (article.urlToImage != null)
Image.network(
article.urlToImage!,
height: 180,
width: double.infinity,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
height: 180,
color: Colors.grey[200],
child: const Center(child: Icon(Icons.image_not_supported)),
);
},
),
Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
article.source.name,
style: TextStyle(
color: Colors.grey[600],
fontSize: 12,
),
),
const SizedBox(height: 8),
Text(
article.title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
if (article.description != null)
Text(
article.description!,
style: const TextStyle(fontSize: 14),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 8),
Text(
_formatDate(article.publishedAt),
style: TextStyle(
color: Colors.grey[600],
fontSize: 12,
),
),
],
),
),
],
),
);
},
);
}
String _formatDate(String dateString) {
DateTime date = DateTime.parse(dateString);
return DateFormat.yMMMd().add_jm().format(date);
}
注意:需要添加
intl
依赖来格式化日期,在pubspec.yaml
中添加intl: ^0.18.1
并运行flutter pub get
。
4. 配置网络权限
对于 Android,需要在 android/app/src/main/AndroidManifest.xml
中添加网络权限:
xml
<uses-permission android:name="android.permission.INTERNET" />
对于 iOS,需要在 ios/Runner/Info.plist
中添加:
xml
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
七、网络请求最佳实践
- 封装网络层:将网络请求逻辑封装在专门的服务类中,与 UI 层分离,提高代码复用性和可维护性。
- 统一错误处理:使用拦截器统一处理网络错误,如超时、无网络、认证失败等。
- 合理管理请求状态:清晰展示加载中、成功、失败等状态,提供良好的用户体验。
- 数据缓存:对于不经常变化的数据,可以实现本地缓存,减少网络请求,提高应用性能。
- 请求取消:在页面销毁时取消未完成的网络请求,避免内存泄漏和不必要的资源消耗。
- 图片处理 :使用
cached_network_image
等库处理网络图片,实现缓存和占位图功能。 - 避免在 UI 线程处理复杂任务:确保网络请求在异步线程执行,避免阻塞 UI。
- 添加日志:在开发环境添加详细的网络日志,方便调试;在生产环境关闭或简化日志。