flutter-error-handling
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseFlutter 에러 처리 — Result<D, E>
Flutter 错误处理 — Result<D, E>
핵심 철학
核心哲学
예상 가능한 실패에는 예외를 던지지 않는다. 대신 타입으로 표현된 를 반환한다. 이렇게 하면 호출자가 실패 케이스를 타입 시스템으로 강제로 다루게 되어, 런타임에 놓치는 UI 에러 경로가 사라진다.
Result예외는 프레임워크/플랫폼이 던진 것을 가장 낮은 레이어(Data)에서 잡아 로 변환하는 용도로만 쓴다. UseCase, ViewModel, Screen 은 더 이상 try/catch 를 보지 않는다.
Result.error(...)对于可预见的失败,不抛出异常。而是返回以类型形式表达的。这样可以强制调用方通过类型系统处理失败场景,消除运行时遗漏的UI错误路径。
Result异常仅用于在最底层(数据层)捕获框架/平台抛出的异常,并转换为。UseCase、ViewModel、Screen层不再需要try/catch。
Result.error(...)기반 타입 (lib/core/domain/error/
)
lib/core/domain/error/基础类型 (lib/core/domain/error/
)
lib/core/domain/error/Error 마커 인터페이스
Error 标记接口
dart
// lib/core/domain/error/error.dart
abstract interface class Error {}모든 커스텀 에러 타입은 이 를 구현한다. 의 는 반드시 이므로 Dart 표준 과 섞이지 않는다.
ErrorResultEextends ErrorExceptiondart
// lib/core/domain/error/error.dart
abstract interface class Error {}所有自定义错误类型都需要实现这个接口。的必须,因此不会与Dart标准混淆。
ErrorResultEextends ErrorExceptionResult<D, E>
Result<D, E>
dart
// lib/core/domain/error/result.dart
import 'package:freezed_annotation/freezed_annotation.dart';
import 'error.dart';
part 'result.freezed.dart';
sealed class Result<D, E extends Error> with _$Result<D, E> {
const factory Result.success(D data) = ResultSuccess;
const factory Result.error(E error) = ResultError;
}중요: 로 선언했기 때문에 에서 모든 케이스를 Dart 컴파일러가 강제한다. 케이스를 빠뜨리면 컴파일 경고가 뜬다.
sealedswitchdart
// lib/core/domain/error/result.dart
import 'package:freezed_annotation/freezed_annotation.dart';
import 'error.dart';
part 'result.freezed.dart';
sealed class Result<D, E extends Error> with _$Result<D, E> {
const factory Result.success(D data) = ResultSuccess;
const factory Result.error(E error) = ResultError;
}重要提示:由于使用声明,Dart编译器会强制处理所有场景。遗漏场景时会触发编译警告。
sealedswitch기능별 에러 정의
功能专属错误定义
에러는 enum + 로 정의한다. 각 값은 사용자에게 보여줄 한국어 메시지를 에 둔다.
implements ErrortoString()dart
// lib/core/domain/error/network_error.dart
enum NetworkError implements Error {
requestTimeout,
noInternet,
serverError,
unknown;
String toString() => switch (this) {
NetworkError.requestTimeout => '요청 시간이 초과되었습니다',
NetworkError.noInternet => '인터넷 연결을 확인해 주세요',
NetworkError.serverError => '서버에 문제가 발생했습니다',
NetworkError.unknown => '알 수 없는 문제가 발생했습니다',
};
}공유 에러()는 에, feature 전용 에러는 에 둔다. 예: , .
NetworkErrorlib/core/domain/error/lib/domain/error/<feature>_error.dartBookmarkErrorNewRecipeError다중 에러는 표현하지 않는다. 한 는 정확히 한 가지 에러만 담는다. 여러 조건을 동시에 알려줘야 한다면 그것은 도메인 설계 문제다.
Result错误通过**enum + **来定义。每个值在中存放要展示给用户的韩语消息。
implements ErrortoString()dart
// lib/core/domain/error/network_error.dart
enum NetworkError implements Error {
requestTimeout,
noInternet,
serverError,
unknown;
String toString() => switch (this) {
NetworkError.requestTimeout => '요청 시간이 초과되었습니다',
NetworkError.noInternet => '인터넷 연결을 확인해 주세요',
NetworkError.serverError => '서버에 문제가 발생했습니다',
NetworkError.unknown => '알 수 없는 문제가 발생했습니다',
};
}公共错误()放在目录下,功能专属错误放在目录下。例如:、。
NetworkErrorlib/core/domain/error/lib/domain/error/<feature>_error.dartBookmarkErrorNewRecipeError不支持多错误表达。一个只能包含一种错误。如果需要同时告知多个条件,这属于领域设计问题。
ResultUseCase / Repository 반환 타입
UseCase / Repository 返回类型
dart
// UseCase 시그니처
Future<Result<List<String>, NetworkError>> execute();
Future<Result<List<Recipe>, BookmarkError>> execute(int recipeId);- 성공 데이터 타입 와 에러 타입
D를 명시한다.E - Data 레이어에서 네트워크/DB 예외를 catch 해 같은 값으로 변환한다.
NetworkError.unknown - UseCase는 여러 Repository 에러를 자기 feature 에러로 매핑해서 반환한다 (예: 북마크 저장 실패 시 ).
BookmarkError.saveFailed
dart
// UseCase 签名
Future<Result<List<String>, NetworkError>> execute();
Future<Result<List<Recipe>, BookmarkError>> execute(int recipeId);- 明确指定成功数据类型和错误类型
D。E - 在数据层捕获网络/数据库异常,转换为等值。
NetworkError.unknown - UseCase将多个Repository错误映射为自身功能的错误后返回(例如:书签保存失败时返回)。
BookmarkError.saveFailed
ViewModel에서 소비하기
在ViewModel中消费
sealed 타입이므로 타입 파라미터를 명시해 에서 패턴 매칭한다. 이 프로젝트는 다음 형태를 정석으로 쓴다 ( 참조).
switchhome_view_model.dartdart
void _fetchCategories() async {
final result = await _getCategoriesUseCase.execute();
switch (result) {
case ResultSuccess<List<String>, NetworkError>():
_state = state.copyWith(
categories: result.data,
selectedCategory: 'All',
);
notifyListeners();
case ResultError<List<String>, NetworkError>():
switch (result.error) {
case NetworkError.requestTimeout:
case NetworkError.noInternet:
case NetworkError.serverError:
case NetworkError.unknown:
_eventController.add(result.error);
}
}
}왜 이렇게 쓰나:
- /
ResultSuccess<D, E>()를 적어야 제네릭이 유지되고ResultError<D, E>()/result.data의 구체 타입이 살아 있다.result.error - 안쪽 는 모든 enum 케이스를 강제로 나열하게 만들어, 새 에러가 추가될 때 누락된 처리 지점을 컴파일러가 알려준다.
switch (result.error)
由于是密封类型,需要明确指定类型参数,在中进行模式匹配。本项目将以下形式作为标准写法(参考)。
switchhome_view_model.dartdart
void _fetchCategories() async {
final result = await _getCategoriesUseCase.execute();
switch (result) {
case ResultSuccess<List<String>, NetworkError>():
_state = state.copyWith(
categories: result.data,
selectedCategory: 'All',
);
notifyListeners();
case ResultError<List<String>, NetworkError>():
switch (result.error) {
case NetworkError.requestTimeout:
case NetworkError.noInternet:
case NetworkError.serverError:
case NetworkError.unknown:
_eventController.add(result.error);
}
}
}为什么这样写:
- 必须填写/
ResultSuccess<D, E>()才能保留泛型,确保ResultError<D, E>()/result.data的具体类型有效。result.error - 内层会强制列出所有enum场景,新增错误时编译器会提示遗漏的处理点。
switch (result.error)
에러를 UI로 전달하는 방식
错误传递到UI的方式
두 가지 표준 패턴이 있다.
有两种标准模式。
1) 한 번 보여주는 스낵바/토스트 — StreamController
이벤트
StreamController1) 一次性显示的Snackbar/Toast — StreamController
事件
StreamControllerdart
final _eventController = StreamController<NetworkError>();
Stream<NetworkError> get eventStream => _eventController.stream;Root 위젯이 을 listen 해서 를 호출한다. 상태에 담으면 리빌드마다 반복되므로 이벤트로 내보낸다.
eventStreamScaffoldMessenger.showSnackBardart
final _eventController = StreamController<NetworkError>();
Stream<NetworkError> get eventStream => _eventController.stream;根Widget监听并调用。如果将错误存入状态,会导致每次重建都重复显示,因此以事件形式抛出。
eventStreamScaffoldMessenger.showSnackBar2) 지속 상태(에러 배너) — State 필드
2) 持续状态(错误横幅)— State字段
에러 화면 자체를 그려야 한다면 에 필드를 두고 로 반영한다. 사용자가 닫거나 재시도하면 로 초기화한다.
StateNetworkError? errorcopyWith(error: ...)error: null如果需要绘制错误界面本身,可以在中添加字段,通过更新状态。用户关闭或重试时重置为。
StateNetworkError? errorcopyWith(error: ...)error: nullData 레이어 — 예외를 Result로 바꾸는 지점
数据层 — 将异常转换为Result的节点
dart
Future<Result<List<RecipeDto>, NetworkError>> getRecipes() async {
try {
final raw = await _recipeDataSource.getRecipes();
return Result.success(raw.map(RecipeDto.fromJson).toList());
} on SocketException {
return const Result.error(NetworkError.noInternet);
} on TimeoutException {
return const Result.error(NetworkError.requestTimeout);
} catch (_) {
return const Result.error(NetworkError.unknown);
}
}원칙:
- 예외가 발생하는 레이어가 곧 잡는 레이어다. 플랫폼/HTTP 예외는 Data에서, 도메인 검증 실패는 Domain에서 로 변환한다.
Result.error - Presentation 에는 예외가 절대 올라오지 않게 한다. ViewModel 의 가 보이면 경고 신호다.
try/catch
dart
Future<Result<List<RecipeDto>, NetworkError>> getRecipes() async {
try {
final raw = await _recipeDataSource.getRecipes();
return Result.success(raw.map(RecipeDto.fromJson).toList());
} on SocketException {
return const Result.error(NetworkError.noInternet);
} on TimeoutException {
return const Result.error(NetworkError.requestTimeout);
} catch (_) {
return const Result.error(NetworkError.unknown);
}
}原则:
- 异常发生的层级就是捕获的层级。平台/HTTP异常在数据层处理,领域验证失败在领域层转换为。
Result.error - 绝对不允许异常传递到Presentation层。如果在ViewModel中看到,就是危险信号。
try/catch
어떤 에러 타입을 쓸지 결정표
错误类型决策表
| 시나리오 | 에러 타입 | 위치 |
|---|---|---|
| 네트워크 호출 실패 | | |
| 로컬 저장소/DB 실패 | | |
| 기능 전용 실패 (북마크 저장 실패 등) | | |
| 여러 DataSource 를 묶는 Repository | 상위 에러 타입 ( | 해당 feature |
| 场景 | 错误类型 | 位置 |
|---|---|---|
| 网络调用失败 | | |
| 本地存储/数据库失败 | | |
| 功能专属失败(如书签保存失败) | 类似 | |
| 整合多个DataSource的Repository | 上层错误类型( | 对应功能模块 |
체크리스트
检查清单
- 새 feature의 실패 유형을 enum + 로 정의했다
implements Error - UseCase 시그니처가 로 타입 파라미터를 명시한다
Future<Result<D, FooError>> - ViewModel에서 가
switch (result)/ResultSuccess<D, E>()를 모두 처리한다ResultError<D, E>() - 에러 enum 의 모든 값이 내부 에서 나열되어 있다
switch (result.error) - Data 레이어가 플랫폼 예외를 catch 해 로 변환한다
Result.error - Presentation 어디에도 가 떠돌지 않는다
try/catch
- 新功能的失败类型已通过**enum + **定义
implements Error - UseCase签名以形式明确指定类型参数
Future<Result<D, FooError>> - ViewModel中的已处理
switch (result)/ResultSuccess<D, E>()所有情况ResultError<D, E>() - 错误enum的所有值都已在内部中列出
switch (result.error) - 数据层已捕获平台异常并转换为
Result.error - Presentation层中不存在游离的
try/catch
안티 패턴
反模式
- ❌ 를 그대로 반환하고 실패를
Future<List<Recipe>>로 전달 → 호출자가 실패를 잊는다.throw - ❌ 처럼
Result<List<Recipe>, Exception>을 에러 타입으로 사용 →Exception마커의 의미가 사라진다.Error - ❌ 한 에 리스트로 여러 에러를 담기 → 모델이 복잡해지고 UI 분기가 폭발한다.
Result - ❌ 에서
switch로 퉁치기 → 새 에러 값 추가 시 컴파일러가 경고해 주는 안전망을 날려버린다.default:
- ❌ 直接返回并通过
Future<List<Recipe>>传递失败 → 调用方可能遗漏失败处理throw - ❌ 像一样使用
Result<List<Recipe>, Exception>作为错误类型 → 失去Exception标记的意义Error - ❌ 在一个中以列表形式包含多个错误 → 模型变得复杂,UI分支爆炸式增长
Result - ❌ 在中用
switch统一处理 → 丢弃了新增错误值时编译器给出警告的安全保障default: