在現(xiàn)代移動應(yīng)用開發(fā)中,幾乎所有應(yīng)用都需要與后端服務(wù)器進(jìn)行數(shù)據(jù)交互,獲取遠(yuǎn)程數(shù)據(jù)并展示給用戶。Flutter 提供了多種方式來處理網(wǎng)絡(luò)請求和數(shù)據(jù)解析,本節(jié)課將詳細(xì)介紹如何在 Flutter 中進(jìn)行網(wǎng)絡(luò)請求、處理響應(yīng)數(shù)據(jù)以及解析 JSON 格式的數(shù)據(jù)。
一、HTTP 庫選擇:dio 安裝與基本使用
Flutter 官方提供了 http 包用于網(wǎng)絡(luò)請求,但在實(shí)際開發(fā)中,dio 庫因其更強(qiáng)大的功能和更簡潔的 API 而被廣泛使用。dio 是一個強(qiáng)大的 Dart HTTP 客戶端,支持?jǐn)r截器、FormData、請求取消、超時設(shè)置等高級功能。
1. 安裝 dio
在 pubspec.yaml 文件中添加依賴:
dependencies:
flutter:
sdk: flutter
dio: ^5.9.0 # 使用最新版本
運(yùn)行 flutter pub get 安裝依賴。
2. dio 基本使用
首先導(dǎo)入 dio 包:
import 'package:dio/dio.dart';
創(chuàng)建 dio 實(shí)例:
// 創(chuàng)建默認(rèn)實(shí)例
Dio dio = Dio();
// 也可以通過 BaseOptions 配置實(shí)例
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);
二、發(fā)起 GET/POST 請求與參數(shù)處理
1. 發(fā)起 GET 請求
GET 請求通常用于從服務(wù)器獲取數(shù)據(jù):
// 簡單的 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');
}
}
// 帶查詢參數(shù)的 GET 請求
Future<void> fetchUserData() async {
try {
// 方式一:直接在 URL 中添加參數(shù)
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. 發(fā)起 POST 請求
POST 請求通常用于向服務(wù)器提交數(shù)據(jù):
// 提交 JSON 數(shù)據(jù)
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(表單數(shù)據(jù))
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. 自定義請求頭
可以為單個請求設(shè)置自定義請求頭:
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. 處理請求超時
可以為單個請求設(shè)置超時時間:
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 數(shù)據(jù)解析
服務(wù)器返回的數(shù)據(jù)通常是 JSON 格式,F(xiàn)lutter 提供了多種方式來解析 JSON 數(shù)據(jù)。
1. 手動解析 JSON
Dart 內(nèi)置了 dart:convert 庫,可以手動解析 JSON 數(shù)據(jù):
import 'dart:convert';
// 假設(shè)服務(wù)器返回的 JSON 數(shù)據(jù)如下:
// {
// "id": 1,
// "name": "John Doe",
// "email": "john@example.com",
// "age": 30,
// "hobbies": ["reading", "sports"]
// }
// 創(chuàng)建模型類
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 映射創(chuàng)建 User 實(shí)例
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']),
);
}
// 轉(zhuǎn)換為 JSON 映射
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'email': email,
'age': age,
'hobbies': hobbies,
};
}
}
// 解析 JSON 數(shù)據(jù)
Future<void> parseUserJson() async {
try {
Response response = await dio.get('/users/1');
// 將 JSON 字符串轉(zhuǎn)換為 Map
Map<String, dynamic> jsonData = response.data;
// 轉(zhuǎn)換為 User 對象
User user = User.fromJson(jsonData);
print('User name: ${user.name}');
print('User email: ${user.email}');
} catch (e) {
print('Error: $e');
}
}
// 解析 JSON 數(shù)組
Future<void> parseUsersJson() async {
try {
Response response = await dio.get('/users');
// 將 JSON 數(shù)組轉(zhuǎn)換為 List
List<dynamic> jsonList = response.data;
// 轉(zhuǎn)換為 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');
}
}
手動解析的優(yōu)點(diǎn)是簡單直接,不需要額外依賴,但缺點(diǎn)是當(dāng) JSON 結(jié)構(gòu)復(fù)雜或字段較多時,編寫解析代碼繁瑣且容易出錯。
2. 使用 json_serializable 自動生成代碼
json_serializable 是一個自動化的源代碼生成器,可以為 JSON 序列化和反序列化生成代碼,減少手動編寫解析代碼的工作量。
安裝依賴
在 pubspec.yaml 中添加依賴:
dependencies:
# ... 其他依賴
json_annotation: ^4.8.1 # 注解包
dev_dependencies:
# ... 其他開發(fā)依賴
build_runner: ^2.4.4 # 構(gòu)建工具
json_serializable: ^6.7.1 # 代碼生成器
運(yùn)行 flutter pub get 安裝依賴。
創(chuàng)建模型類
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 映射創(chuàng)建 User 實(shí)例
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
// 轉(zhuǎn)換為 JSON 映射
Map<String, dynamic> toJson() => _$UserToJson(this);
}
生成代碼
在項(xiàng)目根目錄運(yùn)行以下命令生成序列化代碼:
flutter pub run build_runner build
如果需要在開發(fā)過程中自動生成代碼(當(dāng)模型類變化時),可以使用 watch 模式:
flutter pub run build_runner watch
運(yùn)行成功后,會生成 user.g.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 的優(yōu)點(diǎn)是減少手動編寫解析代碼的工作量,提高代碼的可靠性和可維護(hù)性,特別適合處理復(fù)雜的 JSON 結(jié)構(gòu)。
四、網(wǎng)絡(luò)狀態(tài)處理
在實(shí)際應(yīng)用中,網(wǎng)絡(luò)請求通常有幾種狀態(tài):加載中、成功、失敗。我們需要根據(jù)不同的狀態(tài)展示不同的 UI。
1. 創(chuàng)建網(wǎng)絡(luò)狀態(tài)管理類
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. 基于狀態(tài)展示不同 UI
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 提供了攔截器功能,可以在請求發(fā)送前或響應(yīng)返回后進(jìn)行一些統(tǒng)一處理,如添加認(rèn)證 token、處理錯誤等。
1. 請求攔截器
// 添加請求攔截器
dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) {
// 在請求發(fā)送前做一些處理
print('Request: ${options.method} ${options.uri}');
// 添加認(rèn)證 token
options.headers['Authorization'] = 'Bearer your_token_here';
// 繼續(xù)發(fā)送請求
return handler.next(options);
// 如果想終止請求,可以調(diào)用 handler.reject()
// return handler.reject(DioException(requestOptions: options, type: DioExceptionType.cancel));
},
),
);
2. 響應(yīng)攔截器
// 添加響應(yīng)攔截器
dio.interceptors.add(
InterceptorsWrapper(
onResponse: (response, handler) {
// 在響應(yīng)返回后做一些處理
print('Response: ${response.statusCode} ${response.data}');
// 繼續(xù)處理響應(yīng)
return handler.next(response);
},
),
);
3. 錯誤攔截器
// 添加錯誤攔截器
dio.interceptors.add(
InterceptorsWrapper(
onError: (DioException e, handler) {
// 處理錯誤
print('Error: ${e.message}');
// 統(tǒng)一處理 401 未授權(quán)錯誤
if (e.response?.statusCode == 401) {
// 可以在這里跳轉(zhuǎn)到登錄頁面
print('Unauthorized, redirecting to login');
}
// 繼續(xù)處理錯誤
return handler.next(e);
// 如果想掩蓋錯誤,可以返回一個成功的響應(yīng)
// return handler.resolve(Response(requestOptions: e.requestOptions, data: {}));
},
),
);
4. 日志攔截器
dio 提供了一個內(nèi)置的日志攔截器,方便調(diào)試:
import 'package:dio/io.dart';
dio.interceptors.add(LogInterceptor(
request: true, // 打印請求信息
requestHeader: true, // 打印請求頭
requestBody: true, // 打印請求體
responseHeader: true, // 打印響應(yīng)頭
responseBody: true, // 打印響應(yīng)體
error: true, // 打印錯誤信息
logPrint: (object) {
print('Dio Log: $object');
},
));
六、實(shí)例:請求開源 API 展示新聞列表
下面我們將實(shí)現(xiàn)一個完整的示例,使用公開的新聞 API 獲取新聞列表并展示。
1. 創(chuàng)建新聞模型類
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);
}
運(yùn)行代碼生成命令:
flutter pub run build_runner build
2. 創(chuàng)建新聞服務(wù)類
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 網(wǎng)站注冊獲取 API Key。
3. 實(shí)現(xiàn)新聞列表頁面
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并運(yùn)行flutter pub get。
4. 配置網(wǎng)絡(luò)權(quán)限
對于 Android,需要在 android/app/src/main/AndroidManifest.xml 中添加網(wǎng)絡(luò)權(quán)限:
<uses-permission android:name="android.permission.INTERNET" />
對于 iOS,需要在 ios/Runner/Info.plist 中添加:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
七、網(wǎng)絡(luò)請求最佳實(shí)踐
- 封裝網(wǎng)絡(luò)層:將網(wǎng)絡(luò)請求邏輯封裝在專門的服務(wù)類中,與 UI 層分離,提高代碼復(fù)用性和可維護(hù)性。
- 統(tǒng)一錯誤處理:使用攔截器統(tǒng)一處理網(wǎng)絡(luò)錯誤,如超時、無網(wǎng)絡(luò)、認(rèn)證失敗等。
- 合理管理請求狀態(tài):清晰展示加載中、成功、失敗等狀態(tài),提供良好的用戶體驗(yàn)。
- 數(shù)據(jù)緩存:對于不經(jīng)常變化的數(shù)據(jù),可以實(shí)現(xiàn)本地緩存,減少網(wǎng)絡(luò)請求,提高應(yīng)用性能。
- 請求取消:在頁面銷毀時取消未完成的網(wǎng)絡(luò)請求,避免內(nèi)存泄漏和不必要的資源消耗。
-
圖片處理:使用
cached_network_image等庫處理網(wǎng)絡(luò)圖片,實(shí)現(xiàn)緩存和占位圖功能。 - 避免在 UI 線程處理復(fù)雜任務(wù):確保網(wǎng)絡(luò)請求在異步線程執(zhí)行,避免阻塞 UI。
- 添加日志:在開發(fā)環(huán)境添加詳細(xì)的網(wǎng)絡(luò)日志,方便調(diào)試;在生產(chǎn)環(huán)境關(guān)閉或簡化日志。