Flutter 网络请求最佳实践:构建可靠的异步应用
引言
在现代移动应用开发中,网络请求是不可或缺的一部分。Flutter 提供了多种方式来处理网络请求,从原生的http包到强大的第三方库如dio。本文将深入探讨 Flutter 网络请求的最佳实践,帮助你构建高效、可靠的异步应用。
一、选择合适的网络库
1.1 原生 http 包
import 'package:http/http.dart' as http; Future<void> fetchUserData() async { final response = await http.get( Uri.parse('https://api.example.com/users'), headers: {'Authorization': 'Bearer token'}, ); if (response.statusCode == 200) { // 处理响应 print(response.body); } else { throw Exception('请求失败: ${response.statusCode}'); } }1.2 Dio 库 - 推荐选择
import 'package:dio/dio.dart'; final dio = Dio(BaseOptions( baseUrl: 'https://api.example.com', connectTimeout: const Duration(seconds: 5), receiveTimeout: const Duration(seconds: 3), )); Future<User> fetchUser(String userId) async { try { final response = await dio.get('/users/$userId'); return User.fromJson(response.data); } catch (e) { // 统一错误处理 throw handleError(e); } }1.3 库对比分析
| 特性 | http 包 | Dio | Retrofit |
|---|---|---|---|
| 易用性 | 基础 | 优秀 | 中等 |
| 拦截器支持 | 有限 | 完善 | 依赖 Dio |
| 取消请求 | 支持 | 支持 | 支持 |
| 缓存支持 | 需实现 | 内置 | 需实现 |
| 文件上传 | 基础 | 优秀 | 依赖 Dio |
| 类型安全 | 无 | 无 | 有 |
二、构建网络层架构
2.1 分层架构设计
┌─────────────────────────────────────────────┐ │ UI Layer │ │ (Widgets, State) │ └────────────────────┬────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────┐ │ Repository Layer │ │ (数据获取、缓存、业务逻辑) │ └────────────────────┬────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────┐ │ Service Layer │ │ (网络请求、API 调用) │ └────────────────────┬────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────┐ │ Data Source │ │ (API、本地数据库、缓存) │ └─────────────────────────────────────────────┘2.2 创建 ApiService 单例
class ApiService { static final ApiService _instance = ApiService._internal(); late Dio _dio; factory ApiService() => _instance; ApiService._internal() { _dio = Dio(BaseOptions( baseUrl: 'https://api.example.com', connectTimeout: const Duration(seconds: 10), receiveTimeout: const Duration(seconds: 10), headers: { 'Content-Type': 'application/json', }, )); // 添加拦截器 _setupInterceptors(); } void _setupInterceptors() { // 请求拦截器 _dio.interceptors.add(InterceptorsWrapper( onRequest: (options, handler) { // 添加 token final token = TokenManager.getToken(); if (token != null) { options.headers['Authorization'] = 'Bearer $token'; } return handler.next(options); }, onResponse: (response, handler) { // 统一处理响应 return handler.next(response); }, onError: (error, handler) { // 统一错误处理 return handler.reject(error); }, )); } Dio get dio => _dio; }2.3 创建 Repository 层
class UserRepository { final ApiService _apiService; UserRepository({ApiService? apiService}) : _apiService = apiService ?? ApiService(); Future<User> getUser(String userId) async { try { final response = await _apiService.dio.get('/users/$userId'); return User.fromJson(response.data); } catch (e) { throw RepositoryException('获取用户失败', cause: e); } } Future<List<User>> getUsers({int page = 1, int limit = 10}) async { try { final response = await _apiService.dio.get( '/users', queryParameters: {'page': page, 'limit': limit}, ); final List<dynamic> data = response.data['data']; return data.map((json) => User.fromJson(json)).toList(); } catch (e) { throw RepositoryException('获取用户列表失败', cause: e); } } }三、错误处理策略
3.1 自定义异常类
abstract class AppException implements Exception { final String message; final Exception? cause; AppException(this.message, {this.cause}); @override String toString() => '$message${cause != null ? ': $cause' : ''}'; } class NetworkException extends AppException { NetworkException({String message = '网络连接失败', Exception? cause}) : super(message, cause: cause); } class ServerException extends AppException { final int statusCode; ServerException({ required this.statusCode, String message = '服务器错误', Exception? cause, }) : super(message, cause: cause); } class AuthException extends AppException { AuthException({String message = '认证失败', Exception? cause}) : super(message, cause: cause); } class RepositoryException extends AppException { RepositoryException({required String message, Exception? cause}) : super(message, cause: cause); }3.2 统一错误处理
class ErrorHandler { static AppException handle(dynamic error) { if (error is DioException) { switch (error.type) { case DioExceptionType.connectionTimeout: case DioExceptionType.receiveTimeout: case DioExceptionType.sendTimeout: return NetworkException(message: '请求超时'); case DioExceptionType.connectionError: return NetworkException(message: '网络连接失败'); case DioExceptionType.badResponse: final statusCode = error.response?.statusCode ?? 500; return _handleStatusCode(statusCode, error); case DioExceptionType.cancel: return AppException('请求已取消'); default: return AppException('未知错误'); } } if (error is SocketException) { return NetworkException(message: '网络连接异常'); } if (error is AppException) { return error; } return AppException('未知错误: $error'); } static ServerException _handleStatusCode(int statusCode, DioException error) { final message = error.response?.data?['message'] ?? ''; switch (statusCode) { case 400: return ServerException( statusCode: 400, message: message.isNotEmpty ? message : '请求参数错误', ); case 401: return AuthException(message: message.isNotEmpty ? message : '未授权'); case 403: return ServerException( statusCode: 403, message: message.isNotEmpty ? message : '禁止访问', ); case 404: return ServerException( statusCode: 404, message: message.isNotEmpty ? message : '资源未找到', ); case 500: return ServerException( statusCode: 500, message: message.isNotEmpty ? message : '服务器内部错误', ); default: return ServerException( statusCode: statusCode, message: '请求失败 ($statusCode)', ); } } }四、请求取消机制
4.1 使用 CancelToken
class DataService { CancelToken? _cancelToken; Future<void> fetchData() async { // 取消之前的请求 _cancelToken?.cancel('请求已取消'); _cancelToken = CancelToken(); try { final response = await ApiService().dio.get( '/data', cancelToken: _cancelToken, ); // 处理响应 } on DioException catch (e) { if (e.type == DioExceptionType.cancel) { // 请求被取消,不处理错误 return; } throw ErrorHandler.handle(e); } } void dispose() { _cancelToken?.cancel('组件已销毁'); } }4.2 在 Widget 生命周期中使用
class DataWidget extends StatefulWidget { @override _DataWidgetState createState() => _DataWidgetState(); } class _DataWidgetState extends State<DataWidget> { late DataService _dataService; @override void initState() { super.initState(); _dataService = DataService(); _loadData(); } Future<void> _loadData() async { try { await _dataService.fetchData(); } catch (e) { // 处理错误 } } @override void dispose() { _dataService.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Container(); } }五、请求缓存策略
5.1 使用 Hive 作为缓存
import 'package:hive/hive.dart'; class CacheService { static const String _cacheBox = 'api_cache'; Future<void> save(String key, dynamic data, {Duration? expiresIn}) async { final box = await Hive.openBox(_cacheBox); final cacheData = { 'data': data, 'timestamp': DateTime.now().millisecondsSinceEpoch, 'expiresIn': expiresIn?.inMilliseconds, }; await box.put(key, cacheData); } Future<dynamic?> get(String key) async { final box = await Hive.openBox(_cacheBox); final cacheData = box.get(key); if (cacheData == null) return null; final timestamp = cacheData['timestamp'] as int; final expiresIn = cacheData['expiresIn'] as int?; if (expiresIn != null) { final now = DateTime.now().millisecondsSinceEpoch; if (now - timestamp > expiresIn) { await box.delete(key); return null; } } return cacheData['data']; } Future<void> delete(String key) async { final box = await Hive.openBox(_cacheBox); await box.delete(key); } Future<void> clear() async { final box = await Hive.openBox(_cacheBox); await box.clear(); } }5.2 实现缓存优先策略
class UserRepository { final CacheService _cacheService; Future<User> getUser(String userId) async { // 先尝试从缓存获取 final cachedData = await _cacheService.get('user_$userId'); if (cachedData != null) { return User.fromJson(cachedData); } // 缓存不存在,从网络获取 final response = await ApiService().dio.get('/users/$userId'); final user = User.fromJson(response.data); // 缓存结果,5分钟过期 await _cacheService.save( 'user_$userId', response.data, expiresIn: const Duration(minutes: 5), ); return user; } }六、请求重试机制
6.1 实现指数退避策略
class RetryInterceptor extends Interceptor { final int maxRetries; final Duration initialDelay; RetryInterceptor({ this.maxRetries = 3, this.initialDelay = const Duration(seconds: 1), }); @override void onError(DioException err, ErrorInterceptorHandler handler) { final requestOptions = err.requestOptions; // 检查是否需要重试 if (_shouldRetry(err) && requestOptions.extra['retryCount'] != maxRetries) { final retryCount = requestOptions.extra['retryCount'] ?? 0; requestOptions.extra['retryCount'] = retryCount + 1; // 指数退避延迟 final delay = initialDelay * math.pow(2, retryCount); Future.delayed(delay, () { _retryRequest(requestOptions, handler); }); return; } handler.reject(err); } bool _shouldRetry(DioException err) { // 只对网络错误和服务器错误重试 return err.type == DioExceptionType.connectionError || err.type == DioExceptionType.connectionTimeout || (err.type == DioExceptionType.badResponse && err.response?.statusCode != null && err.response!.statusCode! >= 500); } void _retryRequest( RequestOptions options, ErrorInterceptorHandler handler, ) async { try { final response = await ApiService().dio.fetch(options); handler.resolve(response); } catch (e) { handler.reject(e as DioException); } } }七、实战案例:完整的用户列表页面
7.1 ViewModel 实现
class UserListViewModel extends ChangeNotifier { final UserRepository _repository; List<User> _users = []; bool _isLoading = false; AppException? _error; int _page = 1; bool _hasMore = true; List<User> get users => _users; bool get isLoading => _isLoading; AppException? get error => _error; bool get hasMore => _hasMore; UserListViewModel({UserRepository? repository}) : _repository = repository ?? UserRepository(); Future<void> fetchUsers({bool refresh = false}) async { if (_isLoading) return; if (refresh) { _page = 1; _users.clear(); _hasMore = true; } if (!_hasMore) return; _isLoading = true; _error = null; notifyListeners(); try { final newUsers = await _repository.getUsers(page: _page); if (newUsers.isEmpty) { _hasMore = false; } else { _users.addAll(newUsers); _page++; } } catch (e) { _error = ErrorHandler.handle(e); } finally { _isLoading = false; notifyListeners(); } } Future<void> refreshUsers() => fetchUsers(refresh: true); }7.2 Widget 实现
class UserListPage extends StatefulWidget { @override _UserListPageState createState() => _UserListPageState(); } class _UserListPageState extends State<UserListPage> { late UserListViewModel _viewModel; final _scrollController = ScrollController(); @override void initState() { super.initState(); _viewModel = UserListViewModel(); _viewModel.fetchUsers(); _scrollController.addListener(_onScroll); } void _onScroll() { if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) { _viewModel.fetchUsers(); } } @override void dispose() { _scrollController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('用户列表')), body: RefreshIndicator( onRefresh: _viewModel.refreshUsers, child: _buildContent(), ), ); } Widget _buildContent() { if (_viewModel.error != null) { return _buildError(); } if (_viewModel.users.isEmpty && !_viewModel.isLoading) { return _buildEmptyState(); } return ListView.builder( controller: _scrollController, itemCount: _viewModel.users.length + (_viewModel.hasMore ? 1 : 0), itemBuilder: (context, index) { if (index == _viewModel.users.length) { return _buildLoadingIndicator(); } return _buildUserItem(_viewModel.users[index]); }, ); } Widget _buildError() { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text(_viewModel.error!.message), const SizedBox(height: 16), ElevatedButton( onPressed: _viewModel.refreshUsers, child: const Text('重试'), ), ], ), ); } Widget _buildEmptyState() { return const Center( child: Text('暂无数据'), ); } Widget _buildLoadingIndicator() { return const Center( child: Padding( padding: EdgeInsets.all(16), child: CircularProgressIndicator(), ), ); } Widget _buildUserItem(User user) { return ListTile( leading: CircleAvatar(child: Text(user.name[0])), title: Text(user.name), subtitle: Text(user.email), ); } }八、性能优化建议
8.1 避免重复请求
// 使用 debounce 避免频繁请求 class Debounce { final Duration delay; Timer? _timer; Debounce({this.delay = const Duration(milliseconds: 300)}); void run(VoidCallback action) { _timer?.cancel(); _timer = Timer(delay, action); } void dispose() { _timer?.cancel(); } } // 使用示例 final debounce = Debounce(); void onSearch(String query) { debounce.run(() { fetchSearchResults(query); }); }8.2 使用 HTTP/2 和连接池
final dio = Dio(BaseOptions( baseUrl: 'https://api.example.com', // 启用 HTTP/2 httpClientAdapter: Http2Adapter( ConnectionManager( idleTimeout: const Duration(seconds: 10), // 连接池配置 connectionPool: const ConnectionPoolConfiguration( maxConnections: 5, idleTimeout: Duration(seconds: 30), ), ), ), ));8.3 图片懒加载
class LazyLoadImage extends StatelessWidget { final String imageUrl; final Widget placeholder; const LazyLoadImage({ required this.imageUrl, this.placeholder = const CircularProgressIndicator(), }); @override Widget build(BuildContext context) { return FutureBuilder<Uint8List>( future: _loadImage(), builder: (context, snapshot) { if (snapshot.hasData) { return Image.memory(snapshot.data!); } return placeholder; }, ); } Future<Uint8List> _loadImage() async { final response = await ApiService().dio.get( imageUrl, options: Options(responseType: ResponseType.bytes), ); return response.data; } }九、安全注意事项
9.1 HTTPS 强制使用
final dio = Dio(BaseOptions( baseUrl: 'https://api.example.com', validateStatus: (status) { // 只接受 HTTPS 请求 return status! >= 200 && status < 300; }, ));9.2 Token 安全存储
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; class TokenManager { static const FlutterSecureStorage _storage = FlutterSecureStorage(); static const String _tokenKey = 'auth_token'; static Future<void> saveToken(String token) async { await _storage.write(key: _tokenKey, value: token); } static Future<String?> getToken() async { return _storage.read(key: _tokenKey); } static Future<void> deleteToken() async { await _storage.delete(key: _tokenKey); } }十、总结与展望
10.1 最佳实践总结
- 选择合适的网络库:根据项目需求选择 http、Dio 或 Retrofit
- 分层架构:Repository 层 + Service 层 + Data Source 层
- 统一错误处理:定义自定义异常类,统一处理各类错误
- 请求取消:使用 CancelToken 避免无用请求
- 缓存策略:实现缓存优先,提升用户体验
- 请求重试:实现指数退避策略,提高请求成功率
- 性能优化:使用 debounce、连接池等技术
10.2 未来发展趋势
- HTTP/3 支持:更快的连接建立和多路复用
- GraphQL:更高效的数据获取方式
- WebSocket:实时通信场景的最佳选择
- 离线优先:更好的离线体验
参考资料:
- Dio Documentation
- Retrofit for Dart
- Flutter Secure Storage
- Hive