10_HTTP网络请求
Flutter HTTP 网络请求和数据处理
Flutter 网络请求,http 包基础用法,dio 生产级封装。
http 包(轻量)
dependencies:
http: ^1.1.0
import 'dart:convert';
import 'package:http/http.dart' as http;
// GET 请求
Future<void> fetchPost() async {
final response = await http.get(
Uri.parse('https://jsonplaceholder.typicode.com/posts/1'),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
print('标题: ${data['title']}');
} else {
print('请求失败: ${response.statusCode}');
}
}
// GET 带查询参数
Future<void> search(String query) async {
final uri = Uri.parse('https://api.example.com/search')
.replace(queryParameters: {'q': query, 'page': '1'});
final response = await http.get(uri);
final data = jsonDecode(response.body);
print(data);
}
// POST 请求
Future<void> createPost() async {
final response = await http.post(
Uri.parse('https://jsonplaceholder.typicode.com/posts'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'title': 'Flutter 教程',
'body': '学习 Flutter 网络请求',
'userId': 1,
}),
);
if (response.statusCode == 201) {
print('创建成功: ${jsonDecode(response.body)}');
}
}
数据模型(Model 类)
// 类似 TypeScript 的 interface + class
class Post {
final int id;
final String title;
final String body;
Post({required this.id, required this.title, required this.body});
// 从 JSON 反序列化(类似 TypeScript 的类型断言)
factory Post.fromJson(Map<String, dynamic> json) => Post(
id: json['id'],
title: json['title'],
body: json['body'],
);
// 序列化为 JSON
Map<String, dynamic> toJson() => {
'id': id,
'title': title,
'body': body,
};
}
// 使用
final response = await http.get(Uri.parse('...'));
final post = Post.fromJson(jsonDecode(response.body));
final posts = (jsonDecode(response.body) as List)
.map((e) => Post.fromJson(e))
.toList();
封装 API Service
class ApiException implements Exception {
final String message;
final int? statusCode;
ApiException(this.message, {this.statusCode});
@override
String toString() => 'ApiException: $message (status: $statusCode)';
}
class ApiService {
static const String _baseUrl = 'https://api.example.com';
static const Duration _timeout = Duration(seconds: 15);
static String? _token;
static void setToken(String token) => _token = token;
static Map<String, String> get _headers => {
'Content-Type': 'application/json',
'Accept': 'application/json',
if (_token != null) 'Authorization': 'Bearer $_token',
};
static Future<dynamic> get(String path, {Map<String, String>? params}) async {
final uri = Uri.parse('$_baseUrl$path').replace(queryParameters: params);
try {
final response = await http.get(uri, headers: _headers).timeout(_timeout);
return _handle(response);
} on TimeoutException {
throw ApiException('请求超时');
} on http.ClientException catch (e) {
throw ApiException('网络连接失败: $e');
}
}
static Future<dynamic> post(String path, Map<String, dynamic> body) async {
try {
final response = await http.post(
Uri.parse('$_baseUrl$path'),
headers: _headers,
body: jsonEncode(body),
).timeout(_timeout);
return _handle(response);
} on TimeoutException {
throw ApiException('请求超时');
} on http.ClientException catch (e) {
throw ApiException('网络连接失败: $e');
}
}
static dynamic _handle(http.Response res) {
final body = res.body.isEmpty ? '{}' : res.body;
switch (res.statusCode) {
case >= 200 && < 300: return jsonDecode(body);
case 401: throw ApiException('未登录或登录过期', statusCode: 401);
case 403: throw ApiException('没有权限', statusCode: 403);
case 404: throw ApiException('资源不存在', statusCode: 404);
case >= 500: throw ApiException('服务器错误', statusCode: res.statusCode);
default: throw ApiException('请求失败', statusCode: res.statusCode);
}
}
}
在组件中使用(加载/错误状态)
class PostsPage extends StatefulWidget {
const PostsPage({super.key});
@override
State<PostsPage> createState() => _PostsPageState();
}
class _PostsPageState extends State<PostsPage> {
List<Post> _posts = [];
bool _loading = false;
String? _error;
@override
void initState() {
super.initState();
_load();
}
Future<void> _load() async {
setState(() { _loading = true; _error = null; });
try {
final data = await ApiService.get('/posts') as List;
if (mounted) {
setState(() {
_posts = data.map((e) => Post.fromJson(e)).toList();
_loading = false;
});
}
} on ApiException catch (e) {
if (mounted) setState(() { _error = e.message; _loading = false; });
}
}
@override
Widget build(BuildContext context) {
if (_loading) return const Center(child: CircularProgressIndicator());
if (_error != null) return Center(
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
Text(_error!, style: const TextStyle(color: Colors.red)),
const SizedBox(height: 16),
ElevatedButton(onPressed: _load, child: const Text('重试')),
]),
);
return RefreshIndicator(
onRefresh: _load,
child: ListView.builder(
itemCount: _posts.length,
itemBuilder: (ctx, i) => ListTile(
title: Text(_posts[i].title),
subtitle: Text(_posts[i].body, maxLines: 2, overflow: TextOverflow.ellipsis),
),
),
);
}
}
dio 包(生产环境推荐)
dio 对比 http 包就像 axios 对比原生 fetch,功能更完整:
dependencies:
dio: ^5.4.0
import 'package:dio/dio.dart';
class DioClient {
static final Dio _dio = Dio(BaseOptions(
baseUrl: 'https://api.example.com',
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 15),
headers: {'Accept': 'application/json'},
));
// 添加拦截器(类似 axios.interceptors)
static void init({String? token}) {
_dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) {
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
handler.next(options);
},
onResponse: (response, handler) {
handler.next(response);
},
onError: (DioException e, handler) {
if (e.response?.statusCode == 401) {
// Token 过期,跳转登录页
}
handler.next(e);
},
),
);
// 日志拦截器(开发时开启)
_dio.interceptors.add(LogInterceptor(responseBody: true));
}
static Future<T> get<T>(String path, {Map<String, dynamic>? params}) async {
final response = await _dio.get<T>(path, queryParameters: params);
return response.data!;
}
static Future<T> post<T>(String path, dynamic data) async {
final response = await _dio.post<T>(path, data: data);
return response.data!;
}
// 取消请求(类似 axios CancelToken)
static CancelToken newCancelToken() => CancelToken();
// 文件上传
static Future<void> uploadFile(String path, String filePath) async {
final formData = FormData.fromMap({
'file': await MultipartFile.fromFile(filePath),
});
await _dio.post(path, data: formData);
}
}
FutureBuilder(异步 UI 构建)
Flutter 提供 FutureBuilder 用于在 Widget 中处理异步数据,类似 Vue 的 <Suspense>:
FutureBuilder<List<Post>>(
future: ApiService.get('/posts').then((data) =>
(data as List).map((e) => Post.fromJson(e)).toList()),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(child: Text('错误: ${snapshot.error}'));
}
final posts = snapshot.data!;
return ListView.builder(
itemCount: posts.length,
itemBuilder: (ctx, i) => ListTile(title: Text(posts[i].title)),
);
},
)
注意:
FutureBuilder适合一次性请求。如需刷新,推荐用StatefulWidget+initState的方式手动控制,更灵活。