在Flutter应用开发中,错误处理是构建稳定应用的关键环节。与传统的Web开发不同,移动端应用面临着更复杂的运行环境和更严格的用户体验要求。一个未处理的异常可能导致整个应用崩溃,直接影响用户留存率和应用评分。
Flutter的错误处理体系主要包含三个层次:
移动应用运行在用户设备上,无法像Web应用那样随时热更新修复问题。根据统计,应用崩溃率每增加1%,用户留存率可能下降2-3%。良好的错误处理能:
Dart中的异常分为两大类:
dart复制// 典型Exception示例
class NetworkException implements Exception {
final String message;
NetworkException(this.message);
}
// 典型Error示例
class InvalidStateError extends Error {
@override
String toString() => 'Invalid application state';
}
关键区别:
创建业务异常时应考虑:
dart复制class PaymentException implements Exception {
final String code;
final String message;
final DateTime timestamp;
PaymentException(this.code, this.message)
: timestamp = DateTime.now();
// 添加toJson方法便于序列化
Map<String, dynamic> toJson() => {
'code': code,
'message': message,
'timestamp': timestamp.toIso8601String()
};
@override
String toString() => '[$code] $message (${timestamp.toLocal()})';
}
基本结构:
dart复制try {
// 可能抛出异常的代码
} on SpecificException catch (e) {
// 处理特定异常
} catch (e, s) {
// 捕获所有异常,s是堆栈信息
} finally {
// 无论是否异常都会执行
}
常见错误:
多类型捕获示例:
dart复制try {
await processOrder();
} on NetworkException catch (e) {
showToast('网络异常: ${e.message}');
} on DatabaseException catch (e) {
logDatabaseError(e);
retryDatabaseOperation();
} on FormatException {
validateInputFormat();
} catch (e, s) {
reportCrash(e, s);
rethrow; // 重新抛出未知异常
}
框架层错误捕获:
dart复制void main() {
FlutterError.onError = (details) {
// 开发环境显示错误红屏
if (kDebugMode) FlutterError.presentError(details);
// 生产环境上报错误
CrashReporter.recordError(
details.exception,
details.stack,
reason: details.context?.toString(),
);
};
runApp(MyApp());
}
替换默认错误界面:
dart复制ErrorWidget.builder = (details) => Scaffold(
appBar: AppBar(title: Text('出错了')),
body: ErrorRecoveryView(
error: details.exception,
stackTrace: details.stack,
onRetry: () => reloadApplication(),
),
);
dart复制Future<void> fetchData() async {
try {
final data = await api.getData()
.timeout(Duration(seconds: 10));
process(data);
} on TimeoutException {
showTimeoutDialog();
} on SocketException {
showNetworkError();
}
}
dart复制stream.listen(
(data) => updateUI(data),
onError: (e) => handleError(e),
cancelOnError: false // 保持订阅不自动取消
);
dart复制void main() {
runZonedGuarded(() {
runApp(MyApp());
}, (error, stackTrace) {
// 捕获所有未处理的异常
CrashReporter.recordError(error, stackTrace);
// 重要:防止无限循环
if (!_isCrashReportingError(error)) {
showEmergencyDialog();
}
});
}
dart复制PlatformDispatcher.instance.onError = (error, stack) {
recordPlatformError(error, stack);
return true; // 阻止默认处理
};
上报时应包含:
dart复制void recordError(dynamic error, StackTrace stack, {String? context}) {
final report = ErrorReport(
error: error,
stackTrace: stack,
deviceInfo: await getDeviceInfo(),
user: currentUser?.id,
route: ModalRoute.of(context)?.settings.name,
appState: appState.toJson(),
timestamp: DateTime.now(),
);
await Crashlytics.recordError(report);
await saveLocalCopy(report); // 本地保存副本
}
建议按以下维度分类:
dart复制Future<Profile> loadUserProfile() async {
try {
return await api.fetchProfile();
} catch (e) {
// 网络失败时返回缓存数据
final cached = await cache.getProfile();
if (cached != null) {
return cached;
}
// 无缓存时返回最小可用数据
return Profile.empty();
}
}
好的错误UI应包含:
dart复制class ErrorView extends StatelessWidget {
final VoidCallback onRetry;
final String message;
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.error_outline, size: 48),
SizedBox(height: 16),
Text(message, style: Theme.of(context).textTheme.titleMedium),
SizedBox(height: 24),
FilledButton(
onPressed: onRetry,
child: Text('重试'),
),
TextButton(
onPressed: () => showDetails(context),
child: Text('技术详情'),
),
],
),
);
}
}
注意事项:
dart复制// 使用Isolate处理复杂错误
void handleComplexError(dynamic error) {
Isolate.run(() {
final analysis = analyzeError(error);
CrashReporter.report(analysis);
});
}
确保:
dart复制void dispose() {
_errorController.close(); // 关闭StreamController
_errorSubscriptions.cancelAll(); // 取消所有订阅
}
dart复制testWidgets('测试错误边界', (tester) async {
// 强制触发错误
final error = Exception('测试错误');
when(mockService.fetchData()).thenThrow(error);
await tester.pumpWidget(MyApp());
await tester.pumpAndSettle();
// 验证错误UI显示
expect(find.text('加载失败'), findsOneWidget);
expect(find.byType(RetryButton), findsOneWidget);
});
实用调试方法:
dart复制void recordError(dynamic error, StackTrace stack) {
debugPrint('ERROR: $error');
debugPrintStack(stackTrace: stack);
if (error is DatabaseException) {
debugger(); // 数据库异常时暂停
}
}
dart复制Future<Result> performOperation() async {
try {
return await _complexOperation();
} catch (e, s) {
// 将底层异常转换为业务异常
throw OperationFailed.wrap(e, s);
}
}
class OperationFailed implements Exception {
final String message;
final Object? cause;
final StackTrace? stackTrace;
OperationFailed(this.message, [this.cause, this.stackTrace]);
factory OperationFailed.wrap(Object error, StackTrace stack) {
return OperationFailed(
'操作失败: ${error.toString()}',
error,
stack,
);
}
}
常见恢复策略:
dart复制Future<Data> fetchDataWithRetry({
int maxRetries = 3,
Duration initialDelay = const Duration(seconds: 1),
}) async {
int attempt = 0;
while (true) {
try {
return await fetchData();
} catch (e) {
if (attempt >= maxRetries) rethrow;
await Future.delayed(initialDelay * (1 << attempt));
attempt++;
}
}
}
dart复制final dataProvider = FutureProvider<Data>((ref) async {
try {
return await fetchData();
} catch (e, s) {
ref.read(errorLoggerProvider).record(e, s);
throw DataFetchException('加载失败');
}
});
class DataFetchException implements Exception {
// ...
}
// UI层使用
Consumer(builder: (context, ref, child) {
final asyncData = ref.watch(dataProvider);
return asyncData.when(
loading: () => ProgressIndicator(),
error: (e, _) => ErrorView(e),
data: (d) => DataView(d),
);
});
dart复制class DataBloc extends Bloc<DataEvent, DataState> {
DataBloc() : super(DataInitial()) {
on<FetchData>((event, emit) async {
emit(DataLoading());
try {
final data = await repository.fetchData();
emit(DataLoaded(data));
} catch (e, s) {
emit(DataError(e, s));
}
});
}
}
dart复制// 通过MethodChannel调用原生代码
try {
final result = await platform.invokeMethod('nativeMethod');
} on PlatformException catch (e) {
// 处理平台特定异常
debugPrint('原生代码错误: ${e.code} - ${e.message}');
if (e.code == 'SERVICE_UNAVAILABLE') {
handleServiceUnavailable();
}
}
objectivec复制// iOS端应使用NSError而非异常
FlutterMethodChannel* channel = [FlutterMethodChannel
methodChannelWithName:@"channel"
binaryMessenger:messenger];
[channel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
NSError* error;
id returnValue = [handler handleMethodCall:call error:&error];
if (error) {
result([FlutterError errorWithCode:@"ERROR"
message:error.localizedDescription
details:nil]);
} else {
result(returnValue);
}
}];
可能原因:
解决方案:
dart复制// 错误示例
try {
future.then((_) => throw Error()); // 不会被捕获
} catch (e) {
// 不会执行
}
// 正确写法
try {
await future;
// 或者
future.catchError((e) => handleError(e));
} catch (e) {
// 现在可以捕获
}
确保覆盖所有入口:
dart复制void main() {
// 1. Flutter框架错误
FlutterError.onError = ...;
// 2. Zone全局捕获
runZonedGuarded(() {
// 3. Widget错误边界
ErrorWidget.builder = ...;
// 4. 平台层错误
PlatformDispatcher.instance.onError = ...;
runApp(MyApp());
}, (e, s) => ...);
}
dart复制class ProductService {
Future<Product> loadProduct(String id) async {
try {
// 尝试从网络加载
final product = await _api.getProduct(id);
await _cache.save(product);
return product;
} on NetworkException catch (e) {
// 网络异常尝试读取缓存
final cached = await _cache.get(id);
if (cached != null) {
return cached;
}
throw ProductLoadException('网络不可用且无缓存');
} on FormatException {
// 数据格式错误
await _cache.remove(id); // 清除可能损坏的缓存
throw ProductLoadException('商品数据格式错误');
}
}
}
dart复制class PaymentHandler {
Future<PaymentResult> pay(Order order) async {
PaymentResult result;
try {
result = await _paymentGateway.charge(order);
if (result.isPending) {
result = await _verifyPaymentStatus(result.txId);
}
return result;
} on PaymentException catch (e) {
// 已知支付异常
await _notifyUser(e.userMessage);
await _logPaymentAttempt(order, e);
return PaymentResult.failed(e.code);
} catch (e, s) {
// 未知异常
await _emergencyRollback(order);
await _reportUnknownError(e, s);
return PaymentResult.unknownFailure();
} finally {
await _cleanupPaymentSession();
}
}
}
推荐架构:
code复制表示层
├─ 处理UI相关错误
├─ 显示用户友好提示
└─ 记录视图相关错误
业务逻辑层
├─ 转换底层异常为业务异常
├─ 实现重试逻辑
└─ 决定降级策略
数据层
├─ 处理原始IO异常
├─ 实现缓存回退
└─ 转换数据格式错误
dart复制class ErrorHandlerMiddleware extends Interceptor {
@override
Future onError(
DioException err,
ErrorInterceptorHandler handler,
) async {
if (err.type == DioExceptionType.connectionTimeout) {
// 转换为业务异常
handler.reject(NetworkTimeoutException(err));
} else {
// 其他错误继续传递
handler.next(err);
}
}
}
// 在Dio中使用
final dio = Dio()
..interceptors.add(ErrorHandlerMiddleware());
建议分级:
dart复制class ErrorMonitor {
final Map<ErrorLevel, int> _thresholds = {
ErrorLevel.critical: 1, // 立即报警
ErrorLevel.major: 5, // 每小时超过5次报警
ErrorLevel.minor: 20, // 每天超过20次报警
};
void recordError(ErrorInfo info) {
_updateCounters(info.level);
if (_shouldAlert(info.level)) {
_sendAlert(info);
}
}
bool _shouldAlert(ErrorLevel level) {
return _counters[level] >= _thresholds[level]!;
}
}
dart复制class AppLocalizations {
String get networkError => Intl.message(
'Network unavailable',
name: 'networkError',
desc: 'Network error message',
);
String paymentError(String code) => Intl.message(
'Payment failed: $code',
name: 'paymentError',
args: [code],
);
}
// 使用
throw PaymentException(
'INSUFFICIENT_FUNDS',
AppLocalizations.current.paymentError('INSUFFICIENT_FUNDS'),
);
根据错误类型显示不同提示:
dart复制String getErrorMessage(dynamic error) {
if (error is NetworkException) {
return localizations.networkError;
} else if (error is PaymentException) {
return localizations.paymentError(error.code);
} else {
return localizations.genericError;
}
}
需要注意:
优化方案:
dart复制// 异步上报错误
void recordError(dynamic error) {
// 先收集基本信息
final errorInfo = _collectBasicInfo(error);
// 延迟上报
Future.microtask(() => _sendReport(errorInfo));
}
// 采样率控制
bool _shouldReportError(ErrorType type) {
const samplingRates = {
ErrorType.critical: 1.0, // 100%上报
ErrorType.major: 0.5, // 50%上报
ErrorType.minor: 0.1, // 10%上报
};
return Random().nextDouble() < samplingRates[type]!;
}
dart复制// 生产环境限制堆栈深度
StackTrace getLimitedStackTrace(StackTrace stack) {
if (kReleaseMode) {
return StackTrace.fromString(
stack.toString().split('\n').take(20).join('\n')
);
}
return stack;
}
监控指标:
建议流程:
code复制收集 -> 分类 -> 分析 -> 修复 -> 验证 -> 监控
工具链整合:
需要过滤:
dart复制String sanitizeErrorMessage(String message) {
return message
.replaceAll(RegExp(r'password=[^&]*'), 'password=***')
.replaceAll(RegExp(r'token=\w+'), 'token=***');
}
确保:
dart复制void reportError(ErrorReport report) {
final encrypted = _encrypt(report.toJson());
_sendToSecureEndpoint(encrypted);
}
dart复制test('测试网络异常处理', () async {
// 注入网络异常
when(mockApi.getData()).thenThrow(NetworkException('timeout'));
await tester.tap(find.byKey(Key('loadButton')));
await tester.pumpAndSettle();
expect(find.text('网络不可用'), findsOneWidget);
});
模拟以下场景:
dart复制// 在测试配置中启用混沌模式
void configureChaosMode() {
if (Config.chaosModeEnabled) {
ChaosEngine()
.randomApiFailures(probability: 0.1)
.memoryPressure(interval: Duration(minutes: 5))
.networkLatency(range: Duration(seconds: 1)..Duration(seconds: 10));
}
}
推荐:
集成示例:
dart复制void initCrashReporting() {
FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(true);
FlutterError.onError = (details) {
FirebaseCrashlytics.instance.recordFlutterError(details);
};
}
实用工具:
dart复制final logger = Logger(
printer: PrettyPrinter(),
filter: DevelopmentFilter(),
);
void handleError(dynamic e) {
logger.e('Error occurred', e);
}
建议规则:
markdown复制## [错误类型代码] 错误名称
**描述**:
简要说明错误场景
**可能原因**:
1. 原因A
2. 原因B
**处理建议**:
1. 建议操作A
2. 建议操作B
**相关组件**:
- 组件X
- 组件Y
**示例代码**:
```dart
try {
// 可能抛出此错误的代码
} on ErrorType catch (e) {
// 处理方式
}
日志示例:
[粘贴典型日志片段]
code复制
## 25. 总结与最佳实践
经过对Flutter错误处理体系的全面探讨,我们可以总结出以下核心原则:
1. **防御性编程**:始终假设代码可能失败
2. **明确分类**:区分预期异常和意外错误
3. **优雅降级**:提供备用方案保证基本功能
4. **完整记录**:保留足够的排查信息
5. **及时恢复**:尽可能自动恢复或引导用户恢复
6. **持续改进**:分析错误趋势并优化
在实际项目中,建议建立统一的错误处理框架,将上述原则落实到各个层级。同时要记住,好的错误处理不仅要考虑技术实现,更要关注用户体验 - 当出现问题时,用户应该始终知道发生了什么以及如何解决。