Core Layer Patterns
The core layer provides fundamental building blocks used across all other layers in Clean Architecture. It contains no Flutter-specific code and focuses on pure Dart patterns.
Directory Structure
lib/core/ ├── errors/ │ ├── failures.dart # Base Failure classes │ └── exceptions.dart # Base Exception classes ├── utils/ │ ├── extensions.dart # Dart extensions │ └── validators.dart # Input validators ├── config/ │ ├── app_config.dart # Environment configuration │ └── theme_config.dart # Theme configuration └── di/ └── injection_container.dart # Dependency injection setup
Error Handling Patterns
Failures (Domain Layer)
Failures represent expected error states in the domain layer. They are returned from use cases using the Either<Failure, T> pattern.
// lib/core/errors/failures.dart abstract class Failure { final String message; const Failure(this.message);
@override String toString() => message;
@override bool operator ==(Object other) => identical(this, other) || other is Failure && runtimeType == other.runtimeType && message == other.message;
@override int get hashCode => message.hashCode; }
// Network-related failures class ServerFailure extends Failure { const ServerFailure([String message = 'Server error occurred']) : super(message); }
class NetworkFailure extends Failure { const NetworkFailure([String message = 'Network error occurred']) : super(message); }
class TimeoutFailure extends Failure { const TimeoutFailure([String message = 'Request timed out']) : super(message); }
// Data-related failures class CacheFailure extends Failure { const CacheFailure([String message = 'Cache error occurred']) : super(message); }
class ParseFailure extends Failure { const ParseFailure([String message = 'Failed to parse data']) : super(message); }
// Validation failures class ValidationFailure extends Failure { const ValidationFailure([String message = 'Validation error occurred']) : super(message); }
class InvalidInputFailure extends Failure { const InvalidInputFailure([String message = 'Invalid input provided']) : super(message); }
// Authentication failures class UnauthorizedFailure extends Failure { const UnauthorizedFailure([String message = 'Unauthorized access']) : super(message); }
class ForbiddenFailure extends Failure { const ForbiddenFailure([String message = 'Access forbidden']) : super(message); }
// Not found failures class NotFoundFailure extends Failure { const NotFoundFailure([String message = 'Resource not found']) : super(message); }
Usage in Use Cases:
class LoginUser { final UserRepository repository; const LoginUser({required this.repository});
Future<Either<Failure, User>> call(String email, String password) async { return await repository.login(email, password); } }
Exceptions (Data Layer)
Exceptions represent unexpected error states in the data layer. They are thrown by data sources and caught by repositories.
// lib/core/errors/exceptions.dart class ServerException implements Exception { final String message; final int? statusCode;
const ServerException(this.message, [this.statusCode]);
@override String toString() => 'ServerException: $message (status: $statusCode)'; }
class NetworkException implements Exception { final String message;
const NetworkException(this.message);
@override String toString() => 'NetworkException: $message'; }
class CacheException implements Exception { final String message;
const CacheException(this.message);
@override String toString() => 'CacheException: $message'; }
class ParseException implements Exception { final String message; final dynamic originalError;
const ParseException(this.message, [this.originalError]);
@override String toString() => 'ParseException: $message'; }
class UnauthorizedException implements Exception { final String message;
const UnauthorizedException([this.message = 'Unauthorized']);
@override String toString() => 'UnauthorizedException: $message'; }
Usage in Repositories:
class UserRepositoryImpl implements UserRepository { @override Future<Either<Failure, User>> login(String email, String password) async { try { final userModel = await remoteDataSource.login(email, password); return Right(userModel.toEntity()); } on ServerException catch (e) { return Left(ServerFailure(e.message)); } on NetworkException catch (e) { return Left(NetworkFailure(e.message)); } on UnauthorizedException catch (e) { return Left(UnauthorizedFailure(e.message)); } catch (e) { return Left(ServerFailure('Unexpected error: $e')); } } }
Extension Methods
// lib/core/utils/extensions.dart import 'package:flutter/material.dart';
/// String extensions extension StringExtensions on String { /// Capitalize first letter String capitalize() { if (isEmpty) return this; return '${this[0].toUpperCase()}${substring(1)}'; }
/// Check if string is valid email bool get isValidEmail { final emailRegex = RegExp(r'^[\w-.]+@([\w-]+.)+[\w-]{2,4}$'); return emailRegex.hasMatch(this); }
/// Check if string is valid phone bool get isValidPhone { final phoneRegex = RegExp(r'^+?[\d\s-]{10,}$'); return phoneRegex.hasMatch(this); }
/// Remove all whitespace String removeWhitespace() => replaceAll(RegExp(r'\s+'), ''); }
/// DateTime extensions extension DateTimeExtensions on DateTime { /// Check if date is today bool get isToday { final now = DateTime.now(); return year == now.year && month == now.month && day == now.day; }
/// Check if date is yesterday bool get isYesterday { final yesterday = DateTime.now().subtract(const Duration(days: 1)); return year == yesterday.year && month == yesterday.month && day == yesterday.day; }
/// Format as relative time (2 hours ago, 3 days ago) String get relativeTime { final now = DateTime.now(); final difference = now.difference(this);
if (difference.inDays > 365) {
return '${(difference.inDays / 365).floor()} year${difference.inDays > 730 ? 's' : ''} ago';
} else if (difference.inDays > 30) {
return '${(difference.inDays / 30).floor()} month${difference.inDays > 60 ? 's' : ''} ago';
} else if (difference.inDays > 0) {
return '${difference.inDays} day${difference.inDays > 1 ? 's' : ''} ago';
} else if (difference.inHours > 0) {
return '${difference.inHours} hour${difference.inHours > 1 ? 's' : ''} ago';
} else if (difference.inMinutes > 0) {
return '${difference.inMinutes} minute${difference.inMinutes > 1 ? 's' : ''} ago';
} else {
return 'Just now';
}
} }
/// List extensions extension ListExtensions<T> on List<T> { /// Get element at index or null T? elementAtOrNull(int index) { if (index < 0 || index >= length) return null; return this[index]; }
/// Remove duplicates List<T> unique() => toSet().toList(); }
/// BuildContext extensions extension ContextExtensions on BuildContext { /// Get screen width double get screenWidth => MediaQuery.of(this).size.width;
/// Get screen height double get screenHeight => MediaQuery.of(this).size.height;
/// Get theme ThemeData get theme => Theme.of(this);
/// Get text theme TextTheme get textTheme => Theme.of(this).textTheme;
/// Show snackbar void showSnackBar(String message, {bool isError = false}) { ScaffoldMessenger.of(this).showSnackBar( SnackBar( content: Text(message), backgroundColor: isError ? Colors.red : null, ), ); } }
Validators
// lib/core/utils/validators.dart class Validators { /// Email validator static String? email(String? value) { if (value == null || value.isEmpty) { return 'Email is required'; } if (!value.isValidEmail) { return 'Please enter a valid email'; } return null; }
/// Password validator static String? password(String? value, {int minLength = 8}) { if (value == null || value.isEmpty) { return 'Password is required'; } if (value.length < minLength) { return 'Password must be at least $minLength characters'; } return null; }
/// Phone validator static String? phone(String? value) { if (value == null || value.isEmpty) { return 'Phone number is required'; } if (!value.isValidPhone) { return 'Please enter a valid phone number'; } return null; }
/// Required field validator static String? required(String? value, {String? fieldName}) { if (value == null || value.trim().isEmpty) { return '${fieldName ?? 'This field'} is required'; } return null; }
/// Min length validator static String? minLength(String? value, int min, {String? fieldName}) { if (value == null || value.length < min) { return '${fieldName ?? 'This field'} must be at least $min characters'; } return null; }
/// Max length validator static String? maxLength(String? value, int max, {String? fieldName}) { if (value != null && value.length > max) { return '${fieldName ?? 'This field'} must not exceed $max characters'; } return null; }
/// Compose multiple validators static String? Function(String?) compose(List<String? Function(String?)> validators) { return (value) { for (final validator in validators) { final error = validator(value); if (error != null) return error; } return null; }; } }
Usage in Forms:
TextFormField( validator: Validators.compose([ Validators.required, Validators.email, ]), decoration: const InputDecoration(labelText: 'Email'), )
Application Configuration
// lib/core/config/app_config.dart class AppConfig { final String appName; final String apiBaseUrl; final String apiKey; final int connectTimeout; final int receiveTimeout; final bool enableLogging;
const AppConfig({ required this.appName, required this.apiBaseUrl, required this.apiKey, this.connectTimeout = 30000, this.receiveTimeout = 30000, this.enableLogging = false, });
/// Development configuration factory AppConfig.development() { return const AppConfig( appName: 'MyApp (Dev)', apiBaseUrl: 'https://dev-api.example.com', apiKey: 'dev_api_key', enableLogging: true, ); }
/// Staging configuration factory AppConfig.staging() { return const AppConfig( appName: 'MyApp (Staging)', apiBaseUrl: 'https://staging-api.example.com', apiKey: 'staging_api_key', enableLogging: true, ); }
/// Production configuration factory AppConfig.production() { return const AppConfig( appName: 'MyApp', apiBaseUrl: 'https://api.example.com', apiKey: 'prod_api_key', enableLogging: false, ); } }
Theme Configuration
// lib/core/config/theme_config.dart import 'package:flutter/material.dart';
class ThemeConfig { /// Light theme static ThemeData lightTheme() { return ThemeData( useMaterial3: true, colorScheme: ColorScheme.fromSeed( seedColor: Colors.blue, brightness: Brightness.light, ), appBarTheme: const AppBarTheme( elevation: 0, centerTitle: true, ), cardTheme: CardTheme( elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), inputDecorationTheme: InputDecorationTheme( border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), ), filled: true, ), ); }
/// Dark theme static ThemeData darkTheme() { return ThemeData( useMaterial3: true, colorScheme: ColorScheme.fromSeed( seedColor: Colors.blue, brightness: Brightness.dark, ), appBarTheme: const AppBarTheme( elevation: 0, centerTitle: true, ), cardTheme: CardTheme( elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), inputDecorationTheme: InputDecorationTheme( border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), ), filled: true, ), ); } }
Dependency Injection
// lib/core/di/injection_container.dart import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; import 'package:http/http.dart' as http;
class DependencyInjection { /// Initialize all dependencies static Future<void> init() async { // Core dependencies _initCore();
// Data sources
_initDataSources();
// Repositories
_initRepositories();
// Use cases
_initUseCases();
}
static void _initCore() { // HTTP client Get.put<http.Client>(http.Client(), permanent: true);
// GetStorage
Get.put<GetStorage>(GetStorage(), permanent: true);
// App configuration
Get.put<AppConfig>(AppConfig.production(), permanent: true);
}
static void _initDataSources() { // Register data sources // Example: // Get.lazyPut<UserRemoteDataSource>( // () => UserRemoteDataSourceImpl(http: Get.find()), // ); }
static void _initRepositories() { // Register repositories // Example: // Get.lazyPut<UserRepository>( // () => UserRepositoryImpl( // remoteDataSource: Get.find(), // localDataSource: Get.find(), // ), // ); }
static void _initUseCases() { // Register use cases // Example: // Get.lazyPut(() => LoginUser(repository: Get.find())); } }
Usage in main.dart:
void main() async { WidgetsFlutterBinding.ensureInitialized(); await GetStorage.init(); await DependencyInjection.init(); runApp(const MyApp()); }
Best Practices
Failures vs Exceptions:
-
Use Failure in domain layer (returned via Either )
-
Use Exception in data layer (thrown and caught)
-
Never throw exceptions from use cases
Extension Methods:
-
Keep extensions focused and single-purpose
-
Avoid overly generic extension names
-
Document complex extensions
Configuration:
-
Use factory constructors for different environments
-
Never hardcode sensitive data (API keys, secrets)
-
Use environment variables for sensitive config
Dependency Injection:
-
Register dependencies in correct order (data sources → repositories → use cases → controllers)
-
Use lazyPut for most dependencies
-
Use put with permanent: true for singletons needed throughout app lifecycle