Flutter 网络请求最佳实践:构建可靠的异步应用
Flutter 网络请求最佳实践构建可靠的异步应用引言在现代移动应用开发中网络请求是不可或缺的一部分。Flutter 提供了多种方式来处理网络请求从原生的http包到强大的第三方库如dio。本文将深入探讨 Flutter 网络请求的最佳实践帮助你构建高效、可靠的异步应用。一、选择合适的网络库1.1 原生 http 包import package:http/http.dart as http; Futurevoid 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), )); FutureUser 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 包DioRetrofit易用性基础优秀中等拦截器支持有限完善依赖 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(); FutureUser getUser(String userId) async { try { final response await _apiService.dio.get(/users/$userId); return User.fromJson(response.data); } catch (e) { throw RepositoryException(获取用户失败, cause: e); } } FutureListUser getUsers({int page 1, int limit 10}) async { try { final response await _apiService.dio.get( /users, queryParameters: {page: page, limit: limit}, ); final Listdynamic 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 使用 CancelTokenclass DataService { CancelToken? _cancelToken; Futurevoid 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 StateDataWidget { late DataService _dataService; override void initState() { super.initState(); _dataService DataService(); _loadData(); } Futurevoid _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; Futurevoid 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); } Futuredynamic? 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]; } Futurevoid delete(String key) async { final box await Hive.openBox(_cacheBox); await box.delete(key); } Futurevoid clear() async { final box await Hive.openBox(_cacheBox); await box.clear(); } }5.2 实现缓存优先策略class UserRepository { final CacheService _cacheService; FutureUser 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; ListUser _users []; bool _isLoading false; AppException? _error; int _page 1; bool _hasMore true; ListUser get users _users; bool get isLoading _isLoading; AppException? get error _error; bool get hasMore _hasMore; UserListViewModel({UserRepository? repository}) : _repository repository ?? UserRepository(); Futurevoid 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(); } } Futurevoid refreshUsers() fetchUsers(refresh: true); }7.2 Widget 实现class UserListPage extends StatefulWidget { override _UserListPageState createState() _UserListPageState(); } class _UserListPageState extends StateUserListPage { 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 FutureBuilderUint8List( future: _loadImage(), builder: (context, snapshot) { if (snapshot.hasData) { return Image.memory(snapshot.data!); } return placeholder; }, ); } FutureUint8List _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 Futurevoid saveToken(String token) async { await _storage.write(key: _tokenKey, value: token); } static FutureString? getToken() async { return _storage.read(key: _tokenKey); } static Futurevoid 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 DocumentationRetrofit for DartFlutter Secure StorageHive