第10章 - 网络请求
嗨,朋友!我是长安。
现代 App 基本都需要从网络获取数据,比如获取新闻列表、用户信息、天气数据等。这一章,我们来学习如何在 Flutter 中进行网络请求。
🌐 HTTP 基础知识
常见的 HTTP 方法
| 方法 | 用途 | 示例 |
|---|---|---|
| GET | 获取数据 | 获取用户列表 |
| POST | 创建数据 | 注册新用户 |
| PUT | 更新数据(全量) | 更新用户信息 |
| PATCH | 更新数据(部分) | 修改用户昵称 |
| DELETE | 删除数据 | 删除用户 |
HTTP 状态码
2xx - 成功
200 OK:请求成功
201 Created:创建成功
4xx - 客户端错误
400 Bad Request:请求参数错误
401 Unauthorized:未授权(需要登录)
403 Forbidden:禁止访问
404 Not Found:资源不存在
5xx - 服务器错误
500 Internal Server Error:服务器内部错误
502 Bad Gateway:网关错误
503 Service Unavailable:服务不可用
📦 使用 http 包
Flutter 官方推荐使用 http 包进行网络请求。
1. 添加依赖
在 pubspec.yaml 中添加:
dependencies:
http: ^1.1.0
然后运行 flutter pub get。
2. 基本 GET 请求
import 'dart:convert';
import 'package:http/http.dart' as http;
Future<void> fetchData() async {
final response = await http.get(
Uri.parse('https://jsonplaceholder.typicode.com/posts/1'),
);
if (response.statusCode == 200) {
// 请求成功,解析 JSON
final data = json.decode(response.body);
print('标题: ${data['title']}');
} else {
// 请求失败
print('请求失败: ${response.statusCode}');
}
}
3. GET 请求带参数
Future<void> searchUsers(String keyword) async {
// 方式一:手动拼接
final url = 'https://api.example.com/search?q=$keyword';
// 方式二:使用 Uri(推荐,自动处理特殊字符)
final uri = Uri.https('api.example.com', '/search', {
'q': keyword,
'page': '1',
'limit': '20',
});
final response = await http.get(uri);
// 处理响应...
}
4. POST 请求
Future<void> createUser(String name, String email) async {
final response = await http.post(
Uri.parse('https://api.example.com/users'),
headers: {
'Content-Type': 'application/json',
},
body: json.encode({
'name': name,
'email': email,
}),
);
if (response.statusCode == 201) {
final newUser = json.decode(response.body);
print('创建成功,用户ID: ${newUser['id']}');
} else {
print('创建失败: ${response.body}');
}
}
5. 带 Token 的请求
Future<void> fetchUserProfile(String token) async {
final response = await http.get(
Uri.parse('https://api.example.com/profile'),
headers: {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
},
);
if (response.statusCode == 200) {
final profile = json.decode(response.body);
print('用户名: ${profile['name']}');
} else if (response.statusCode == 401) {
print('登录已过期,请重新登录');
}
}
🔄 在 Widget 中使用网络请求
使用 FutureBuilder
FutureBuilder 是处理异步数据最常用的方式:
class PostListPage extends StatefulWidget {
const PostListPage({super.key});
State<PostListPage> createState() => _PostListPageState();
}
class _PostListPageState extends State<PostListPage> {
late Future<List<Post>> _futurePosts;
void initState() {
super.initState();
_futurePosts = fetchPosts();
}
Future<List<Post>> fetchPosts() async {
final response = await http.get(
Uri.parse('https://jsonplaceholder.typicode.com/posts'),
);
if (response.statusCode == 200) {
final List<dynamic> jsonData = json.decode(response.body);
return jsonData.map((json) => Post.fromJson(json)).toList();
} else {
throw Exception('加载失败');
}
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('文章列表')),
body: FutureBuilder<List<Post>>(
future: _futurePosts,
builder: (context, snapshot) {
// 加载中
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
// 加载出错
if (snapshot.hasError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error, size: 60, color: Colors.red),
const SizedBox(height: 16),
Text('错误: ${snapshot.error}'),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
setState(() {
_futurePosts = fetchPosts();
});
},
child: const Text('重试'),
),
],
),
);
}
// 加载成功
final posts = snapshot.data!;
return ListView.builder(
itemCount: posts.length,
itemBuilder: (context, index) {
final post = posts[index];
return ListTile(
title: Text(post.title),
subtitle: Text(
post.body,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
);
},
);
},
),
);
}
}
// 数据模型
class Post {
final int id;
final String title;
final String body;
Post({required this.id, required this.title, required this.body});
factory Post.fromJson(Map<String, dynamic> json) {
return Post(
id: json['id'],
title: json['title'],
body: json['body'],
);
}
}
手动管理状态
有时候你需要更细粒度的控制:
class UserListPage extends StatefulWidget {
const UserListPage({super.key});
State<UserListPage> createState() => _UserListPageState();
}
class _UserListPageState extends State<UserListPage> {
List<User> _users = [];
bool _isLoading = false;
String? _error;
void initState() {
super.initState();
_loadUsers();
}
Future<void> _loadUsers() async {
setState(() {
_isLoading = true;
_error = null;
});
try {
final response = await http.get(
Uri.parse('https://jsonplaceholder.typicode.com/users'),
);
if (response.statusCode == 200) {
final List<dynamic> jsonData = json.decode(response.body);
setState(() {
_users = jsonData.map((json) => User.fromJson(json)).toList();
_isLoading = false;
});
} else {
throw Exception('请求失败: ${response.statusCode}');
}
} catch (e) {
setState(() {
_error = e.toString();
_isLoading = false;
});
}
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('用户列表'),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: _loadUsers,
),
],
),
body: _buildBody(),
);
}
Widget _buildBody() {
if (_isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (_error != null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('错误: $_error'),
ElevatedButton(
onPressed: _loadUsers,
child: const Text('重试'),
),
],
),
);
}
return ListView.builder(
itemCount: _users.length,
itemBuilder: (context, index) {
final user = _users[index];
return ListTile(
leading: CircleAvatar(child: Text(user.name[0])),
title: Text(user.name),
subtitle: Text(user.email),
);
},
);
}
}
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'],
);
}
}
🔧 使用 Dio(推荐)
Dio 是一个更强大的 HTTP 客户端,支持拦截器、文件上传下载、超时等高级功能。
1. 添加依赖
dependencies:
dio: ^5.4.0
2. 基本用法
import 'package:dio/dio.dart';
final dio = Dio();
// GET 请求
Future<void> getUser() async {
final response = await dio.get('https://api.example.com/user/1');
print(response.data);
}
// POST 请求
Future<void> createUser() async {
final response = await dio.post(
'https://api.example.com/users',
data: {
'name': '长安',
'email': 'changan@example.com',
},
);
print(response.data);
}
3. 配置 Dio
class ApiService {
static final Dio _dio = Dio(
BaseOptions(
baseUrl: 'https://api.example.com',
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 10),
headers: {
'Content-Type': 'application/json',
},
),
);
// 添加拦截器
static void init() {
_dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) {
// 请求前处理(如添加 Token)
final token = getToken(); // 从本地获取 Token
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
print('请求: ${options.uri}');
return handler.next(options);
},
onResponse: (response, handler) {
// 响应处理
print('响应: ${response.statusCode}');
return handler.next(response);
},
onError: (error, handler) {
// 错误处理
print('错误: ${error.message}');
if (error.response?.statusCode == 401) {
// Token 过期,跳转登录页
}
return handler.next(error);
},
),
);
}
// GET 请求
static Future<Response> get(String path, {Map<String, dynamic>? params}) {
return _dio.get(path, queryParameters: params);
}
// POST 请求
static Future<Response> post(String path, {dynamic data}) {
return _dio.post(path, data: data);
}
}
4. 错误处理
Future<void> fetchData() async {
try {
final response = await dio.get('https://api.example.com/data');
print(response.data);
} on DioException catch (e) {
switch (e.type) {
case DioExceptionType.connectionTimeout:
print('连接超时');
break;
case DioExceptionType.receiveTimeout:
print('接收超时');
break;
case DioExceptionType.badResponse:
print('服务器错误: ${e.response?.statusCode}');
break;
case DioExceptionType.connectionError:
print('网络连接失败,请检查网络');
break;
default:
print('请求失败: ${e.message}');
}
}
}
5. 文件上传
Future<void> uploadImage(File file) async {
final formData = FormData.fromMap({
'file': await MultipartFile.fromFile(
file.path,
filename: 'avatar.jpg',
),
'description': '用户头像',
});
final response = await dio.post(
'https://api.example.com/upload',
data: formData,
onSendProgress: (sent, total) {
final progress = (sent / total * 100).toStringAsFixed(0);
print('上传进度: $progress%');
},
);
print('上传成功: ${response.data}');
}
🏗️ API 服务封装
实际项目中,建议封装 API 服务:
// lib/services/api_service.dart
import 'package:dio/dio.dart';
class ApiService {
static final Dio _dio = Dio(
BaseOptions(
baseUrl: 'https://api.example.com',
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 10),
),
);
// 用户相关 API
static Future<User> getUser(int id) async {
final response = await _dio.get('/users/$id');
return User.fromJson(response.data);
}
static Future<List<User>> getUsers() async {
final response = await _dio.get('/users');
return (response.data as List)
.map((json) => User.fromJson(json))
.toList();
}
static Future<User> createUser(String name, String email) async {
final response = await _dio.post('/users', data: {
'name': name,
'email': email,
});
return User.fromJson(response.data);
}
static Future<void> deleteUser(int id) async {
await _dio.delete('/users/$id');
}
// 文章相关 API
static Future<List<Post>> getPosts({int page = 1, int limit = 20}) async {
final response = await _dio.get('/posts', queryParameters: {
'page': page,
'limit': limit,
});
return (response.data as List)
.map((json) => Post.fromJson(json))
.toList();
}
}
// 使用
void main() async {
final users = await ApiService.getUsers();
final user = await ApiService.getUser(1);
final newUser = await ApiService.createUser('张三', 'zhangsan@example.com');
}
📱 完整实战示例
import 'package:flutter/material.dart';
import 'package:dio/dio.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: '新闻列表',
theme: ThemeData(primarySwatch: Colors.blue),
home: const NewsListPage(),
);
}
}
// 新闻模型
class News {
final int id;
final String title;
final String summary;
final String imageUrl;
final String date;
News({
required this.id,
required this.title,
required this.summary,
required this.imageUrl,
required this.date,
});
factory News.fromJson(Map<String, dynamic> json) {
return News(
id: json['id'],
title: json['title'],
summary: json['body'].toString().substring(0, 50),
imageUrl: 'https://picsum.photos/200/100?random=${json['id']}',
date: '2024-01-${json['id'].toString().padLeft(2, '0')}',
);
}
}
// 新闻列表页
class NewsListPage extends StatefulWidget {
const NewsListPage({super.key});
State<NewsListPage> createState() => _NewsListPageState();
}
class _NewsListPageState extends State<NewsListPage> {
final Dio _dio = Dio();
List<News> _newsList = [];
bool _isLoading = false;
bool _hasError = false;
void initState() {
super.initState();
_loadNews();
}
Future<void> _loadNews() async {
setState(() {
_isLoading = true;
_hasError = false;
});
try {
final response = await _dio.get(
'https://jsonplaceholder.typicode.com/posts',
queryParameters: {'_limit': 20},
);
setState(() {
_newsList = (response.data as List)
.map((json) => News.fromJson(json))
.toList();
_isLoading = false;
});
} catch (e) {
setState(() {
_hasError = true;
_isLoading = false;
});
}
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('新闻列表'),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: _loadNews,
),
],
),
body: _buildBody(),
);
}
Widget _buildBody() {
if (_isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (_hasError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline, size: 60, color: Colors.grey),
const SizedBox(height: 16),
const Text('加载失败,请重试'),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _loadNews,
child: const Text('重新加载'),
),
],
),
);
}
return RefreshIndicator(
onRefresh: _loadNews,
child: ListView.builder(
itemCount: _newsList.length,
itemBuilder: (context, index) {
final news = _newsList[index];
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: InkWell(
onTap: () {
// 跳转到详情页
},
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
news.imageUrl,
width: 100,
height: 80,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
width: 100,
height: 80,
color: Colors.grey[200],
child: const Icon(Icons.image),
);
},
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
news.title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
news.summary,
style: TextStyle(
color: Colors.grey[600],
fontSize: 14,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
news.date,
style: TextStyle(
color: Colors.grey[400],
fontSize: 12,
),
),
],
),
),
],
),
),
),
);
},
),
);
}
}
📝 小结
这一章我们学习了:
- ✅ HTTP 基础知识(方法、状态码)
- ✅ 使用 http 包进行网络请求
- ✅ 使用 FutureBuilder 处理异步数据
- ✅ 使用 Dio 进行更强大的网络请求
- ✅ 拦截器、错误处理、文件上传
- ✅ API 服务的封装
网络请求最佳实践
- 统一封装 - 把 API 请求封装成服务类
- 错误处理 - 处理网络异常和业务错误
- 加载状态 - 显示加载中、加载失败的状态
- 数据缓存 - 合理缓存数据,减少请求
- 超时设置 - 设置合理的超时时间
💪 练习题
- 使用 http 包请求一个免费 API,在列表中显示数据
- 封装一个 API 服务类,包含 GET 和 POST 方法
- 实现一个带下拉刷新功能的列表页面
🚀 下一步
学会了网络请求后,下一章我们来学习 本地存储,让应用能够持久化数据!
由 编程指南 提供
