
Flutter Enterprise
- 19 installs
- 4 repo stars
- Updated December 6, 2025
- ajianaz/skills-collection
flutter-enterprise is a skill for building enterprise Flutter apps with feature-based clean architecture, dependency injection, and code generation.
About
flutter-enterprise is a skill for building enterprise Flutter applications using feature-based clean architecture. A developer uses it to set up a modular, scalable project structure with dependency injection and code generation for maintainability. It runs a sequential, phased workflow beginning with requirements analysis.
- Feature-based clean architecture for enterprise Flutter apps
- Modular structure with dependency injection and code generation
- Sequential phased workflow starting from requirements analysis
Flutter Enterprise by the numbers
- 19 all-time installs (skills.sh)
- Ranked #770 of 1,039 Mobile Development skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
flutter-enterprise capabilities & compatibility
- Capabilities
- frontend · ui design
- Use cases
- frontend · ui design
- Pricing
- Free
What flutter-enterprise says it does
Lightweight Flutter development skill for building enterprise applications using feature-based clean architecture patterns.
"Feature-first, testable, maintainable enterprise code"
npx skills add https://github.com/ajianaz/skills-collection --skill flutter-enterpriseAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 19 |
|---|---|
| repo stars | ★ 4 |
| Last updated | December 6, 2025 |
| Repository | ajianaz/skills-collection ↗ |
What it does
Structure an enterprise Flutter app with feature-based clean architecture, DI, and code generation.
Who is it for?
Setting up a scalable, maintainable enterprise Flutter codebase with clean architecture.
Skip if: Simple prototype apps or non-Flutter mobile frameworks.
When should I use this skill?
You want to build an enterprise Flutter app with feature-based modular clean architecture.
What you get
A feature-based, clean-architecture Flutter codebase with DI and code generation ready to scale.
- Feature-based project structure
- Clean architecture layers
- Dependency injection setup
By the numbers
- 5 prioritized architecture areas
Files
Flutter Enterprise - Feature-Based Clean Architecture
Lightweight Flutter development skill for building enterprise applications using feature-based clean architecture patterns.
Core Philosophy
"Feature-first, testable, maintainable enterprise code" - Focus on:
| Priority | Area | Purpose |
|---|---|---|
| 1 | Feature-Based Structure | Modular, scalable code organization |
| 2 | Clean Architecture | Separation of concerns and testability |
| 3 | Dependency Injection | Loose coupling and maintainability |
| 4 | Enterprise Patterns | Proven enterprise development practices |
| 5 | Code Generation | Boilerplate reduction and consistency |
Development Workflow
Execute phases sequentially. Complete each before proceeding.
Phase 1: Analyze Requirements
1. Feature identification - Identify distinct business features 2. Data flow analysis - Map data dependencies between features 3. Integration points - Define external service integrations 4. Scalability requirements - Plan for future feature additions
Output: Feature breakdown with dependency mapping.
Phase 2: Design Feature Architecture
1. Feature boundary definition - Define clear feature boundaries 2. Data layer planning - Design repositories and data sources 3. Domain modeling - Create entities and use cases 4. Presentation layer design - Plan UI components and state management
Output: Feature architecture diagram and data contracts.
Phase 3: Implement Core Structure
1. Project setup - Create feature-based directory structure 2. Dependency injection - Set up service locator or DI container 3. Core utilities - Create shared utilities and constants 4. Navigation setup - Implement routing structure
Feature Structure Pattern:
lib/
├── core/
│ ├── constants/
│ ├── errors/
│ ├── network/
│ ├── utils/
│ └── widgets/
├── features/
│ ├── feature_name/
│ │ ├── data/
│ │ │ ├── datasources/
│ │ │ ├── models/
│ │ │ └── repositories/
│ │ ├── domain/
│ │ │ ├── entities/
│ │ │ ├── repositories/
│ │ │ └── usecases/
│ │ └── presentation/
│ │ ├── pages/
│ │ ├── widgets/
│ │ └── providers/
│ └── ...
└── main.dartPhase 4: Implement Feature Modules
1. Data layer - Implement repositories and data sources 2. Domain layer - Create business logic and use cases 3. Presentation layer - Build UI components and state management 4. Feature integration - Connect feature to main app
Clean Architecture Implementation:
// Domain Layer - Entity
class User {
final String id;
final String name;
final String email;
User({required this.id, required this.name, required this.email});
}
// Domain Layer - Repository (Abstract)
abstract class UserRepository {
Future<List<User>> getUsers();
Future<User> getUserById(String id);
}
// Domain Layer - Use Case
class GetUsersUseCase {
final UserRepository repository;
GetUsersUseCase(this.repository);
Future<List<User>> call() async {
return await repository.getUsers();
}
}
// Data Layer - Repository Implementation
class UserRepositoryImpl implements UserRepository {
final RemoteDataSource remoteDataSource;
UserRepositoryImpl(this.remoteDataSource);
@override
Future<List<User>> getUsers() async {
final userModels = await remoteDataSource.getUsers();
return userModels.map((model) => model.toEntity()).toList();
}
}Phase 5: Setup Testing Structure
1. Unit tests - Test domain layer and use cases 2. Integration tests - Test data layer and repositories 3. Widget tests - Test presentation layer components 4. Test utilities - Create mock objects and test helpers
Quick Reference
Feature-Based Architecture Patterns
| Layer | Responsibility | Key Components |
|---|---|---|
| Presentation | UI and State Management | Pages, Widgets, Providers/Bloc |
| Domain | Business Logic | Entities, Use Cases, Repository Interfaces |
| Data | Data Implementation | Models, Data Sources, Repository Implementations |
Dependency Injection Setup
// main.dart
void main() {
// Initialize dependencies
final serviceLocator = GetIt.instance;
// Data sources
serviceLocator.registerLazySingleton<RemoteDataSource>(
() => RemoteDataSourceImpl(httpClient: serviceLocator()));
// Repositories
serviceLocator.registerLazySingleton<UserRepository>(
() => UserRepositoryImpl(serviceLocator()));
// Use cases
serviceLocator.registerFactory<GetUsersUseCase>(
() => GetUsersUseCase(serviceLocator()));
runApp(MyApp());
}State Management Patterns
This skill now supports state management neutrality with equivalent implementations for all four major approaches:
Provider Pattern
// Presentation Layer - Provider
class UserProvider extends ChangeNotifier {
final GetUsersUseCase getUsersUseCase;
List<User> _users = [];
bool _isLoading = false;
UserProvider({required this.getUsersUseCase});
List<User> get users => _users;
bool get isLoading => _isLoading;
Future<void> loadUsers() async {
_isLoading = true;
notifyListeners();
try {
_users = await getUsersUseCase();
} catch (e) {
// Handle error
} finally {
_isLoading = false;
notifyListeners();
}
}
}Bloc Pattern
// Presentation Layer - Bloc
abstract class UserEvent extends Equatable {}
class LoadUsers extends UserEvent {}
abstract class UserState extends Equatable {}
class UserLoading extends UserState {}
class UserLoaded extends UserState {
final List<User> users;
UserLoaded(this.users);
@override
List<Object> get props => [users];
}
class UserBloc extends Bloc<UserEvent, UserState> {
final GetUsersUseCase getUsersUseCase;
UserBloc({required this.getUsersUseCase}) : super(UserInitial()) {
on<LoadUsers>(_onLoadUsers);
}
Future<void> _onLoadUsers(LoadUsers event, Emitter<UserState> emit) async {
emit(UserLoading());
try {
final users = await getUsersUseCase();
emit(UserLoaded(users));
} catch (e) {
// Handle error
}
}
}Riverpod Pattern
// Presentation Layer - Riverpod
class UserNotifier extends StateNotifier<AsyncValue<List<User>>> {
final GetUsersUseCase getUsersUseCase;
UserNotifier({required this.getUsersUseCase}) : super(const AsyncValue.loading());
Future<void> loadUsers() async {
state = const AsyncValue.loading();
try {
final users = await getUsersUseCase();
state = AsyncValue.data(users);
} catch (e, stackTrace) {
state = AsyncValue.error(e, stackTrace);
}
}
}
final userProvider = StateNotifierProvider<UserNotifier, AsyncValue<List<User>>>((ref) {
return UserNotifier(getUsersUseCase: ref.watch(getUsersUseCaseProvider));
});GetX Pattern
// Presentation Layer - GetX
class UserController extends GetxController {
final GetUsersUseCase getUsersUseCase;
UserController({required this.getUsersUseCase});
final RxList<User> _users = <User>[].obs;
final RxBool _isLoading = false.obs;
List<User> get users => _users;
bool get isLoading => _isLoading.value;
Future<void> loadUsers() async {
_isLoading.value = true;
try {
final userList = await getUsersUseCase();
_users.assignAll(userList);
} catch (e) {
// Handle error
} finally {
_isLoading.value = false;
}
}
}
// Page Example with GetX
class UserListPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GetBuilder<UserController>(
init: UserController(getUsersUseCase: Get.find()),
builder: (controller) {
return Scaffold(
appBar: AppBar(title: Text('Users')),
body: Obx(() {
if (controller.isLoading.value) {
return Center(child: CircularProgressIndicator());
}
return ListView.builder(
itemCount: controller.users.length,
itemBuilder: (context, index) {
final user = controller.users[index];
return UserTile(user: user);
},
);
}),
);
},
);
}
}Resources
- Architecture patterns: See
references/clean-architecture.md - Feature templates: See
references/feature-templates.md - Testing patterns: See
references/testing-patterns.md - Code generation: See
references/code-generation.md
Technical Stack
- Architecture: Clean Architecture with feature-based structure
- State Management: Provider/Bloc/Riverpod/GetX (state management neutral - all four approaches fully supported with equivalent examples)
- Dependency Injection: GetIt/Injectable
- Code Generation: build_runner, json_annotation, freezed
- Testing: mockito, bloc_test, widget testing
Best Practices
- Feature Independence: Each feature should be self-contained
- Dependency Rule: Dependencies point inward (Presentation → Domain ← Data)
- Interface Segregation: Keep interfaces small and focused
- Single Responsibility: Each class has one reason to change
- Test Coverage: Aim for 80%+ coverage on domain and data layers
---
This Flutter enterprise skill transforms complex enterprise app development into a systematic process that ensures maintainable, scalable, and testable applications using feature-based clean architecture.
Flutter Enterprise Project Structure Template
Complete Directory Structure
my_enterprise_app/
├── lib/
│ ├── core/
│ │ ├── constants/
│ │ │ ├── app_constants.dart
│ │ │ ├── api_constants.dart
│ │ │ └── route_constants.dart
│ │ ├── errors/
│ │ │ ├── exceptions.dart
│ │ │ ├── failures.dart
│ │ │ └── error_handler.dart
│ │ ├── network/
│ │ │ ├── api_client.dart
│ │ │ ├── network_info.dart
│ │ │ ├── interceptors/
│ │ │ │ ├── auth_interceptor.dart
│ │ │ │ ├── logging_interceptor.dart
│ │ │ │ └── error_interceptor.dart
│ │ │ └── adapters/
│ │ │ └── dio_adapter.dart
│ │ ├── utils/
│ │ │ ├── date_utils.dart
│ │ │ ├── validation_utils.dart
│ │ │ ├── format_utils.dart
│ │ │ └── extensions/
│ │ │ ├── string_extension.dart
│ │ │ └── datetime_extension.dart
│ │ ├── widgets/
│ │ │ ├── common/
│ │ │ │ ├── loading_widget.dart
│ │ │ │ ├── error_widget.dart
│ │ │ │ ├── empty_state_widget.dart
│ │ │ │ └── custom_button.dart
│ │ │ └── forms/
│ │ │ ├── text_field_widget.dart
│ │ │ ├── dropdown_widget.dart
│ │ │ └── date_picker_widget.dart
│ │ ├── theme/
│ │ │ ├── app_theme.dart
│ │ │ ├── light_theme.dart
│ │ │ ├── dark_theme.dart
│ │ │ └── colors.dart
│ │ └── di/
│ │ ├── injection_container.dart
│ │ └── feature_injection.dart
│ ├── features/
│ │ ├── authentication/
│ │ │ ├── data/
│ │ │ │ ├── datasources/
│ │ │ │ │ ├── auth_remote_data_source.dart
│ │ │ │ │ └── auth_local_data_source.dart
│ │ │ │ ├── models/
│ │ │ │ │ ├── auth_model.dart
│ │ │ │ │ ├── user_model.dart
│ │ │ │ │ └── token_model.dart
│ │ │ │ └── repositories/
│ │ │ │ └── auth_repository_impl.dart
│ │ │ ├── domain/
│ │ │ │ ├── entities/
│ │ │ │ │ ├── auth_entity.dart
│ │ │ │ │ ├── user_entity.dart
│ │ │ │ │ └── token_entity.dart
│ │ │ │ ├── repositories/
│ │ │ │ │ └── auth_repository.dart
│ │ │ │ └── usecases/
│ │ │ │ ├── login_usecase.dart
│ │ │ │ ├── logout_usecase.dart
│ │ │ │ ├── register_usecase.dart
│ │ │ │ └── get_current_user_usecase.dart
│ │ │ └── presentation/
│ │ │ ├── pages/
│ │ │ │ ├── login_page.dart
│ │ │ │ ├── register_page.dart
│ │ │ │ └── forgot_password_page.dart
│ │ │ ├── widgets/
│ │ │ │ ├── login_form_widget.dart
│ │ │ │ ├── register_form_widget.dart
│ │ │ │ └── social_login_widget.dart
│ │ │ └── state_management/
│ │ │ ├── providers/ # Provider pattern
│ │ │ │ ├── auth_provider.dart
│ │ │ │ └── login_provider.dart
│ │ │ ├── bloc/ # Bloc pattern
│ │ │ │ ├── auth_bloc.dart
│ │ │ │ ├── auth_state.dart
│ │ │ │ ├── auth_event.dart
│ │ │ │ └── login_bloc.dart
│ │ │ └── riverpod/ # Riverpod pattern
│ │ │ ├── auth_provider.dart
│ │ │ ├── auth_notifier.dart
│ └── getx/ # GetX pattern
│ ├── auth_controller.dart
│ └── auth_binding.dart
│ │ │ └── auth_state.dart
│ │ ├── user_management/
│ │ │ ├── data/
│ │ │ │ ├── datasources/
│ │ │ │ ├── user_remote_data_source.dart
│ │ │ │ └── user_local_data_source.dart
│ │ │ ├── models/
│ │ │ │ └── user_model.dart
│ │ │ └── repositories/
│ │ │ └── user_repository_impl.dart
│ │ ├── domain/
│ │ │ ├── entities/
│ │ │ │ └── user_entity.dart
│ │ │ ├── repositories/
│ │ │ │ └── user_repository.dart
│ │ │ └── usecases/
│ │ │ ├── get_users_usecase.dart
│ │ │ ├── get_user_by_id_usecase.dart
│ │ │ ├── create_user_usecase.dart
│ │ │ ├── update_user_usecase.dart
│ │ │ └── delete_user_usecase.dart
│ │ └── presentation/
│ │ ├── pages/
│ │ │ ├── user_list_page.dart
│ │ │ ├── user_detail_page.dart
│ │ │ └── user_form_page.dart
│ │ ├── widgets/
│ │ │ ├── user_card_widget.dart
│ │ │ ├── user_form_widget.dart
│ │ │ └── user_avatar_widget.dart
│ │ └── state_management/
│ │ ├── providers/ # Provider pattern
│ │ │ ├── user_provider.dart
│ │ │ └── user_form_provider.dart
│ │ ├── bloc/ # Bloc pattern
│ │ │ ├── user_bloc.dart
│ │ │ ├── user_state.dart
│ │ │ ├── user_event.dart
│ │ │ └── user_form_bloc.dart
│ │ └── riverpod/ # Riverpod pattern
│ │ ├── user_provider.dart
│ │ ├── user_notifier.dart
│ └── getx/ # GetX pattern
│ ├── user_controller.dart
│ └── user_binding.dart
│ │ └── user_state.dart
│ │ └── [other_features...]
│ ├── main.dart
│ └── app.dart
├── test/
│ ├── fixtures/
│ ├── helpers/
│ ├── mocks/
│ └── unit/
│ ├── core/
│ └── features/
├── integration_test/
├── assets/
│ ├── images/
│ ├── fonts/
│ └── data/
├── pubspec.yaml
├── analysis_options.yaml
└── README.mdCore Files Templates
main.dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'app.dart';
import 'core/di/injection_container.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Set preferred orientations
await SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
]);
// Initialize dependencies
await configureDependencies();
runApp(MyApp());
}app.dart
Provider Implementation
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'core/theme/app_theme.dart';
import 'core/constants/route_constants.dart';
import 'features/authentication/presentation/providers/auth_provider.dart';
import 'features/user_management/presentation/providers/user_provider.dart';
import 'features/authentication/presentation/pages/login_page.dart';
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => sl<AuthProvider>()),
ChangeNotifierProvider(create: (_) => sl<UserProvider>()),
],
child: MaterialApp(
title: 'Enterprise App',
theme: AppTheme.lightTheme,
darkTheme: AppTheme.darkTheme,
themeMode: ThemeMode.system,
initialRoute: RouteConstants.login,
routes: {
RouteConstants.login: (context) => LoginPage(),
// Add other routes
},
onGenerateRoute: _generateRoute,
),
);
}
Route<dynamic>? _generateRoute(RouteSettings settings) {
// Handle dynamic routes
return null;
}
}Bloc Implementation
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'core/theme/app_theme.dart';
import 'core/constants/route_constants.dart';
import 'features/authentication/presentation/bloc/auth_bloc.dart';
import 'features/user_management/presentation/bloc/user_bloc.dart';
import 'features/authentication/presentation/pages/login_page.dart';
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider(create: (_) => sl<AuthBloc>()),
BlocProvider(create: (_) => sl<UserBloc>()),
],
child: MaterialApp(
title: 'Enterprise App',
theme: AppTheme.lightTheme,
darkTheme: AppTheme.darkTheme,
themeMode: ThemeMode.system,
initialRoute: RouteConstants.login,
routes: {
RouteConstants.login: (context) => LoginPage(),
// Add other routes
},
onGenerateRoute: _generateRoute,
),
);
}
Route<dynamic>? _generateRoute(RouteSettings settings) {
// Handle dynamic routes
return null;
}
}Riverpod Implementation
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'core/theme/app_theme.dart';
import 'core/constants/route_constants.dart';
import 'features/authentication/presentation/riverpod/auth_provider.dart';
import 'features/user_management/presentation/riverpod/user_provider.dart';
import 'features/authentication/presentation/pages/login_page.dart';
class MyApp extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return MaterialApp(
title: 'Enterprise App',
theme: AppTheme.lightTheme,
darkTheme: AppTheme.darkTheme,
themeMode: ThemeMode.system,
initialRoute: RouteConstants.login,
routes: {
RouteConstants.login: (context) => LoginPage(),
// Add other routes
},
onGenerateRoute: _generateRoute,
);
}
Route<dynamic>? _generateRoute(RouteSettings settings) {
// Handle dynamic routes
return null;
}
}
#### GetX Implementationimport 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'core/theme/app_theme.dart'; import 'core/constants/route_constants.dart'; import 'features/authentication/presentation/controllers/auth_controller.dart'; import 'features/user_management/presentation/controllers/user_controller.dart'; import 'features/authentication/presentation/pages/login_page.dart';
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return GetMaterialApp( title: 'Enterprise App', theme: AppTheme.lightTheme, darkTheme: AppTheme.darkTheme, themeMode: ThemeMode.system, initialRoute: RouteConstants.login, getPages: [ GetPage( name: RouteConstants.login, page: () => LoginPage(), binding: AuthBinding(), ), // Add other routes ], ); } }
app.dart (Bloc Implementation)
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'core/theme/app_theme.dart';
import 'core/constants/route_constants.dart';
import 'features/authentication/presentation/bloc/auth_bloc.dart';
import 'features/user_management/presentation/bloc/user_bloc.dart';
import 'features/authentication/presentation/pages/login_page.dart';
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider(create: (_) => sl<AuthBloc>()),
BlocProvider(create: (_) => sl<UserBloc>()),
],
child: MaterialApp(
title: 'Enterprise App',
theme: AppTheme.lightTheme,
darkTheme: AppTheme.darkTheme,
themeMode: ThemeMode.system,
initialRoute: RouteConstants.login,
routes: {
RouteConstants.login: (context) => LoginPage(),
// Add other routes
},
onGenerateRoute: _generateRoute,
),
);
}
Route<dynamic>? _generateRoute(RouteSettings settings) {
// Handle dynamic routes
return null;
}
}app.dart (Riverpod Implementation)
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'core/theme/app_theme.dart';
import 'core/constants/route_constants.dart';
import 'features/authentication/presentation/pages/login_page.dart';
class MyApp extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return MaterialApp(
title: 'Enterprise App',
theme: AppTheme.lightTheme,
darkTheme: AppTheme.darkTheme,
themeMode: ThemeMode.system,
initialRoute: RouteConstants.login,
routes: {
RouteConstants.login: (context) => LoginPage(),
// Add other routes
},
onGenerateRoute: _generateRoute,
);
}
Route<dynamic>? _generateRoute(RouteSettings settings) {
// Handle dynamic routes
return null;
}
}Constants Templates
app_constants.dart
class AppConstants {
// App info
static const String appName = 'Enterprise App';
static const String appVersion = '1.0.0';
// Pagination
static const int defaultPageSize = 20;
static const int maxPageSize = 100;
// Cache
static const Duration cacheTimeout = Duration(hours: 1);
static const Duration userCacheTimeout = Duration(minutes: 30);
// Validation
static const int minPasswordLength = 8;
static const int maxUsernameLength = 50;
static const int maxEmailLength = 100;
// UI
static const double defaultBorderRadius = 8.0;
static const double largeBorderRadius = 12.0;
static const double defaultPadding = 16.0;
static const double smallPadding = 8.0;
static const double largePadding = 24.0;
}api_constants.dart
class ApiConstants {
static const String baseUrl = 'https://api.example.com/v1';
// Endpoints
static const String auth = '/auth';
static const String login = '/auth/login';
static const String register = '/auth/register';
static const String logout = '/auth/logout';
static const String refreshToken = '/auth/refresh';
static const String users = '/users';
static const String userProfile = '/users/profile';
// Headers
static const String contentType = 'application/json';
static const String accept = 'application/json';
static const String authorization = 'Authorization';
// Timeouts
static const Duration connectTimeout = Duration(seconds: 30);
static const Duration receiveTimeout = Duration(seconds: 30);
static const Duration sendTimeout = Duration(seconds: 30);
}route_constants.dart
class RouteConstants {
static const String splash = '/splash';
static const String onboarding = '/onboarding';
static const String login = '/login';
static const String register = '/register';
static const String forgotPassword = '/forgot-password';
static const String home = '/home';
static const String profile = '/profile';
static const String settings = '/settings';
static const String userList = '/users';
static const String userDetail = '/users/:id';
static const String userForm = '/users/form';
}Error Handling Templates
exceptions.dart
abstract class AppException implements Exception {
final String message;
final String? code;
const AppException(this.message, [this.code]);
@override
String toString() => 'AppException: $message';
}
class ServerException extends AppException {
const ServerException(String message, [String? code]) : super(message, code);
}
class NetworkException extends AppException {
const NetworkException(String message, [String? code]) : super(message, code);
}
class ValidationException extends AppException {
const ValidationException(String message, [String? code]) : super(message, code);
}
class CacheException extends AppException {
const CacheException(String message, [String? code]) : super(message, code);
}
class AuthenticationException extends AppException {
const AuthenticationException(String message, [String? code]) : super(message, code);
}
class AuthorizationException extends AppException {
const AuthorizationException(String message, [String? code]) : super(message, code);
}failures.dart
abstract class Failure {
final String message;
final String? code;
const Failure(this.message, [this.code]);
}
class ServerFailure extends Failure {
const ServerFailure(String message, [String? code]) : super(message, code);
}
class NetworkFailure extends Failure {
const NetworkFailure(String message, [String? code]) : super(message, code);
}
class ValidationFailure extends Failure {
const ValidationFailure(String message, [String? code]) : super(message, code);
}
class CacheFailure extends Failure {
const CacheFailure(String message, [String? code]) : super(message, code);
}
class AuthenticationFailure extends Failure {
const AuthenticationFailure(String message, [String? code]) : super(message, code);
}
class AuthorizationFailure extends Failure {
const AuthorizationFailure(String message, [String? code]) : super(message, code);
}Dependency Injection Template
injection_container.dart
import 'package:get_it/get_it.dart';
import 'package:injectable/injectable.dart';
final GetIt sl = GetIt.instance;
@InjectableInit()
Future<void> configureDependencies() async {
await sl.init();
}pubspec.yaml Template
name: enterprise_app
description: Flutter Enterprise Application
version: 1.0.0+1
environment:
sdk: '>=3.0.0 <4.0.0'
flutter: ">=3.10.0"
dependencies:
flutter:
sdk: flutter
# State Management (Choose based on preference)
provider: ^6.1.1 # For Provider pattern
flutter_bloc: ^8.1.3 # For Bloc pattern
flutter_riverpod: ^2.4.9 # For Riverpod pattern
get: ^4.6.6 # For GetX pattern
# Dependency Injection
get_it: ^7.6.4
injectable: ^2.3.2
# Network
dio: ^5.3.2
retrofit: ^4.0.3
# Local Storage
shared_preferences: ^2.2.2
hive: ^2.2.3
hive_flutter: ^1.1.0
# JSON Serialization
json_annotation: ^4.8.1
# Code Generation
freezed_annotation: ^2.4.1
equatable: ^2.0.5
# UI
cupertino_icons: ^1.0.2
# Utils
intl: ^0.18.1
uuid: ^4.2.1
# Navigation
go_router: ^12.1.3
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^3.0.0
# Code Generation
build_runner: ^2.4.6
json_serializable: ^6.7.1
freezed: ^2.4.6
injectable_generator: ^2.4.1
retrofit_generator: ^8.0.4
# Testing
mockito: ^5.4.2
bloc_test: ^9.1.4 # For Bloc testing
flutter_riverpod: ^2.4.9 # For Riverpod testing
get_test: ^3.0.0 # For GetX testing
integration_test:
sdk: flutter
flutter:
uses-material-design: true
assets:
- assets/images/
- assets/data/
fonts:
- family: Roboto
fonts:
- asset: assets/fonts/Roboto-Regular.ttf
- asset: assets/fonts/Roboto-Bold.ttf
weight: 700analysis_options.yaml Template
include: package:flutter_lints/flutter.yaml
analyzer:
exclude:
- "**/*.g.dart"
- "**/*.freezed.dart"
errors:
invalid_annotation_target: ignore
linter:
rules:
# Enable additional lint rules
prefer_single_quotes: true
sort_constructors_first: true
sort_unnamed_constructors_first: true
always_declare_return_types: true
avoid_print: true
avoid_unnecessary_containers: true
sized_box_for_whitespace: true
use_key_in_widget_constructors: true
prefer_const_constructors: true
prefer_const_literals_to_create_immutables: true
prefer_const_declarations: true
prefer_final_fields: true
prefer_final_locals: true
avoid_redundant_argument_values: true
avoid_types_on_closure_parameters: true
avoid_function_literals_in_foreach_calls: true
avoid_returning_null_for_void: true
prefer_is_empty: true
prefer_is_not_empty: true
unnecessary_const: true
unnecessary_new: true
prefer_if_null_operators: true
prefer_null_aware_operators: trueSetup Commands
Initial Setup
# Create new Flutter project
flutter create enterprise_app --org com.example
# Navigate to project
cd enterprise_app
# Add dependencies
flutter pub add provider flutter_bloc flutter_riverpod get_it injectable dio retrofit shared_preferences hive hive_flutter json_annotation freezed_annotation equatable cupertino_icons intl uuid go_router
# Add dev dependencies
flutter pub add --dev flutter_test flutter_lints build_runner json_serializable freezed injectable_generator retrofit_generator mockito bloc_test integration_test
# Create directory structure
mkdir -p lib/core/{constants,errors,network,utils,widgets,theme,di}
mkdir -p lib/features/authentication/{data/{datasources,models,repositories},domain/{entities,repositories,usecases},presentation/{pages,widgets,state_management/{providers,bloc,riverpod}}}
mkdir -p lib/features/user_management/{data/{datasources,models,repositories},domain/{entities,repositories,usecases},presentation/{pages,widgets,state_management/{providers,bloc,riverpod}}}
mkdir -p test/{fixtures,helpers,mocks,unit/{core,features}}
mkdir -p integration_test
mkdir -p assets/{images,fonts,data}Code Generation
# Generate code
flutter pub run build_runner build --delete-conflicting-outputs
# Watch mode for development
flutter pub run build_runner watch --delete-conflicting-outputsState Management Selection Guide
Choosing Your State Management Solution
Provider Pattern
- When to use: Simple to medium complexity apps, teams familiar with Provider
- Pros: Lightweight, easy to learn, good performance
- Cons: Manual boilerplate for state comparison
- Dependencies:
provider: ^6.1.1
Bloc Pattern
- When to use: Complex apps with strict state management requirements
- Pros: Predictable state, excellent tooling, strong separation of concerns
- Cons: More boilerplate, steeper learning curve
- Dependencies:
flutter_bloc: ^8.1.3,equatable: ^2.0.5
Riverpod Pattern
- When to use: Modern apps needing compile-time safety and flexibility
- Pros: Compile-time safety, great testing support, flexible providers
- Cons: Newer ecosystem, requires mindset shift
- Dependencies:
flutter_riverpod: ^2.4.9
GetX Pattern
- When to use: Apps requiring minimal boilerplate and high performance
- Pros: Minimal boilerplate, excellent performance, simple syntax, built-in dependency injection
- Cons: Less conventional, magic-based approach can hide complexity
- Dependencies:
get: ^4.6.6
Migration Paths
All four patterns can be easily migrated between since they share the same clean architecture foundation:
1. Keep Domain Layer: Entities, repositories, and use cases remain unchanged 2. Adapt Presentation Layer: Only state management implementation changes 3. Update Dependency Injection: Register appropriate providers/blocs/notifiers 4. Update Testing: Use corresponding testing libraries and patterns
Implementation Notes
- Choose one state management solution per project
- All examples in documentation are functionally equivalent
- Testing patterns are provided for each approach
- Directory structure supports all four patterns simultaneously
This template provides a comprehensive foundation for enterprise Flutter applications with clean architecture and feature-based structure, supporting all major state management approaches.
Clean Architecture Patterns for Flutter
Overview
Clean Architecture is a software design philosophy that separates concerns into distinct layers, making the system more maintainable, testable, and independent of external frameworks.
Layer Structure
1. Domain Layer (Core Business Logic)
Purpose: Contains the core business logic and rules of the application. This layer should be completely independent of any external frameworks or UI.
Components:
- Entities: Core business objects with no dependencies
- Repository Interfaces: Abstract contracts for data access
- Use Cases: Application-specific business rules
// Entity Example
class User {
final String id;
final String name;
final String email;
final DateTime createdAt;
User({
required this.id,
required this.name,
required this.email,
required this.createdAt,
});
// Business logic methods
bool isValidEmail() {
return email.contains('@');
}
}
// Repository Interface Example
abstract class UserRepository {
Future<List<User>> getUsers();
Future<User> getUserById(String id);
Future<void> createUser(User user);
Future<void> updateUser(User user);
Future<void> deleteUser(String id);
}
// Use Case Example
class GetUserByIdUseCase {
final UserRepository _repository;
GetUserByIdUseCase(this._repository);
Future<User?> call(String userId) async {
if (userId.isEmpty) {
throw ArgumentError('User ID cannot be empty');
}
return await _repository.getUserById(userId);
}
}2. Data Layer (Data Implementation)
Purpose: Implements the repository interfaces defined in the domain layer. Handles data sources, caching, and data transformation.
Components:
- Data Sources: Remote and local data access
- Models: Data transfer objects
- Repository Implementations: Concrete implementations of domain repositories
// Data Source Example
abstract class UserRemoteDataSource {
Future<List<UserModel>> getUsers();
Future<UserModel?> getUserById(String id);
Future<void> createUser(UserModel user);
}
class UserRemoteDataSourceImpl implements UserRemoteDataSource {
final HttpClient httpClient;
UserRemoteDataSourceImpl(this.httpClient);
@override
Future<List<UserModel>> getUsers() async {
final response = await httpClient.get('/users');
return (response.data as List)
.map((json) => UserModel.fromJson(json))
.toList();
}
@override
Future<UserModel?> getUserById(String id) async {
final response = await httpClient.get('/users/$id');
return UserModel.fromJson(response.data);
}
}
// Model Example
class UserModel {
final String id;
final String name;
final String email;
final String createdAt;
UserModel({
required this.id,
required this.name,
required this.email,
required this.createdAt,
});
factory UserModel.fromJson(Map<String, dynamic> json) {
return UserModel(
id: json['id'],
name: json['name'],
email: json['email'],
createdAt: json['created_at'],
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'email': email,
'created_at': createdAt,
};
}
// Convert to domain entity
User toEntity() {
return User(
id: id,
name: name,
email: email,
createdAt: DateTime.parse(createdAt),
);
}
// Convert from domain entity
factory UserModel.fromEntity(User user) {
return UserModel(
id: user.id,
name: user.name,
email: user.email,
createdAt: user.createdAt.toIso8601String(),
);
}
}
// Repository Implementation Example
class UserRepositoryImpl implements UserRepository {
final UserRemoteDataSource remoteDataSource;
final UserLocalDataSource localDataSource;
final NetworkInfo networkInfo;
UserRepositoryImpl({
required this.remoteDataSource,
required this.localDataSource,
required this.networkInfo,
});
@override
Future<List<User>> getUsers() async {
if (await networkInfo.isConnected) {
try {
final remoteUsers = await remoteDataSource.getUsers();
await localDataSource.cacheUsers(remoteUsers);
return remoteUsers.map((model) => model.toEntity()).toList();
} on ServerException {
// Fallback to local cache if server fails
return await _getCachedUsers();
}
} else {
return await _getCachedUsers();
}
}
Future<List<User>> _getCachedUsers() async {
final cachedUsers = await localDataSource.getCachedUsers();
return cachedUsers.map((model) => model.toEntity()).toList();
}
}3. Presentation Layer (UI and State Management)
Purpose: Handles UI presentation and user interaction. Depends on the domain layer through use cases.
Components:
- Pages: Full-screen UI components
- Widgets: Reusable UI components
- State Management: Provider/Bloc/Riverpod/GetX (choose based on preference)
Provider Implementation Example
// Provider Example
class UserProvider extends ChangeNotifier {
final GetUserByIdUseCase getUserById;
final GetUsersUseCase getUsers;
User? _currentUser;
List<User> _users = [];
bool _isLoading = false;
String? _error;
UserProvider({
required this.getUserById,
required this.getUsers,
});
// Getters
User? get currentUser => _currentUser;
List<User> get users => _users;
bool get isLoading => _isLoading;
String? get error => _error;
// Methods
Future<void> loadUser(String userId) async {
_isLoading = true;
_error = null;
notifyListeners();
try {
_currentUser = await getUserById(userId);
} catch (e) {
_error = e.toString();
} finally {
_isLoading = false;
notifyListeners();
}
}
Future<void> loadUsers() async {
_isLoading = true;
_error = null;
notifyListeners();
try {
_users = await getUsers();
} catch (e) {
_error = e.toString();
} finally {
_isLoading = false;
notifyListeners();
}
}
}
// Page Example with Provider
class UserListPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => UserProvider(
getUserById: context.read<GetUserByIdUseCase>(),
getUsers: context.read<GetUsersUseCase>(),
),
child: UserListView(),
);
}
}
class UserListView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Users')),
body: Consumer<UserProvider>(
builder: (context, provider, child) {
if (provider.isLoading) {
return Center(child: CircularProgressIndicator());
}
if (provider.error != null) {
return Center(
child: Text('Error: ${provider.error}'),
);
}
return ListView.builder(
itemCount: provider.users.length,
itemBuilder: (context, index) {
final user = provider.users[index];
return UserTile(user: user);
},
);
},
),
);
}
}Bloc Implementation Example
// Bloc Events
abstract class UserEvent extends Equatable {
const UserEvent();
@override
List<Object> get props => [];
}
class LoadUsers extends UserEvent {}
class LoadUserById extends UserEvent {
final String userId;
const LoadUserById(this.userId);
@override
List<Object> get props => [userId];
}
// Bloc States
abstract class UserState extends Equatable {
const UserState();
@override
List<Object> get props => [];
}
class UserInitial extends UserState {}
class UserLoading extends UserState {}
class UserLoaded extends UserState {
final List<User> users;
const UserLoaded(this.users);
@override
List<Object> get props => [users];
}
class UserError extends UserState {
final String message;
const UserError(this.message);
@override
List<Object> get props => [message];
}
// Bloc Implementation
class UserBloc extends Bloc<UserEvent, UserState> {
final GetUsersUseCase getUsers;
final GetUserByIdUseCase getUserById;
UserBloc({
required this.getUsers,
required this.getUserById,
}) : super(UserInitial()) {
on<LoadUsers>(_onLoadUsers);
on<LoadUserById>(_onLoadUserById);
}
Future<void> _onLoadUsers(LoadUsers event, Emitter<UserState> emit) async {
emit(UserLoading());
try {
final users = await getUsers();
emit(UserLoaded(users));
} catch (e) {
emit(UserError(e.toString()));
}
}
Future<void> _onLoadUserById(LoadUserById event, Emitter<UserState> emit) async {
emit(UserLoading());
try {
final user = await getUserById(event.userId);
emit(UserLoaded([user]));
} catch (e) {
emit(UserError(e.toString()));
}
}
}
// Page Example with Bloc
class UserListPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => UserBloc(
getUsers: context.read<GetUsersUseCase>(),
getUserById: context.read<GetUserByIdUseCase>(),
),
child: UserListView(),
);
}
}
class UserListView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Users')),
body: BlocBuilder<UserBloc, UserState>(
builder: (context, state) {
if (state is UserLoading) {
return Center(child: CircularProgressIndicator());
}
if (state is UserError) {
return Center(
child: Text('Error: ${state.message}'),
);
}
if (state is UserLoaded) {
return ListView.builder(
itemCount: state.users.length,
itemBuilder: (context, index) {
final user = state.users[index];
return UserTile(user: user);
},
);
}
return Container();
},
),
);
}
}Riverpod Implementation Example
// State Notifier with Riverpod
class UserNotifier extends StateNotifier<AsyncValue<List<User>>> {
final GetUsersUseCase getUsers;
final GetUserByIdUseCase getUserById;
UserNotifier({
required this.getUsers,
required this.getUserById,
}) : super(const AsyncValue.loading());
Future<void> loadUsers() async {
state = const AsyncValue.loading();
try {
final users = await getUsers();
state = AsyncValue.data(users);
} catch (e, stackTrace) {
state = AsyncValue.error(e, stackTrace);
}
}
Future<void> loadUserById(String userId) async {
state = const AsyncValue.loading();
try {
final user = await getUserById(userId);
state = AsyncValue.data([user]);
} catch (e, stackTrace) {
state = AsyncValue.error(e, stackTrace);
}
}
}
// Providers
final userProvider = StateNotifierProvider<UserNotifier, AsyncValue<List<User>>>(
(ref) => UserNotifier(
getUsers: ref.watch(getUsersUseCaseProvider),
getUserById: ref.watch(getUserByIdUseCaseProvider),
),
);
// Use Case Providers
final getUsersUseCaseProvider = Provider<GetUsersUseCase>(
(ref) => GetUsersUseCase(ref.watch(userRepositoryProvider)),
);
final getUserByIdUseCaseProvider = Provider<GetUserByIdUseCase>(
(ref) => GetUserByIdUseCase(ref.watch(userRepositoryProvider)),
);
// Page Example with Riverpod
class UserListPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final userState = ref.watch(userProvider);
return Scaffold(
appBar: AppBar(title: Text('Users')),
body: userState.when(
loading: () => Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(
child: Text('Error: ${error.toString()}'),
),
data: (users) => ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
final user = users[index];
return UserTile(user: user);
},
),
),
);
}
}GetX Implementation Example
// GetX Controller Example
class UserController extends GetxController {
final GetUserByIdUseCase getUserById;
final GetUsersUseCase getUsers;
UserController({
required this.getUserById,
required this.getUsers,
});
// Reactive state variables
final Rx<User?> _currentUser = Rx<User?>(null);
final RxList<User> _users = <User>[].obs;
final RxBool _isLoading = false.obs;
final RxString _error = ''.obs;
// Getters for reactive variables
User? get currentUser => _currentUser.value;
List<User> get users => _users;
bool get isLoading => _isLoading.value;
String? get error => _error.value.isEmpty ? null : _error.value;
// Methods
Future<void> loadUser(String userId) async {
_isLoading.value = true;
_error.value = '';
try {
_currentUser.value = await getUserById(userId);
} catch (e) {
_error.value = e.toString();
} finally {
_isLoading.value = false;
}
}
Future<void> loadUsers() async {
_isLoading.value = true;
_error.value = '';
try {
final userList = await getUsers();
_users.assignAll(userList);
} catch (e) {
_error.value = e.toString();
} finally {
_isLoading.value = false;
}
}
}
// Page Example with GetX
class UserListPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Initialize controller with dependencies
final UserController controller = Get.put(
UserController(
getUserById: Get.find(),
getUsers: Get.find(),
),
);
return Scaffold(
appBar: AppBar(title: Text('Users')),
body: Obx(() {
if (controller.isLoading) {
return Center(child: CircularProgressIndicator());
}
if (controller.error != null) {
return Center(
child: Text('Error: ${controller.error}'),
);
}
return ListView.builder(
itemCount: controller.users.length,
itemBuilder: (context, index) {
final user = controller.users[index];
return UserTile(user: user);
},
);
}),
);
}
}
// Dependency Injection with GetX
class UserBinding extends Bindings {
@override
void dependencies() {
// Use cases
Get.lazyPut<GetUserByIdUseCase>(
() => GetUserByIdUseCase(Get.find()),
);
Get.lazyPut<GetUsersUseCase>(
() => GetUsersUseCase(Get.find()),
);
// Controller
Get.lazyPut<UserController>(
() => UserController(
getUserById: Get.find(),
getUsers: Get.find(),
),
);
}
}Dependency Rules
1. Inner layers cannot depend on outer layers 2. Dependencies point inward 3. Outer layers can depend on inner layers 4. Domain layer has no dependencies
Presentation → Domain ← DataBenefits
1. Testability: Business logic is isolated from UI and frameworks 2. Maintainability: Clear separation of concerns 3. Flexibility: Easy to swap implementations (e.g., change database) 4. Scalability: Features can be developed independently 5. Reusability: Domain logic can be reused across different platforms
Common Pitfalls to Avoid
1. Don't pass UI models to domain layer 2. Don't import Flutter framework in domain layer 3. Don't put business logic in presentation layer 4. Don't make repositories depend on concrete implementations 5. Don't skip error handling in use cases
Testing Strategy
Domain Layer Testing
class GetUserByIdUseCaseTest {
late GetUserByIdUseCase useCase;
late MockUserRepository mockRepository;
setUp() {
mockRepository = MockUserRepository();
useCase = GetUserByIdUseCase(mockRepository);
}
test('should get user from repository', () async {
// Arrange
const userId = '1';
final testUser = User(
id: userId,
name: 'Test User',
email: 'test@example.com',
createdAt: DateTime.now(),
);
when(mockRepository.getUserById(userId))
.thenAnswer((_) async => testUser);
// Act
final result = await useCase(userId);
// Assert
expect(result, testUser);
verify(mockRepository.getUserById(userId));
});
}Data Layer Testing
class UserRepositoryImplTest {
late UserRepositoryImpl repository;
late MockUserRemoteDataSource mockRemoteDataSource;
late MockUserLocalDataSource mockLocalDataSource;
late MockNetworkInfo mockNetworkInfo;
setUp() {
mockRemoteDataSource = MockUserRemoteDataSource();
mockLocalDataSource = MockUserLocalDataSource();
mockNetworkInfo = MockNetworkInfo();
repository = UserRepositoryImpl(
remoteDataSource: mockRemoteDataSource,
localDataSource: mockLocalDataSource,
networkInfo: mockNetworkInfo,
);
}
test('should return remote data when device is online', () async {
// Arrange
when(mockNetworkInfo.isConnected).thenAnswer((_) async => true);
final testModels = [UserModel(id: '1', name: 'Test', email: 'test@test.com', createdAt: '2023-01-01')];
when(mockRemoteDataSource.getUsers()).thenAnswer((_) async => testModels);
// Act
final result = await repository.getUsers();
// Assert
expect(result, isA<List<User>>());
verify(mockRemoteDataSource.getUsers());
verify(mockLocalDataSource.cacheUsers(testModels));
});
}Code Generation for Flutter Enterprise Apps
Overview
Code generation reduces boilerplate and ensures consistency across enterprise Flutter applications. This guide covers essential code generation tools and patterns for clean architecture.
Essential Code Generation Tools
1. JSON Serialization
Setup
# pubspec.yaml
dependencies:
json_annotation: ^4.8.1
dev_dependencies:
build_runner: ^2.4.6
json_serializable: ^6.7.1Model Template with Code Generation
// lib/features/user/data/models/user_model.dart
import 'package:json_annotation/json_annotation.dart';
import '../../domain/entities/user_entity.dart';
part 'user_model.g.dart';
@JsonSerializable()
class UserModel {
final String id;
final String name;
final String email;
final String? avatar;
@JsonKey(name: 'created_at')
final String createdAt;
@JsonKey(name: 'updated_at')
final String updatedAt;
const UserModel({
required this.id,
required this.name,
required this.email,
this.avatar,
required this.createdAt,
required this.updatedAt,
});
factory UserModel.fromJson(Map<String, dynamic> json) =>
_$UserModelFromJson(json);
Map<String, dynamic> toJson() => _$UserModelToJson(this);
UserEntity toEntity() {
return UserEntity(
id: id,
name: name,
email: email,
avatar: avatar,
createdAt: DateTime.parse(createdAt),
updatedAt: DateTime.parse(updatedAt),
);
}
factory UserModel.fromEntity(UserEntity entity) {
return UserModel(
id: entity.id,
name: entity.name,
email: entity.email,
avatar: entity.avatar,
createdAt: entity.createdAt.toIso8601String(),
updatedAt: entity.updatedAt.toIso8601String(),
);
}
}Build Command
flutter pub run build_runner build --delete-conflicting-outputs2. Freezed for Immutable Classes
Setup
# pubspec.yaml
dependencies:
freezed_annotation: ^2.4.1
dev_dependencies:
build_runner: ^2.4.6
freezed: ^2.4.6Entity Template with Freezed
// lib/features/user/domain/entities/user_entity.dart
import 'package:freezed_annotation/freezed_annotation.dart';
part 'user_entity.freezed.dart';
@freezed
class UserEntity with _$UserEntity {
const factory UserEntity({
required String id,
required String name,
required String email,
String? avatar,
required DateTime createdAt,
required DateTime updatedAt,
}) = _UserEntity;
const UserEntity._();
bool get isValidEmail => email.contains('@');
}Response Model Template
// lib/core/models/api_response_model.dart
import 'package:freezed_annotation/freezed_annotation.dart';
part 'api_response_model.freezed.dart';
part 'api_response_model.g.dart';
@freezed
class ApiResponse<T> with _$ApiResponse<T> {
const factory ApiResponse({
required bool success,
required T data,
String? message,
@JsonKey(name: 'error_code') int? errorCode,
}) = _ApiResponse<T>;
factory ApiResponse.fromJson(
Map<String, dynamic> json,
T Function(Object? json) fromJsonT,
) =>
_$ApiResponseFromJson(json, fromJsonT);
}
@freezed
class PaginatedResponse<T> with _$PaginatedResponse<T> {
const factory PaginatedResponse({
required List<T> data,
required int page,
required int limit,
required int total,
@JsonKey(name: 'total_pages') required int totalPages,
}) = _PaginatedResponse<T>;
factory PaginatedResponse.fromJson(
Map<String, dynamic> json,
T Function(Object? json) fromJsonT,
) =>
_$PaginatedResponseFromJson(json, fromJsonT);
}3. Injectable for Dependency Injection
Setup
# pubspec.yaml
dependencies:
get_it: ^7.6.4
injectable: ^2.3.2
dev_dependencies:
build_runner: ^2.4.6
injectable_generator: ^2.4.1Dependency Injection Setup
// lib/core/di/injection_container.dart
import 'package:get_it/get_it.dart';
import 'package:injectable/injectable.dart';
final GetIt sl = GetIt.instance;
@InjectableInit()
Future<void> configureDependencies() async {
sl.init();
}Repository with Injectable
// lib/features/user/data/repositories/user_repository_impl.dart
import 'package:injectable/injectable.dart';
import '../../domain/repositories/user_repository.dart';
import '../datasources/user_remote_data_source.dart';
import '../datasources/user_local_data_source.dart';
import '../../../core/network/network_info.dart';
@LazySingleton(as: UserRepository)
class UserRepositoryImpl implements UserRepository {
final UserRemoteDataSource remoteDataSource;
final UserLocalDataSource localDataSource;
final NetworkInfo networkInfo;
UserRepositoryImpl({
required this.remoteDataSource,
required this.localDataSource,
required this.networkInfo,
});
// Implementation...
}Use Case with Injectable
// lib/features/user/domain/usecases/get_users_usecase.dart
import 'package:injectable/injectable.dart';
import '../repositories/user_repository.dart';
@LazySingleton()
class GetUsersUseCase {
final UserRepository _repository;
GetUsersUseCase(this._repository);
Future<List<UserEntity>> call({int page = 1, int limit = 20}) async {
return await _repository.getUsers(page: page, limit: limit);
}
}Provider with Injectable
// lib/features/user/presentation/providers/user_provider.dart
import 'package:injectable/injectable.dart';
import '../../domain/usecases/get_users_usecase.dart';
import '../../domain/usecases/create_user_usecase.dart';
import '../../domain/usecases/search_users_usecase.dart';
@LazySingleton()
class UserProvider extends ChangeNotifier {
final GetUsersUseCase _getUsersUseCase;
final CreateUserUseCase _createUserUseCase;
final SearchUsersUseCase _searchUsersUseCase;
UserProvider({
required GetUsersUseCase getUsersUseCase,
required CreateUserUseCase createUserUseCase,
required SearchUsersUseCase searchUsersUseCase,
}) : _getUsersUseCase = getUsersUseCase,
_createUserUseCase = createUserUseCase,
_searchUsersUseCase = searchUsersUseCase;
// Implementation...
}4. Retrofit for API Clients
Setup
# pubspec.yaml
dependencies:
retrofit: ^4.0.3
dio: ^5.3.2
json_annotation: ^4.8.1
dev_dependencies:
build_runner: ^2.4.6
retrofit_generator: ^8.0.4
json_serializable: ^6.7.1API Client Template
// lib/core/network/api_client.dart
import 'package:retrofit/retrofit.dart';
import 'package:dio/dio.dart';
import '../models/api_response_model.dart';
import '../models/paginated_response_model.dart';
import '../../features/user/data/models/user_model.dart';
part 'api_client.g.dart';
@RestApi(baseUrl: 'https://api.example.com/v1/')
abstract class ApiClient {
factory ApiClient(Dio dio, {String baseUrl}) = _ApiClient;
@GET('/users')
Future<ApiResponse<PaginatedResponse<UserModel>>> getUsers(
@Query('page') int page,
@Query('limit') int limit,
);
@GET('/users/{id}')
Future<ApiResponse<UserModel>> getUserById(@Path('id') String id);
@POST('/users')
Future<ApiResponse<UserModel>> createUser(@Body() UserModel user);
@PUT('/users/{id}')
Future<ApiResponse<UserModel>> updateUser(
@Path('id') String id,
@Body() UserModel user,
);
@DELETE('/users/{id}')
Future<ApiResponse<void>> deleteUser(@Path('id') String id);
@GET('/users/search')
Future<ApiResponse<List<UserModel>>> searchUsers(@Query('q') String query);
}5. Equatable for Value Equality
Setup
# pubspec.yaml
dependencies:
equatable: ^2.0.5Entity with Equatable
// lib/features/user/domain/entities/user_entity.dart
import 'package:equatable/equatable.dart';
class UserEntity extends Equatable {
final String id;
final String name;
final String email;
final String? avatar;
final DateTime createdAt;
final DateTime updatedAt;
const UserEntity({
required this.id,
required this.name,
required this.email,
this.avatar,
required this.createdAt,
required this.updatedAt,
});
@override
List<Object?> get props => [
id,
name,
email,
avatar,
createdAt,
updatedAt,
];
UserEntity copyWith({
String? id,
String? name,
String? email,
String? avatar,
DateTime? createdAt,
DateTime? updatedAt,
}) {
return UserEntity(
id: id ?? this.id,
name: name ?? this.name,
email: email ?? this.email,
avatar: avatar ?? this.avatar,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
);
}
bool get isValidEmail => email.contains('@');
}6. Mock Generation for Testing
Setup
# pubspec.yaml
dev_dependencies:
mockito: ^5.4.2
build_runner: ^2.4.6
mockito_generator: ^5.4.2Mock Classes
// test/features/user/domain/repositories/user_repository_test.dart
import 'package:mockito/annotations.dart';
import '../../../../../lib/features/user/domain/repositories/user_repository.dart';
@GenerateMocks([UserRepository])
void main() {}Build Scripts
Development Build Script
# build.yaml
targets:
$default:
builders:
json_serializable:
options:
explicit_to_json: true
include_if_null: false
retrofit_generator:
options:
null_safety: true
injectable_generator:
options:
auto_register: true
as_extension: trueWatch Command for Development
flutter pub run build_runner watch --delete-conflicting-outputsBuild Commands for CI/CD
# Generate code
flutter pub run build_runner build --delete-conflicting-outputs
# Clean generated files
flutter pub run build_runner clean
# Generate and validate
flutter pub run build_runner build --delete-conflicting-outputs && flutter analyzeCode Generation Workflow
1. Initial Setup
# Add dependencies
flutter pub add json_annotation freezed_annotation injectable retrofit dio equatable
flutter pub add --dev build_runner json_serializable freezed injectable_generator retrofit_generator mockito
# Generate initial code
flutter pub run build_runner build --delete-conflicting-outputs2. Development Workflow
# Start watch mode
flutter pub run build_runner watch --delete-conflicting-outputs
# Make changes to files
# Code will be generated automatically3. Before Commit
# Clean and regenerate
flutter pub run build_runner clean
flutter pub run build_runner build --delete-conflicting-outputs
# Run analysis
flutter analyze
# Run tests
flutter testCustom Code Generation Templates
Feature Generator Script
// scripts/generate_feature.dart
import 'dart:io';
void main() {
if (args.length != 2) {
print('Usage: dart scripts/generate_feature.dart <feature_name> <entity_name>');
exit(1);
}
final featureName = args[0];
final entityName = args[1];
generateFeature(featureName, entityName);
}
void generateFeature(String featureName, String entityName) {
final featureDir = Directory('lib/features/$featureName');
featureDir.createSync(recursive: true);
// Create directory structure
final directories = [
'data/datasources',
'data/models',
'data/repositories',
'domain/entities',
'domain/repositories',
'domain/usecases',
'presentation/pages',
'presentation/widgets',
'presentation/providers',
];
for (final dir in directories) {
Directory('${featureDir.path}/$dir').createSync(recursive: true);
}
// Generate entity
generateEntity(featureDir, entityName);
// Generate repository interface
generateRepositoryInterface(featureDir, entityName);
// Generate use cases
generateUseCases(featureDir, entityName);
// Generate model
generateModel(featureDir, entityName);
// Generate repository implementation
generateRepositoryImpl(featureDir, entityName);
// Generate provider
generateProvider(featureDir, entityName);
print('Feature "$featureName" generated successfully!');
}Entity Template Generator
void generateEntity(Directory featureDir, String entityName) {
final entityFile = File('${featureDir.path}/domain/entities/${entityName.toLowerCase()}_entity.dart');
final entityContent = '''
import 'package:freezed_annotation/freezed_annotation.dart';
part '${entityName.toLowerCase()}_entity.freezed.dart';
@freezed
class ${entityName}Entity with _\$${entityName}Entity {
const factory ${entityName}Entity({
required String id,
required String name,
required DateTime createdAt,
required DateTime updatedAt,
}) = _\$${entityName}Entity;
const ${entityName}Entity._();
}
''';
entityFile.writeAsStringSync(entityContent);
}Best Practices
1. Consistent Naming: Use consistent naming conventions across generated files 2. Version Control: Add generated files to .gitignore but keep templates 3. CI/CD Integration: Include code generation in build pipeline 4. Documentation: Document code generation setup and commands 5. Performance: Use watch mode during development, build mode for production 6. Validation: Validate generated code with analysis and tests 7. Clean Builds: Clean and regenerate when switching branches 8. Team Coordination: Ensure team uses same code generation versions
Troubleshooting
Common Issues
1. Conflicting Classes: Use --delete-conflicting-outputs flag 2. Import Errors: Check pubspec.yaml dependencies 3. Generation Failures: Clean with flutter pub run build_runner clean 4. Version Conflicts: Ensure compatible package versions 5. Cache Issues: Clear pub cache with flutter pub cache repair
Debug Commands
# Check what's being generated
flutter pub run build_runner build --verbose
# Clean everything
flutter pub run build_runner clean
flutter pub get
# Force regeneration
flutter pub run build_runner build --delete-conflicting-outputsComprehensive Error Handling for Flutter Enterprise Apps
Overview
Effective error handling is crucial for enterprise applications to ensure reliability, maintainability, and good user experience. This guide covers comprehensive error handling strategies for Flutter apps using clean architecture.
Error Handling Architecture
Error Types
// lib/core/errors/exceptions.dart
abstract class AppException implements Exception {
final String message;
final String? code;
final dynamic details;
const AppException(this.message, [this.code, this.details]);
@override
String toString() => 'AppException: $message';
}
class ServerException extends AppException {
const ServerException(String message, [String? code, dynamic details])
: super(message, code, details);
}
class NetworkException extends AppException {
const NetworkException(String message, [String? code, dynamic details])
: super(message, code, details);
}
class ValidationException extends AppException {
const ValidationException(String message, [String? code, dynamic details])
: super(message, code, details);
}
class CacheException extends AppException {
const CacheException(String message, [String? code, dynamic details])
: super(message, code, details);
}
class AuthenticationException extends AppException {
const AuthenticationException(String message, [String? code, dynamic details])
: super(message, code, details);
}
class AuthorizationException extends AppException {
const AuthorizationException(String message, [String? code, dynamic details])
: super(message, code, details);
}
class TimeoutException extends AppException {
const TimeoutException(String message, [String? code, dynamic details])
: super(message, code, details);
}
class UnknownException extends AppException {
const UnknownException(String message, [String? code, dynamic details])
: super(message, code, details);
}Failure Classes
// lib/core/errors/failures.dart
abstract class Failure {
final String message;
final String? code;
final dynamic details;
final DateTime timestamp;
const Failure(this.message, [this.code, this.details])
: timestamp = DateTime.now();
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is Failure &&
other.message == message &&
other.code == code &&
other.details == details;
}
@override
int get hashCode {
return message.hashCode ^
code.hashCode ^
details.hashCode;
}
}
class ServerFailure extends Failure {
const ServerFailure(String message, [String? code, dynamic details])
: super(message, code, details);
}
class NetworkFailure extends Failure {
const NetworkFailure(String message, [String? code, dynamic details])
: super(message, code, details);
}
class ValidationFailure extends Failure {
const ValidationFailure(String message, [String? code, dynamic details])
: super(message, code, details);
}
class CacheFailure extends Failure {
const CacheFailure(String message, [String? code, dynamic details])
: super(message, code, details);
}
class AuthenticationFailure extends Failure {
const AuthenticationFailure(String message, [String? code, dynamic details])
: super(message, code, details);
}
class AuthorizationFailure extends Failure {
const AuthorizationFailure(String message, [String? code, dynamic details])
: super(message, code, details);
}
class TimeoutFailure extends Failure {
const TimeoutFailure(String message, [String? code, dynamic details])
: super(message, code, details);
}
class UnknownFailure extends Failure {
const UnknownFailure(String message, [String? code, dynamic details])
: super(message, code, details);
}Centralized Error Handler
// lib/core/errors/error_handler.dart
import 'package:dio/dio.dart';
import '../network/network_info.dart';
import 'exceptions.dart';
import 'failures.dart';
class ErrorHandler {
final NetworkInfo networkInfo;
const ErrorHandler(this.networkInfo);
Failure handleException(Exception exception) {
if (exception is DioException) {
return _handleDioException(exception);
} else if (exception is AppException) {
return _handleAppException(exception);
} else {
return UnknownFailure(exception.toString());
}
}
Failure _handleDioException(DioException exception) {
switch (exception.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
return TimeoutFailure('Request timeout', 'TIMEOUT');
case DioExceptionType.connectionError:
return NetworkFailure('No internet connection', 'NO_INTERNET');
case DioExceptionType.badResponse:
return _handleHttpError(exception);
case DioExceptionType.cancel:
return NetworkFailure('Request cancelled', 'CANCELLED');
default:
return UnknownFailure('Network error: ${exception.message}');
}
}
Failure _handleHttpError(DioException exception) {
final statusCode = exception.response?.statusCode;
final message = exception.response?.data?['message'] ?? 'Unknown error';
switch (statusCode) {
case 400:
return ValidationFailure(message, 'BAD_REQUEST');
case 401:
return AuthenticationFailure(message, 'UNAUTHORIZED');
case 403:
return AuthorizationFailure(message, 'FORBIDDEN');
case 404:
return ServerFailure('Resource not found', 'NOT_FOUND');
case 422:
return ValidationFailure(message, 'VALIDATION_ERROR');
case 429:
return ServerFailure('Too many requests', 'RATE_LIMIT');
case 500:
case 502:
case 503:
return ServerFailure(message, 'SERVER_ERROR');
default:
return ServerFailure(message, statusCode.toString());
}
}
Failure _handleAppException(AppException exception) {
if (exception is ServerException) {
return ServerFailure(exception.message, exception.code, exception.details);
} else if (exception is NetworkException) {
return NetworkFailure(exception.message, exception.code, exception.details);
} else if (exception is ValidationException) {
return ValidationFailure(exception.message, exception.code, exception.details);
} else if (exception is CacheException) {
return CacheFailure(exception.message, exception.code, exception.details);
} else if (exception is AuthenticationException) {
return AuthenticationFailure(exception.message, exception.code, exception.details);
} else if (exception is AuthorizationException) {
return AuthorizationFailure(exception.message, exception.code, exception.details);
} else {
return UnknownFailure(exception.message, exception.code, exception.details);
}
}
}Error Reporting Service
// lib/core/errors/error_reporting_service.dart
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:logger/logger.dart';
import 'failures.dart';
class ErrorReportingService {
final Logger _logger;
final FirebaseCrashlytics _crashlytics;
ErrorReportingService(this._logger, this._crashlytics);
Future<void> reportError(Failure failure, {String? context}) async {
// Log to console
_logger.e(
'Error reported',
error: failure.message,
stackTrace: StackTrace.current,
);
// Send to crash reporting service
await _crashlytics.recordError(
failure,
fatal: _isFatalError(failure),
information: [
DiagnosticsProperty('code', failure.code),
DiagnosticsProperty('timestamp', failure.timestamp),
if (context != null) DiagnosticsProperty('context', context),
if (failure.details != null) DiagnosticsProperty('details', failure.details),
],
);
}
Future<void> reportMessage(String message, {LogLevel level = LogLevel.info}) async {
_logger.log(level, message);
if (level == LogLevel.error || level == LogLevel.fatal) {
await _crashlytics.log(message);
}
}
void setUserIdentifier(String userId) {
_crashlytics.setUserIdentifier(userId);
}
void setCustomKey(String key, dynamic value) {
_crashlytics.setCustomKey(key, value);
}
bool _isFatalError(Failure failure) {
return failure is AuthenticationFailure ||
failure is AuthorizationFailure ||
failure is ServerFailure;
}
}Error Handling in Repository Layer
// lib/features/user/data/repositories/user_repository_impl.dart
class UserRepositoryImpl implements UserRepository {
final UserRemoteDataSource remoteDataSource;
final UserLocalDataSource localDataSource;
final NetworkInfo networkInfo;
final ErrorHandler errorHandler;
final ErrorReportingService errorReporting;
UserRepositoryImpl({
required this.remoteDataSource,
required this.localDataSource,
required this.networkInfo,
required this.errorHandler,
required this.errorReporting,
});
@override
Future<List<User>> getUsers({int page = 1, int limit = 20}) async {
try {
if (await networkInfo.isConnected) {
final remoteUsers = await remoteDataSource.getUsers(page: page, limit: limit);
await localDataSource.cacheUsers(remoteUsers);
return remoteUsers.map((model) => model.toEntity()).toList();
} else {
return await _getCachedUsers();
}
} on Exception catch (e) {
final failure = errorHandler.handleException(e);
await errorReporting.reportError(failure, context: 'UserRepository.getUsers');
throw failure;
}
}
@override
Future<User> getUserById(String id) async {
try {
if (await networkInfo.isConnected) {
final remoteUser = await remoteDataSource.getUserById(id);
if (remoteUser != null) {
await localDataSource.cacheUser(remoteUser);
return remoteUser.toEntity();
}
throw ServerException('User not found', 'NOT_FOUND');
} else {
return await _getCachedUserById(id);
}
} on Exception catch (e) {
final failure = errorHandler.handleException(e);
await errorReporting.reportError(failure, context: 'UserRepository.getUserById');
throw failure;
}
}
}Error Handling in Use Cases
// lib/features/user/domain/usecases/get_users_usecase.dart
class GetUsersUseCase {
final UserRepository repository;
final ErrorHandler errorHandler;
final ErrorReportingService errorReporting;
GetUsersUseCase(this.repository, this.errorHandler, this.errorReporting);
Future<Either<Failure, List<User>>> call({int page = 1, int limit = 20}) async {
if (page < 1) {
final failure = ValidationFailure('Page must be greater than 0', 'INVALID_PAGE');
await errorReporting.reportError(failure, context: 'GetUsersUseCase.validation');
return Left(failure);
}
if (limit < 1 || limit > 100) {
final failure = ValidationFailure('Limit must be between 1 and 100', 'INVALID_LIMIT');
await errorReporting.reportError(failure, context: 'GetUsersUseCase.validation');
return Left(failure);
}
try {
final users = await repository.getUsers(page: page, limit: limit);
return Right(users);
} on Failure catch (failure) {
await errorReporting.reportError(failure, context: 'GetUsersUseCase.execution');
return Left(failure);
} catch (e) {
final failure = errorHandler.handleException(e);
await errorReporting.reportError(failure, context: 'GetUsersUseCase.execution');
return Left(failure);
}
}
}Error Handling in Presentation Layer
Provider Error Handling
// lib/features/user/presentation/providers/user_provider.dart
class UserProvider extends ChangeNotifier {
final GetUsersUseCase getUsers;
final ErrorHandler errorHandler;
final ErrorReportingService errorReporting;
UserState _state = const UserState();
UserState get state => _state;
UserProvider({
required this.getUsers,
required this.errorHandler,
required this.errorReporting,
});
Future<void> loadUsers({bool refresh = false}) async {
_state = _state.copyWith(isLoading: true, error: null);
notifyListeners();
try {
final result = await getUsers();
result.fold(
(failure) {
_state = _state.copyWith(isLoading: false, error: failure);
_showUserFriendlyError(failure);
},
(users) {
_state = _state.copyWith(isLoading: false, users: users);
},
);
} catch (e) {
final failure = errorHandler.handleException(e);
await errorReporting.reportError(failure, context: 'UserProvider.loadUsers');
_state = _state.copyWith(isLoading: false, error: failure);
_showUserFriendlyError(failure);
}
notifyListeners();
}
void _showUserFriendlyError(Failure failure) {
// Convert technical errors to user-friendly messages
String userMessage;
if (failure is NetworkFailure) {
userMessage = 'Please check your internet connection and try again.';
} else if (failure is ServerFailure) {
userMessage = 'Server is temporarily unavailable. Please try again later.';
} else if (failure is ValidationFailure) {
userMessage = 'Please check your input and try again.';
} else if (failure is AuthenticationFailure) {
userMessage = 'Please log in to continue.';
} else {
userMessage = 'An unexpected error occurred. Please try again.';
}
// Show snackbar or dialog
_showErrorDialog(userMessage);
}
void _showErrorDialog(String message) {
// Implementation for showing error dialog
}
}Bloc Error Handling
// lib/features/user/presentation/bloc/user_bloc.dart
class UserBloc extends Bloc<UserEvent, UserState> {
final GetUsersUseCase getUsers;
final ErrorHandler errorHandler;
final ErrorReportingService errorReporting;
UserBloc({
required this.getUsers,
required this.errorHandler,
required this.errorReporting,
}) : super(UserInitial()) {
on<LoadUsers>(_onLoadUsers);
}
Future<void> _onLoadUsers(LoadUsers event, Emitter<UserState> emit) async {
emit(UserLoading());
try {
final result = await getUsers();
result.fold(
(failure) async {
await errorReporting.reportError(failure, context: 'UserBloc.loadUsers');
emit(UserError(_getUserFriendlyMessage(failure)));
},
(users) {
emit(UserLoaded(users));
},
);
} catch (e) {
final failure = errorHandler.handleException(e);
await errorReporting.reportError(failure, context: 'UserBloc.loadUsers');
emit(UserError(_getUserFriendlyMessage(failure)));
}
}
String _getUserFriendlyMessage(Failure failure) {
if (failure is NetworkFailure) {
return 'Please check your internet connection and try again.';
} else if (failure is ServerFailure) {
return 'Server is temporarily unavailable. Please try again later.';
} else if (failure is ValidationFailure) {
return 'Please check your input and try again.';
} else if (failure is AuthenticationFailure) {
return 'Please log in to continue.';
} else {
return 'An unexpected error occurred. Please try again.';
}
}
}Riverpod Error Handling
// lib/features/user/presentation/providers/user_provider.dart
class UserNotifier extends StateNotifier<AsyncValue<List<User>>> {
final GetUsersUseCase getUsers;
final ErrorHandler errorHandler;
final ErrorReportingService errorReporting;
UserNotifier({
required this.getUsers,
required this.errorHandler,
required this.errorReporting,
}) : super(const AsyncValue.loading());
Future<void> loadUsers() async {
state = const AsyncValue.loading();
try {
final result = await getUsers();
result.fold(
(failure) async {
await errorReporting.reportError(failure, context: 'UserNotifier.loadUsers');
state = AsyncValue.error(_getUserFriendlyMessage(failure));
},
(users) {
state = AsyncValue.data(users);
},
);
} catch (e) {
final failure = errorHandler.handleException(e);
await errorReporting.reportError(failure, context: 'UserNotifier.loadUsers');
state = AsyncValue.error(_getUserFriendlyMessage(failure));
}
}
String _getUserFriendlyMessage(Failure failure) {
if (failure is NetworkFailure) {
return 'Please check your internet connection and try again.';
} else if (failure is ServerFailure) {
return 'Server is temporarily unavailable. Please try again later.';
} else if (failure is ValidationFailure) {
return 'Please check your input and try again.';
} else if (failure is AuthenticationFailure) {
return 'Please log in to continue.';
} else {
return 'An unexpected error occurred. Please try again.';
}
}
}Error UI Components
Error Widget
// lib/core/widgets/error_widget.dart
class ErrorWidget extends StatelessWidget {
final Failure failure;
final VoidCallback? onRetry;
const ErrorWidget({
Key? key,
required this.failure,
this.onRetry,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
_getErrorIcon(),
size: 64,
color: Theme.of(context).errorColor,
),
const SizedBox(height: 16),
Text(
_getErrorMessage(),
style: Theme.of(context).textTheme.bodyLarge,
textAlign: TextAlign.center,
),
if (onRetry != null) ...[
const SizedBox(height: 16),
ElevatedButton(
onPressed: onRetry,
child: const Text('Retry'),
),
],
],
),
),
);
}
IconData _getErrorIcon() {
if (failure is NetworkFailure) {
return Icons.wifi_off;
} else if (failure is ServerFailure) {
return Icons.cloud_off;
} else if (failure is ValidationFailure) {
return Icons.error_outline;
} else {
return Icons.error;
}
}
String _getErrorMessage() {
if (failure is NetworkFailure) {
return 'Please check your internet connection and try again.';
} else if (failure is ServerFailure) {
return 'Server is temporarily unavailable. Please try again later.';
} else if (failure is ValidationFailure) {
return 'Please check your input and try again.';
} else if (failure is AuthenticationFailure) {
return 'Please log in to continue.';
} else {
return failure.message;
}
}
}Error Dialog
// lib/core/widgets/error_dialog.dart
class ErrorDialog extends StatelessWidget {
final Failure failure;
final VoidCallback? onRetry;
final VoidCallback? onDismiss;
const ErrorDialog({
Key? key,
required this.failure,
this.onRetry,
this.onDismiss,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Row(
children: [
Icon(
_getErrorIcon(),
color: Theme.of(context).errorColor,
),
const SizedBox(width: 8),
Text(_getErrorTitle()),
],
),
content: Text(_getErrorMessage()),
actions: [
if (onRetry != null)
TextButton(
onPressed: () {
Navigator.of(context).pop();
onRetry?.call();
},
child: const Text('Retry'),
),
TextButton(
onPressed: onDismiss ?? () => Navigator.of(context).pop(),
child: const Text('Dismiss'),
),
],
);
}
IconData _getErrorIcon() {
if (failure is NetworkFailure) {
return Icons.wifi_off;
} else if (failure is ServerFailure) {
return Icons.cloud_off;
} else if (failure is ValidationFailure) {
return Icons.error_outline;
} else {
return Icons.error;
}
}
String _getErrorTitle() {
if (failure is NetworkFailure) {
return 'Network Error';
} else if (failure is ServerFailure) {
return 'Server Error';
} else if (failure is ValidationFailure) {
return 'Validation Error';
} else if (failure is AuthenticationFailure) {
return 'Authentication Error';
} else {
return 'Error';
}
}
String _getErrorMessage() {
if (failure is NetworkFailure) {
return 'Please check your internet connection and try again.';
} else if (failure is ServerFailure) {
return 'Server is temporarily unavailable. Please try again later.';
} else if (failure is ValidationFailure) {
return 'Please check your input and try again.';
} else if (failure is AuthenticationFailure) {
return 'Please log in to continue.';
} else {
return failure.message;
}
}
}Error Handling Best Practices
1. Centralized Error Handling: Use a centralized error handler to convert exceptions to failures 2. User-Friendly Messages: Convert technical errors to user-friendly messages 3. Error Reporting: Report errors to analytics for monitoring and debugging 4. Consistent Error Types: Use consistent error types across the application 5. Proper Error Display: Show appropriate error UI based on error type 6. Retry Mechanisms: Provide retry options for recoverable errors 7. Error Logging: Log errors for debugging and monitoring 8. Graceful Degradation: Handle errors gracefully without crashing the app
Error Handling Configuration
// lib/core/errors/error_config.dart
class ErrorConfig {
static const Duration defaultTimeout = Duration(seconds: 30);
static const int maxRetryAttempts = 3;
static const Duration retryDelay = Duration(seconds: 1);
static const bool enableErrorReporting = true;
static const bool enableErrorLogging = true;
static Map<String, String> get errorMessages => {
'NETWORK_ERROR': 'Please check your internet connection and try again.',
'SERVER_ERROR': 'Server is temporarily unavailable. Please try again later.',
'VALIDATION_ERROR': 'Please check your input and try again.',
'AUTHENTICATION_ERROR': 'Please log in to continue.',
'UNKNOWN_ERROR': 'An unexpected error occurred. Please try again.',
};
}This comprehensive error handling guide ensures robust error management in enterprise Flutter applications, providing better user experience and easier debugging.
Feature-Based Templates for Flutter Enterprise
Overview
Feature-based architecture organizes code around business features rather than technical layers. Each feature is self-contained with its own data, domain, and presentation layers.
Feature Structure Template
features/
├── feature_name/
│ ├── data/
│ │ ├── datasources/
│ │ │ ├── feature_remote_data_source.dart
│ │ │ └── feature_local_data_source.dart
│ │ ├── models/
│ │ │ ├── feature_model.dart
│ │ │ └── feature_response_model.dart
│ │ └── repositories/
│ │ └── feature_repository_impl.dart
│ ├── domain/
│ │ ├── entities/
│ │ │ └── feature_entity.dart
│ │ ├── repositories/
│ │ │ └── feature_repository.dart
│ │ └── usecases/
│ │ ├── get_feature_usecase.dart
│ │ ├── create_feature_usecase.dart
│ │ ├── update_feature_usecase.dart
│ │ └── delete_feature_usecase.dart
│ └── presentation/
│ ├── pages/
│ │ ├── feature_list_page.dart
│ │ └── feature_detail_page.dart
│ ├── widgets/
│ │ ├── feature_card_widget.dart
│ │ ├── feature_form_widget.dart
│ │ └── feature_loading_widget.dart
│ └── state_management/
│ ├── providers/ # Provider pattern
│ │ ├── feature_provider.dart
│ │ └── feature_list_provider.dart
│ ├── bloc/ # Bloc pattern
│ │ ├── feature_bloc.dart
│ │ ├── feature_state.dart
│ │ └── feature_event.dart
│ ├── riverpod/ # Riverpod pattern
│ │ ├── feature_provider.dart
│ │ ├── feature_notifier.dart
│ │ └── feature_state.dart
│ └── getx/ # GetX pattern
│ ├── feature_controller.dart
│ └── feature_binding.dartTemplate: User Management Feature
Domain Layer
Entity Template
// lib/features/user/domain/entities/user_entity.dart
class UserEntity {
final String id;
final String name;
final String email;
final String? avatar;
final DateTime createdAt;
final DateTime updatedAt;
const UserEntity({
required this.id,
required this.name,
required this.email,
this.avatar,
required this.createdAt,
required this.updatedAt,
});
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is UserEntity &&
other.id == id &&
other.name == name &&
other.email == email &&
other.avatar == avatar &&
other.createdAt == createdAt &&
other.updatedAt == updatedAt;
}
@override
int get hashCode {
return id.hashCode ^
name.hashCode ^
email.hashCode ^
avatar.hashCode ^
createdAt.hashCode ^
updatedAt.hashCode;
}
UserEntity copyWith({
String? id,
String? name,
String? email,
String? avatar,
DateTime? createdAt,
DateTime? updatedAt,
}) {
return UserEntity(
id: id ?? this.id,
name: name ?? this.name,
email: email ?? this.email,
avatar: avatar ?? this.avatar,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
);
}
}Repository Interface Template
// lib/features/user/domain/repositories/user_repository.dart
import '../entities/user_entity.dart';
abstract class UserRepository {
/// Get all users
Future<List<UserEntity>> getUsers({int page = 1, int limit = 20});
/// Get user by ID
Future<UserEntity?> getUserById(String id);
/// Search users by name or email
Future<List<UserEntity>> searchUsers(String query);
/// Create new user
Future<UserEntity> createUser(UserEntity user);
/// Update existing user
Future<UserEntity> updateUser(UserEntity user);
/// Delete user
Future<void> deleteUser(String id);
/// Get user profile
Future<UserEntity> getUserProfile();
/// Update user profile
Future<UserEntity> updateUserProfile(UserEntity user);
}Use Case Templates
// lib/features/user/domain/usecases/get_users_usecase.dart
import '../entities/user_entity.dart';
import '../repositories/user_repository.dart';
class GetUsersUseCase {
final UserRepository _repository;
GetUsersUseCase(this._repository);
Future<List<UserEntity>> call({int page = 1, int limit = 20}) async {
if (page < 1) {
throw ArgumentError('Page must be greater than 0');
}
if (limit < 1 || limit > 100) {
throw ArgumentError('Limit must be between 1 and 100');
}
return await _repository.getUsers(page: page, limit: limit);
}
}
// lib/features/user/domain/usecases/create_user_usecase.dart
class CreateUserUseCase {
final UserRepository _repository;
CreateUserUseCase(this._repository);
Future<UserEntity> call(UserEntity user) async {
if (user.name.isEmpty) {
throw ArgumentError('User name cannot be empty');
}
if (!user.email.contains('@')) {
throw ArgumentError('Invalid email format');
}
return await _repository.createUser(user);
}
}
// lib/features/user/domain/usecases/search_users_usecase.dart
class SearchUsersUseCase {
final UserRepository _repository;
SearchUsersUseCase(this._repository);
Future<List<UserEntity>> call(String query) async {
if (query.length < 2) {
throw ArgumentError('Search query must be at least 2 characters');
}
return await _repository.searchUsers(query);
}
}Data Layer
Model Template
// lib/features/user/data/models/user_model.dart
import '../../domain/entities/user_entity.dart';
class UserModel {
final String id;
final String name;
final String email;
final String? avatar;
final String createdAt;
final String updatedAt;
const UserModel({
required this.id,
required this.name,
required this.email,
this.avatar,
required this.createdAt,
required this.updatedAt,
});
factory UserModel.fromJson(Map<String, dynamic> json) {
return UserModel(
id: json['id'] as String,
name: json['name'] as String,
email: json['email'] as String,
avatar: json['avatar'] as String?,
createdAt: json['created_at'] as String,
updatedAt: json['updated_at'] as String,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'email': email,
if (avatar != null) 'avatar': avatar,
'created_at': createdAt,
'updated_at': updatedAt,
};
}
UserEntity toEntity() {
return UserEntity(
id: id,
name: name,
email: email,
avatar: avatar,
createdAt: DateTime.parse(createdAt),
updatedAt: DateTime.parse(updatedAt),
);
}
factory UserModel.fromEntity(UserEntity entity) {
return UserModel(
id: entity.id,
name: entity.name,
email: entity.email,
avatar: entity.avatar,
createdAt: entity.createdAt.toIso8601String(),
updatedAt: entity.updatedAt.toIso8601String(),
);
}
}Data Source Template
// lib/features/user/data/datasources/user_remote_data_source.dart
import '../models/user_model.dart';
abstract class UserRemoteDataSource {
Future<List<UserModel>> getUsers({int page = 1, int limit = 20});
Future<UserModel?> getUserById(String id);
Future<List<UserModel>> searchUsers(String query);
Future<UserModel> createUser(UserModel user);
Future<UserModel> updateUser(UserModel user);
Future<void> deleteUser(String id);
}
// lib/features/user/data/datasources/user_remote_data_source_impl.dart
import 'package:http/http.dart' as http;
import 'package:convert/convert.dart';
import '../../core/network/network_info.dart';
import '../models/user_model.dart';
class UserRemoteDataSourceImpl implements UserRemoteDataSource {
final HttpClient httpClient;
final NetworkInfo networkInfo;
UserRemoteDataSourceImpl({
required this.httpClient,
required this.networkInfo,
});
@override
Future<List<UserModel>> getUsers({int page = 1, int limit = 20}) async {
final response = await httpClient.get(
'/users',
queryParameters: {
'page': page,
'limit': limit,
},
);
if (response.statusCode == 200) {
final List<dynamic> jsonData = response.data['data'];
return jsonData.map((json) => UserModel.fromJson(json)).toList();
} else {
throw ServerException('Failed to load users');
}
}
@override
Future<UserModel?> getUserById(String id) async {
final response = await httpClient.get('/users/$id');
if (response.statusCode == 200) {
return UserModel.fromJson(response.data['data']);
} else if (response.statusCode == 404) {
return null;
} else {
throw ServerException('Failed to load user');
}
}
@override
Future<List<UserModel>> searchUsers(String query) async {
final response = await httpClient.get(
'/users/search',
queryParameters: {'q': query},
);
if (response.statusCode == 200) {
final List<dynamic> jsonData = response.data['data'];
return jsonData.map((json) => UserModel.fromJson(json)).toList();
} else {
throw ServerException('Failed to search users');
}
}
@override
Future<UserModel> createUser(UserModel user) async {
final response = await httpClient.post(
'/users',
data: user.toJson(),
);
if (response.statusCode == 201) {
return UserModel.fromJson(response.data['data']);
} else {
throw ServerException('Failed to create user');
}
}
@override
Future<UserModel> updateUser(UserModel user) async {
final response = await httpClient.put(
'/users/${user.id}',
data: user.toJson(),
);
if (response.statusCode == 200) {
return UserModel.fromJson(response.data['data']);
} else {
throw ServerException('Failed to update user');
}
}
@override
Future<void> deleteUser(String id) async {
final response = await httpClient.delete('/users/$id');
if (response.statusCode != 204) {
throw ServerException('Failed to delete user');
}
}
}Repository Implementation Template
// lib/features/user/data/repositories/user_repository_impl.dart
import '../../domain/entities/user_entity.dart';
import '../../domain/repositories/user_repository.dart';
import '../datasources/user_remote_data_source.dart';
import '../datasources/user_local_data_source.dart';
import '../models/user_model.dart';
import '../../../core/network/network_info.dart';
import '../../../core/exceptions/exceptions.dart';
class UserRepositoryImpl implements UserRepository {
final UserRemoteDataSource remoteDataSource;
final UserLocalDataSource localDataSource;
final NetworkInfo networkInfo;
UserRepositoryImpl({
required this.remoteDataSource,
required this.localDataSource,
required this.networkInfo,
});
@override
Future<List<UserEntity>> getUsers({int page = 1, int limit = 20}) async {
if (await networkInfo.isConnected) {
try {
final remoteUsers = await remoteDataSource.getUsers(page: page, limit: limit);
// Cache the users locally
await localDataSource.cacheUsers(remoteUsers);
return remoteUsers.map((model) => model.toEntity()).toList();
} on ServerException {
// Fallback to cached data if server fails
return await _getCachedUsers();
}
} else {
// Use cached data when offline
return await _getCachedUsers();
}
}
@override
Future<UserEntity?> getUserById(String id) async {
if (await networkInfo.isConnected) {
try {
final remoteUser = await remoteDataSource.getUserById(id);
if (remoteUser != null) {
// Cache the user locally
await localDataSource.cacheUser(remoteUser);
return remoteUser.toEntity();
}
return null;
} on ServerException {
// Fallback to cached data
return await _getCachedUserById(id);
}
} else {
return await _getCachedUserById(id);
}
}
@override
Future<List<UserEntity>> searchUsers(String query) async {
if (await networkInfo.isConnected) {
try {
final searchResults = await remoteDataSource.searchUsers(query);
return searchResults.map((model) => model.toEntity()).toList();
} on ServerException {
throw SearchException('Failed to search users');
}
} else {
throw NetworkException('No internet connection');
}
}
@override
Future<UserEntity> createUser(UserEntity user) async {
if (await networkInfo.isConnected) {
try {
final userModel = UserModel.fromEntity(user);
final createdUser = await remoteDataSource.createUser(userModel);
// Update local cache
await localDataSource.cacheUser(createdUser);
return createdUser.toEntity();
} on ServerException {
throw CreateException('Failed to create user');
}
} else {
throw NetworkException('No internet connection');
}
}
@override
Future<UserEntity> updateUser(UserEntity user) async {
if (await networkInfo.isConnected) {
try {
final userModel = UserModel.fromEntity(user);
final updatedUser = await remoteDataSource.updateUser(userModel);
// Update local cache
await localDataSource.cacheUser(updatedUser);
return updatedUser.toEntity();
} on ServerException {
throw UpdateException('Failed to update user');
}
} else {
throw NetworkException('No internet connection');
}
}
@override
Future<void> deleteUser(String id) async {
if (await networkInfo.isConnected) {
try {
await remoteDataSource.deleteUser(id);
// Remove from local cache
await localDataSource.deleteCachedUser(id);
} on ServerException {
throw DeleteException('Failed to delete user');
}
} else {
throw NetworkException('No internet connection');
}
}
Future<List<UserEntity>> _getCachedUsers() async {
final cachedUsers = await localDataSource.getCachedUsers();
return cachedUsers.map((model) => model.toEntity()).toList();
}
Future<UserEntity?> _getCachedUserById(String id) async {
final cachedUser = await localDataSource.getCachedUserById(id);
return cachedUser?.toEntity();
}
}Presentation Layer
Provider Template
// lib/features/user/presentation/providers/user_provider.dart
import 'package:flutter/foundation.dart';
import '../../domain/entities/user_entity.dart';
import '../../domain/usecases/get_users_usecase.dart';
import '../../domain/usecases/create_user_usecase.dart';
import '../../domain/usecases/search_users_usecase.dart';
class UserProvider extends ChangeNotifier {
final GetUsersUseCase _getUsersUseCase;
final CreateUserUseCase _createUserUseCase;
final SearchUsersUseCase _searchUsersUseCase;
UserProvider({
required GetUsersUseCase getUsersUseCase,
required CreateUserUseCase createUserUseCase,
required SearchUsersUseCase searchUsersUseCase,
}) : _getUsersUseCase = getUsersUseCase,
_createUserUseCase = createUserUseCase,
_searchUsersUseCase = searchUsersUseCase;
// State
List<UserEntity> _users = [];
List<UserEntity> _searchResults = [];
bool _isLoading = false;
bool _isSearching = false;
String? _error;
int _currentPage = 1;
bool _hasMore = true;
// Getters
List<UserEntity> get users => _users;
List<UserEntity> get searchResults => _searchResults;
bool get isLoading => _isLoading;
bool get isSearching => _isSearching;
String? get error => _error;
bool get hasMore => _hasMore;
// Methods
Future<void> loadUsers({bool refresh = false}) async {
if (refresh) {
_currentPage = 1;
_hasMore = true;
_users.clear();
}
if (_isLoading || !_hasMore) return;
_isLoading = true;
_error = null;
notifyListeners();
try {
final newUsers = await _getUsersUseCase(page: _currentPage, limit: 20);
if (refresh) {
_users = newUsers;
} else {
_users.addAll(newUsers);
}
_hasMore = newUsers.length == 20;
_currentPage++;
} catch (e) {
_error = e.toString();
} finally {
_isLoading = false;
notifyListeners();
}
}
Future<void> searchUsers(String query) async {
if (query.isEmpty) {
_searchResults.clear();
_isSearching = false;
notifyListeners();
return;
}
_isSearching = true;
notifyListeners();
try {
_searchResults = await _searchUsersUseCase(query);
} catch (e) {
_error = e.toString();
} finally {
_isSearching = false;
notifyListeners();
}
}
Future<void> createUser(UserEntity user) async {
try {
await _createUserUseCase(user);
await loadUsers(refresh: true);
} catch (e) {
_error = e.toString();
notifyListeners();
}
}
void clearError() {
_error = null;
notifyListeners();
}
}Bloc Template
// lib/features/user/presentation/bloc/user_bloc.dart
import 'package:flutter/material.dart';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import '../../domain/entities/user_entity.dart';
import '../../domain/usecases/get_users_usecase.dart';
import '../../domain/usecases/create_user_usecase.dart';
import '../../domain/usecases/search_users_usecase.dart';
// Events
abstract class UserEvent extends Equatable {
const UserEvent();
@override
List<Object> get props => [];
}
class LoadUsers extends UserEvent {
final bool refresh;
final int page;
const LoadUsers({this.refresh = false, this.page = 1});
@override
List<Object> get props => [refresh, page];
}
class SearchUsers extends UserEvent {
final String query;
const SearchUsers(this.query);
@override
List<Object> get props => [query];
}
class CreateUser extends UserEvent {
final UserEntity user;
const CreateUser(this.user);
@override
List<Object> get props => [user];
}
class ClearError extends UserEvent {}
// States
abstract class UserState extends Equatable {
const UserState();
@override
List<Object> get props => [];
}
class UserInitial extends UserState {}
class UserLoading extends UserState {}
class UserLoaded extends UserState {
final List<UserEntity> users;
final bool hasMore;
final List<UserEntity> searchResults;
final bool isSearching;
const UserLoaded({
required this.users,
required this.hasMore,
this.searchResults = const [],
this.isSearching = false,
});
@override
List<Object> get props => [users, hasMore, searchResults, isSearching];
}
class UserError extends UserState {
final String message;
const UserError(this.message);
@override
List<Object> get props => [message];
}
// Bloc Implementation
class UserBloc extends Bloc<UserEvent, UserState> {
final GetUsersUseCase _getUsersUseCase;
final CreateUserUseCase _createUserUseCase;
final SearchUsersUseCase _searchUsersUseCase;
int _currentPage = 1;
UserBloc({
required GetUsersUseCase getUsersUseCase,
required CreateUserUseCase createUserUseCase,
required SearchUsersUseCase searchUsersUseCase,
}) : _getUsersUseCase = getUsersUseCase,
_createUserUseCase = createUserUseCase,
_searchUsersUseCase = searchUsersUseCase,
super(UserInitial()) {
on<LoadUsers>(_onLoadUsers);
on<SearchUsers>(_onSearchUsers);
on<CreateUser>(_onCreateUser);
on<ClearError>(_onClearError);
}
Future<void> _onLoadUsers(LoadUsers event, Emitter<UserState> emit) async {
if (event.refresh) {
_currentPage = 1;
}
final currentState = state;
List<UserEntity> currentUsers = [];
if (currentState is UserLoaded && !event.refresh) {
currentUsers = currentState.users;
}
emit(UserLoading());
try {
final newUsers = await _getUsersUseCase(page: _currentPage, limit: 20);
final allUsers = event.refresh ? newUsers : [...currentUsers, ...newUsers];
final hasMore = newUsers.length == 20;
_currentPage++;
emit(UserLoaded(
users: allUsers,
hasMore: hasMore,
searchResults: currentState is UserLoaded ? currentState.searchResults : [],
isSearching: currentState is UserLoaded ? currentState.isSearching : false,
));
} catch (e) {
emit(UserError(e.toString()));
}
}
Future<void> _onSearchUsers(SearchUsers event, Emitter<UserState> emit) async {
if (event.query.isEmpty) {
final currentState = state;
if (currentState is UserLoaded) {
emit(currentState.copyWith(searchResults: [], isSearching: false));
}
return;
}
final currentState = state;
if (currentState is UserLoaded) {
emit(currentState.copyWith(isSearching: true));
}
try {
final searchResults = await _searchUsersUseCase(event.query);
final updatedState = state is UserLoaded
? (state as UserLoaded).copyWith(searchResults: searchResults, isSearching: false)
: UserLoaded(users: [], hasMore: false, searchResults: searchResults, isSearching: false);
emit(updatedState);
} catch (e) {
emit(UserError(e.toString()));
}
}
Future<void> _onCreateUser(CreateUser event, Emitter<UserState> emit) async {
try {
await _createUserUseCase(event.user);
add(LoadUsers(refresh: true));
} catch (e) {
emit(UserError(e.toString()));
}
}
Future<void> _onClearError(ClearError event, Emitter<UserState> emit) async {
final currentState = state;
if (currentState is UserError) {
emit(UserInitial());
add(LoadUsers(refresh: true));
}
}
}
extension UserStateCopyWith on UserLoaded {
UserLoaded copyWith({
List<UserEntity>? users,
bool? hasMore,
List<UserEntity>? searchResults,
bool? isSearching,
}) {
return UserLoaded(
users: users ?? this.users,
hasMore: hasMore ?? this.hasMore,
searchResults: searchResults ?? this.searchResults,
isSearching: isSearching ?? this.isSearching,
);
}
}Riverpod Template
// lib/features/user/presentation/providers/user_provider.dart
import 'package:flutter/material.dart';
import 'package:riverpod/riverpod.dart';
import '../../domain/entities/user_entity.dart';
import '../../domain/usecases/get_users_usecase.dart';
import '../../domain/usecases/create_user_usecase.dart';
import '../../domain/usecases/search_users_usecase.dart';
// State Class
class UserState {
final List<UserEntity> users;
final List<UserEntity> searchResults;
final bool isLoading;
final bool isSearching;
final String? error;
final int currentPage;
final bool hasMore;
const UserState({
this.users = const [],
this.searchResults = const [],
this.isLoading = false,
this.isSearching = false,
this.error,
this.currentPage = 1,
this.hasMore = true,
});
UserState copyWith({
List<UserEntity>? users,
List<UserEntity>? searchResults,
bool? isLoading,
bool? isSearching,
String? error,
int? currentPage,
bool? hasMore,
}) {
return UserState(
users: users ?? this.users,
searchResults: searchResults ?? this.searchResults,
isLoading: isLoading ?? this.isLoading,
isSearching: isSearching ?? this.isSearching,
error: error ?? this.error,
currentPage: currentPage ?? this.currentPage,
hasMore: hasMore ?? this.hasMore,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is UserState &&
other.users == users &&
other.searchResults == searchResults &&
other.isLoading == isLoading &&
other.isSearching == isSearching &&
other.error == error &&
other.currentPage == currentPage &&
other.hasMore == hasMore;
}
@override
int get hashCode {
return users.hashCode ^
searchResults.hashCode ^
isLoading.hashCode ^
isSearching.hashCode ^
error.hashCode ^
currentPage.hashCode ^
hasMore.hashCode;
}
}
// State Notifier
class UserNotifier extends StateNotifier<UserState> {
final GetUsersUseCase _getUsersUseCase;
final CreateUserUseCase _createUserUseCase;
final SearchUsersUseCase _searchUsersUseCase;
UserNotifier({
required GetUsersUseCase getUsersUseCase,
required CreateUserUseCase createUserUseCase,
required SearchUsersUseCase searchUsersUseCase,
}) : _getUsersUseCase = getUsersUseCase,
_createUserUseCase = createUserUseCase,
_searchUsersUseCase = searchUsersUseCase,
super(const UserState());
Future<void> loadUsers({bool refresh = false}) async {
if (refresh) {
state = state.copyWith(
currentPage: 1,
hasMore: true,
users: [],
);
}
if (state.isLoading || !state.hasMore) return;
state = state.copyWith(isLoading: true, error: null);
try {
final newUsers = await _getUsersUseCase(page: state.currentPage, limit: 20);
state = state.copyWith(
users: refresh ? newUsers : [...state.users, ...newUsers],
hasMore: newUsers.length == 20,
currentPage: state.currentPage + 1,
isLoading: false,
);
} catch (e) {
state = state.copyWith(
error: e.toString(),
isLoading: false,
);
}
}
Future<void> searchUsers(String query) async {
if (query.isEmpty) {
state = state.copyWith(searchResults: [], isSearching: false);
return;
}
state = state.copyWith(isSearching: true);
try {
final searchResults = await _searchUsersUseCase(query);
state = state.copyWith(
searchResults: searchResults,
isSearching: false,
);
} catch (e) {
state = state.copyWith(
error: e.toString(),
isSearching: false,
);
}
}
Future<void> createUser(UserEntity user) async {
try {
await _createUserUseCase(user);
await loadUsers(refresh: true);
} catch (e) {
state = state.copyWith(error: e.toString());
}
}
void clearError() {
state = state.copyWith(error: null);
}
}
// Providers
final userProvider = StateNotifierProvider<UserNotifier, UserState>((ref) {
return UserNotifier(
getUsersUseCase: ref.watch(getUsersUseCaseProvider),
createUserUseCase: ref.watch(createUserUseCaseProvider),
searchUsersUseCase: ref.watch(searchUsersUseCaseProvider),
);
});
// Use Case Providers
final getUsersUseCaseProvider = Provider<GetUsersUseCase>((ref) {
return GetUsersUseCase(ref.watch(userRepositoryProvider));
});
final createUserUseCaseProvider = Provider<CreateUserUseCase>((ref) {
return CreateUserUseCase(ref.watch(userRepositoryProvider));
});
final searchUsersUseCaseProvider = Provider<SearchUsersUseCase>((ref) {
return SearchUsersUseCase(ref.watch(userRepositoryProvider));
});GetX Controller Template
// lib/features/user/presentation/controllers/user_controller.dart
import 'package:get/get.dart';
import '../../domain/entities/user_entity.dart';
import '../../domain/usecases/get_users_usecase.dart';
import '../../domain/usecases/create_user_usecase.dart';
import '../../domain/usecases/search_users_usecase.dart';
class UserController extends GetxController {
final GetUsersUseCase _getUsersUseCase;
final CreateUserUseCase _createUserUseCase;
final SearchUsersUseCase _searchUsersUseCase;
UserController({
required GetUsersUseCase getUsersUseCase,
required CreateUserUseCase createUserUseCase,
required SearchUsersUseCase searchUsersUseCase,
}) : _getUsersUseCase = getUsersUseCase,
_createUserUseCase = createUserUseCase,
_searchUsersUseCase = searchUsersUseCase;
// Reactive state variables
final RxList<UserEntity> _users = <UserEntity>[].obs;
final RxList<UserEntity> _searchResults = <UserEntity>[].obs;
final RxBool _isLoading = false.obs;
final RxBool _isSearching = false.obs;
final RxString _error = ''.obs;
final RxInt _currentPage = 1.obs;
final RxBool _hasMore = true.obs;
// Getters for reactive variables
List<UserEntity> get users => _users;
List<UserEntity> get searchResults => _searchResults;
bool get isLoading => _isLoading.value;
bool get isSearching => _isSearching.value;
String? get error => _error.value.isEmpty ? null : _error.value;
bool get hasMore => _hasMore.value;
// Methods
Future<void> loadUsers({bool refresh = false}) async {
if (refresh) {
_currentPage.value = 1;
_hasMore.value = true;
_users.clear();
}
if (_isLoading.value || !_hasMore.value) return;
_isLoading.value = true;
_error.value = '';
try {
final newUsers = await _getUsersUseCase(page: _currentPage.value, limit: 20);
if (refresh) {
_users.assignAll(newUsers);
} else {
_users.addAll(newUsers);
}
_hasMore.value = newUsers.length == 20;
_currentPage.value++;
} catch (e) {
_error.value = e.toString();
} finally {
_isLoading.value = false;
}
}
Future<void> searchUsers(String query) async {
if (query.isEmpty) {
_searchResults.clear();
_isSearching.value = false;
return;
}
_isSearching.value = true;
try {
_searchResults.assignAll(await _searchUsersUseCase(query));
} catch (e) {
_error.value = e.toString();
} finally {
_isSearching.value = false;
}
}
Future<void> createUser(UserEntity user) async {
try {
await _createUserUseCase(user);
await loadUsers(refresh: true);
} catch (e) {
_error.value = e.toString();
}
}
void clearError() {
_error.value = '';
}
}
// lib/features/user/presentation/bindings/user_binding.dart
import 'package:get/get.dart';
import '../../domain/usecases/get_users_usecase.dart';
import '../../domain/usecases/create_user_usecase.dart';
import '../../domain/usecases/search_users_usecase.dart';
import '../../domain/repositories/user_repository.dart';
import 'user_controller.dart';
class UserBinding extends Bindings {
@override
void dependencies() {
// Repositories
Get.lazyPut<UserRepository>(
() => UserRepositoryImpl(
remoteDataSource: Get.find(),
localDataSource: Get.find(),
networkInfo: Get.find(),
),
);
// Use cases
Get.lazyPut<GetUsersUseCase>(
() => GetUsersUseCase(Get.find()),
);
Get.lazyPut<CreateUserUseCase>(
() => CreateUserUseCase(Get.find()),
);
Get.lazyPut<SearchUsersUseCase>(
() => SearchUsersUseCase(Get.find()),
);
// Controller
Get.lazyPut<UserController>(
() => UserController(
getUsersUseCase: Get.find(),
createUserUseCase: Get.find(),
searchUsersUseCase: Get.find(),
),
);
}
}Feature Registration Template
// lib/core/di/feature_injection.dart
import 'package:get_it/get_it.dart';
import '../../features/user/data/datasources/user_remote_data_source_impl.dart';
import '../../features/user/data/repositories/user_repository_impl.dart';
import '../../features/user/domain/repositories/user_repository.dart';
import '../../features/user/domain/usecases/get_users_usecase.dart';
import '../../features/user/domain/usecases/create_user_usecase.dart';
import '../../features/user/domain/usecases/search_users_usecase.dart';
import '../../features/user/presentation/providers/user_provider.dart';
Future<void> registerUserFeatures(GetIt sl) async {
// Data sources
sl.registerLazySingleton<UserRemoteDataSource>(
() => UserRemoteDataSourceImpl(
httpClient: sl(),
networkInfo: sl(),
),
);
// Repositories
sl.registerLazySingleton<UserRepository>(
() => UserRepositoryImpl(
remoteDataSource: sl(),
localDataSource: sl(),
networkInfo: sl(),
),
);
// Use cases
sl.registerFactory<GetUsersUseCase>(
() => GetUsersUseCase(sl()),
);
sl.registerFactory<CreateUserUseCase>(
() => CreateUserUseCase(sl()),
);
sl.registerFactory<SearchUsersUseCase>(
() => SearchUsersUseCase(sl()),
);
// Providers
sl.registerFactory<UserProvider>(
() => UserProvider(
getUsersUseCase: sl(),
createUserUseCase: sl(),
searchUsersUseCase: sl(),
),
);
}GetX Feature Registration Template
// lib/core/di/feature_injection.dart
import 'package:get/get.dart';
import '../../features/user/data/datasources/user_remote_data_source_impl.dart';
import '../../features/user/data/repositories/user_repository_impl.dart';
import '../../features/user/domain/repositories/user_repository.dart';
import '../../features/user/domain/usecases/get_users_usecase.dart';
import '../../features/user/domain/usecases/create_user_usecase.dart';
import '../../features/user/domain/usecases/search_users_usecase.dart';
import '../../features/user/presentation/controllers/user_controller.dart';
Future<void> registerUserFeatures() async {
// Data sources
Get.lazyPut<UserRemoteDataSource>(
() => UserRemoteDataSourceImpl(
httpClient: Get.find(),
networkInfo: Get.find(),
),
);
// Repositories
Get.lazyPut<UserRepository>(
() => UserRepositoryImpl(
remoteDataSource: Get.find(),
localDataSource: Get.find(),
networkInfo: Get.find(),
),
);
// Use cases
Get.lazyPut<GetUsersUseCase>(
() => GetUsersUseCase(Get.find()),
);
Get.lazyPut<CreateUserUseCase>(
() => CreateUserUseCase(Get.find()),
);
Get.lazyPut<SearchUsersUseCase>(
() => SearchUsersUseCase(Get.find()),
);
// Controllers
Get.lazyPut<UserController>(
() => UserController(
getUsersUseCase: Get.find(),
createUserUseCase: Get.find(),
searchUsersUseCase: Get.find(),
),
);
}
// Initial dependencies setup
Future<void> setupGetItDependencies() async {
// Core services
Get.lazyPut<HttpClient>(() => DioClient());
Get.lazyPut<NetworkInfo>(() => NetworkInfoImpl());
Get.lazyPut<UserLocalDataSource>(() => UserLocalDataSourceImpl());
// Register features
await registerUserFeatures();
}Best Practices
1. Feature Independence: Each feature should be self-contained 2. Clear Boundaries: Avoid cross-feature dependencies in domain layer 3. Consistent Naming: Use consistent naming conventions across features 4. Error Handling: Implement proper error handling in each layer 5. Testing: Write tests for each layer independently 6. Documentation: Document feature responsibilities and APIs
Related skills
FAQ
What architecture does flutter-enterprise use?
Feature-based clean architecture with dependency injection and code generation for maintainable enterprise apps.
When should I use it?
When building or organizing a scalable enterprise Flutter app that must stay testable and maintainable.