// โ Correct status definition (sealed class + Equatable, no codegen)@immutablesealedclassHomeStatusextendsEquatable{constHomeStatus();constfactoryHomeStatus.initial()=HomeInitial;constfactoryHomeStatus.loading()=HomeLoading;constfactoryHomeStatus.loaded(User user)=HomeLoaded;constfactoryHomeStatus.error(Failure failure)=HomeError;@overrideList<Object?>get props =>const[];}// โ State class: immutable fields + copyWith + Equatable props@immutablefinalclassHomeStateextendsEquatable{constHomeState({this.status =constHomeStatus.initial()});finalHomeStatus status;HomeStatecopyWith({HomeStatus? status})=>HomeState(status: status ??this.status);@overrideList<Object?>get props =>[status];}// โ Incorrect: mutable, ad-hoc flags, no Equatable/copyWithclassHomeState{bool isLoading =false;User? user;String? error;}
kobic does NOT use @freezed. State/Event are plain sealed class ... extends Equatable
with const factory sub-states and copyWith โ no *.freezed.dart
codegen.
Dart's Error (TypeError, StateError, NoSuchMethodError,
JsonUnsupportedObjectError, ...) does not extend Exception, but most catch blocks conventionally use
on Exception catch only. Behind a normal awaited call this is harmless โ an uncaught
Error propagates and surfaces as a crash/log. Behind unawaited(fn()) (fire-and-forget), a missed
Error becomes an exception no code path observes; whether it also surfaces as a crash/log elsewhere depends on the execution zone (plain Dart isolate vs. a Flutter app's guarded zone) โ but regardless, the specific consumer waiting on the result (BLoC/Cubit state, a
ValueNotifier/useState-gated button or spinner, a StreamController) hangs forever โ infinite loading spinner, permanently disabled button, dialog that never closes.
// โ Error thrown inside disappears completely โ consumer hangs foreverunawaited(_revalidate(query, controller));Future<void>_revalidate(...)async{try{awaitsaveToCache(...);// may throw a JsonUnsupportedObjectError / TypeError (an Error, not Exception)}onExceptioncatch(e, st){// โ Error passes straight throughLog.e('failed', error: e, stackTrace: st);}}// โ on Object catches both Exception and Error}onObjectcatch(e, st){Log.e('failed', error: e, stackTrace: st);}
Real case: kobic #7746 (SwrStrategyImpl._revalidate, see [cc-flutter-dev:swr-pattern] ยง16). A codebase-wide audit of
unawaited( call sites found 11 confirmed reproductions of this pattern โ see [cc-dev-cycle:discovery-audit] for the triageโverify audit workflow.
// โ Correct validationif(!EmailValidator.validate(email)){returnleft(ValidationFailure('Invalid email'));}// โ Direct use without validationfinal user =await api.login(email, password);
// โ Correct test pattern (AAA)test('should return user when repository succeeds',()async{// Arrangewhen(()=> mockRepository.getUser(any())).thenAnswer((_)async=>Right(testUser));// Actfinal result =awaituseCase(GetUserParams(id:1));// Assertexpect(result,Right(testUser));verify(()=> mockRepository.getUser(1)).called(1);});
// โ Single Responsibility PrincipleclassUserRepositoryimplementsIUserRepository{// User-related logic only}// โ Multiple responsibilities mixedclassUserRepository{voidgetUser(){}voidsendEmail(){}// Email should be a separate servicevoidgenerateReport(){}// Report should be separate too}
# Full analysis
melos run analyze
# Combined lint check (format-check โ dart analyze โ dcm analyze)
melos run lint:check
# Formatting(kobic uses dcm format, not dart format)
melos run format
# Underlying command: dcm format .