feat(mobile): implement centralized error handling and project cleanup
- Implemented centralized error handling system (#377) - Unified UIErrorSnackbar and BlocErrorHandler mixin - Migrated ClientAuthBloc and ClientHubsBloc - Consolidated documentation - Addresses Mobile Apps: Project Cleanup (#378)
This commit is contained in:
@@ -4,5 +4,7 @@ export 'src/domain/arguments/usecase_argument.dart';
|
||||
export 'src/domain/usecases/usecase.dart';
|
||||
export 'src/utils/date_time_utils.dart';
|
||||
export 'src/presentation/widgets/web_mobile_frame.dart';
|
||||
export 'src/presentation/mixins/bloc_error_handler.dart';
|
||||
export 'src/presentation/observers/core_bloc_observer.dart';
|
||||
export 'src/config/app_config.dart';
|
||||
export 'src/routing/routing.dart';
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import 'dart:developer' as developer;
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
/// Mixin to standardize error handling across all BLoCs.
|
||||
///
|
||||
/// This mixin provides a centralized way to handle errors in BLoC event handlers,
|
||||
/// reducing boilerplate and ensuring consistent error handling patterns.
|
||||
///
|
||||
/// **Benefits:**
|
||||
/// - Eliminates repetitive try-catch blocks
|
||||
/// - Automatically logs errors with technical details
|
||||
/// - Converts AppException to localized error keys
|
||||
/// - Handles unexpected errors gracefully
|
||||
///
|
||||
/// **Usage:**
|
||||
/// ```dart
|
||||
/// class MyBloc extends Bloc<MyEvent, MyState> with BlocErrorHandler<MyState> {
|
||||
/// Future<void> _onEvent(MyEvent event, Emitter<MyState> emit) async {
|
||||
/// await handleError(
|
||||
/// emit: emit,
|
||||
/// action: () async {
|
||||
/// final result = await _useCase();
|
||||
/// emit(MyState.success(result));
|
||||
/// },
|
||||
/// onError: (errorKey) => MyState.error(errorKey),
|
||||
/// );
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
mixin BlocErrorHandler<S> {
|
||||
/// Executes an async action with centralized error handling.
|
||||
///
|
||||
/// [emit] - The state emitter from the event handler
|
||||
/// [action] - The async operation to execute (e.g., calling a use case)
|
||||
/// [onError] - Function that creates an error state from the error message key
|
||||
/// [loggerName] - Optional custom logger name (defaults to BLoC class name)
|
||||
///
|
||||
/// **Error Flow:**
|
||||
/// 1. Executes the action
|
||||
/// 2. If AppException is thrown:
|
||||
/// - Logs error code and technical message
|
||||
/// - Emits error state with localization key
|
||||
/// 3. If unexpected error is thrown:
|
||||
/// - Logs full error and stack trace
|
||||
/// - Emits generic error state
|
||||
Future<void> handleError({
|
||||
required Emitter<S> emit,
|
||||
required Future<void> Function() action,
|
||||
required S Function(String errorKey) onError,
|
||||
String? loggerName,
|
||||
}) async {
|
||||
try {
|
||||
await action();
|
||||
} on AppException catch (e) {
|
||||
// Known application error - log technical details
|
||||
developer.log(
|
||||
'Error ${e.code}: ${e.technicalMessage}',
|
||||
name: loggerName ?? runtimeType.toString(),
|
||||
);
|
||||
// Emit error state with localization key
|
||||
emit(onError(e.messageKey));
|
||||
} catch (e, stackTrace) {
|
||||
// Unexpected error - log everything for debugging
|
||||
developer.log(
|
||||
'Unexpected error: $e',
|
||||
name: loggerName ?? runtimeType.toString(),
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
// Emit generic error state
|
||||
emit(onError('errors.generic.unknown'));
|
||||
}
|
||||
}
|
||||
|
||||
/// Executes an async action with error handling and returns a result.
|
||||
///
|
||||
/// This variant is useful when you need to get a value from the action
|
||||
/// and handle errors without emitting states.
|
||||
///
|
||||
/// Returns the result of the action, or null if an error occurred.
|
||||
///
|
||||
/// **Usage:**
|
||||
/// ```dart
|
||||
/// final user = await handleErrorWithResult(
|
||||
/// action: () => _getUserUseCase(),
|
||||
/// onError: (errorKey) {
|
||||
/// emit(MyState.error(errorKey));
|
||||
/// },
|
||||
/// );
|
||||
/// if (user != null) {
|
||||
/// emit(MyState.success(user));
|
||||
/// }
|
||||
/// ```
|
||||
Future<T?> handleErrorWithResult<T>({
|
||||
required Future<T> Function() action,
|
||||
required void Function(String errorKey) onError,
|
||||
String? loggerName,
|
||||
}) async {
|
||||
try {
|
||||
return await action();
|
||||
} on AppException catch (e) {
|
||||
developer.log(
|
||||
'Error ${e.code}: ${e.technicalMessage}',
|
||||
name: loggerName ?? runtimeType.toString(),
|
||||
);
|
||||
onError(e.messageKey);
|
||||
return null;
|
||||
} catch (e, stackTrace) {
|
||||
developer.log(
|
||||
'Unexpected error: $e',
|
||||
name: loggerName ?? runtimeType.toString(),
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
onError('errors.generic.unknown');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import 'dart:developer' as developer;
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
/// Global BLoC observer for centralized logging and monitoring.
|
||||
///
|
||||
/// This observer provides visibility into all BLoC lifecycle events across
|
||||
/// the entire application, enabling centralized logging, debugging, and
|
||||
/// error monitoring.
|
||||
///
|
||||
/// **Features:**
|
||||
/// - Logs BLoC creation and disposal
|
||||
/// - Logs all events and state changes
|
||||
/// - Captures and logs errors with stack traces
|
||||
/// - Ready for integration with monitoring services (Sentry, Firebase Crashlytics)
|
||||
///
|
||||
/// **Setup:**
|
||||
/// Register this observer in your app's main.dart before runApp():
|
||||
/// ```dart
|
||||
/// void main() {
|
||||
/// Bloc.observer = CoreBlocObserver();
|
||||
/// runApp(MyApp());
|
||||
/// }
|
||||
/// ```
|
||||
class CoreBlocObserver extends BlocObserver {
|
||||
/// Whether to log state changes (can be verbose in production)
|
||||
final bool logStateChanges;
|
||||
|
||||
/// Whether to log events
|
||||
final bool logEvents;
|
||||
|
||||
CoreBlocObserver({
|
||||
this.logStateChanges = false,
|
||||
this.logEvents = true,
|
||||
});
|
||||
|
||||
@override
|
||||
void onCreate(BlocBase bloc) {
|
||||
super.onCreate(bloc);
|
||||
developer.log(
|
||||
'Created: ${bloc.runtimeType}',
|
||||
name: 'BlocObserver',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void onEvent(Bloc bloc, Object? event) {
|
||||
super.onEvent(bloc, event);
|
||||
if (logEvents) {
|
||||
developer.log(
|
||||
'Event: ${event.runtimeType}',
|
||||
name: bloc.runtimeType.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onChange(BlocBase bloc, Change change) {
|
||||
super.onChange(bloc, change);
|
||||
if (logStateChanges) {
|
||||
developer.log(
|
||||
'State: ${change.currentState.runtimeType} → ${change.nextState.runtimeType}',
|
||||
name: bloc.runtimeType.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onError(BlocBase bloc, Object error, StackTrace stackTrace) {
|
||||
super.onError(bloc, error, stackTrace);
|
||||
|
||||
// Log error to console
|
||||
developer.log(
|
||||
'ERROR in ${bloc.runtimeType}',
|
||||
name: 'BlocObserver',
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
|
||||
// TODO: Send to monitoring service
|
||||
// Example integrations:
|
||||
//
|
||||
// Sentry:
|
||||
// Sentry.captureException(
|
||||
// error,
|
||||
// stackTrace: stackTrace,
|
||||
// hint: Hint.withMap({'bloc': bloc.runtimeType.toString()}),
|
||||
// );
|
||||
//
|
||||
// Firebase Crashlytics:
|
||||
// FirebaseCrashlytics.instance.recordError(
|
||||
// error,
|
||||
// stackTrace,
|
||||
// reason: 'BLoC Error in ${bloc.runtimeType}',
|
||||
// );
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose(BlocBase bloc) {
|
||||
super.onClose(bloc);
|
||||
developer.log(
|
||||
'Closed: ${bloc.runtimeType}',
|
||||
name: 'BlocObserver',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void onTransition(Bloc bloc, Transition transition) {
|
||||
super.onTransition(bloc, transition);
|
||||
if (logStateChanges) {
|
||||
developer.log(
|
||||
'Transition: ${transition.event.runtimeType} → ${transition.nextState.runtimeType}',
|
||||
name: bloc.runtimeType.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user