Flutter Enterprise Architecture Guidelines¶
Important Process Requirements¶
- ALWAYS present a detailed plan and wait for explicit approval before implementing any code changes
- Do not proceed with implementation until receiving confirmation from the user
- When presenting the plan, provide a step-by-step breakdown of all files to be created or modified
- Ask directly: "Do you approve this plan before I proceed with implementation?"
Project Structure¶
- Root:
/libfolder - Features structure:
/lib/src/features/<feature_name>/ data/- Models and repositoriesdomain/- Entities and use casespresentation/- Widgets, screens and state management
Naming Conventions¶
- Data Layer:
- Models:
{Name}Modelindata/models/ - Repositories:
{Name}Repositoryindata/repositories/ - Domain Layer:
- Entities: No suffix (e.g.,
User) indomain/entities/ - Use Cases:
{Action}{Entity}UseCaseindomain/usecases/ - Presentation Layer:
- Providers:
{name}Providerinpresentation/providers/ - Widgets:
{Name}Widgetinpresentation/widgets/
Code Style¶
- Comments:
- Keep comments under 80 characters per line
- Break long comments into multiple lines
- Example:
/// This is a long comment that would exceed 80 characters, so we break it into /// multiple lines to maintain readability and follow the style guide. - Don't create single-line methods
Documentation¶
Public API Documentation¶
/// Description of what the function/class does.
///
/// Parameters:
/// * [param1] - Description of param1
/// * [param2] - Description of param2
///
/// Returns:
/// * Description of return value
///
/// Throws:
/// * ExceptionType - When/why this exception occurs
Testing Documentation¶
group('Scenario: <description>', () {
test('''
GIVEN initial condition
WHEN action occurs
THEN expected result
''', () { /* test code */ });
});
Testing¶
Protocol for Test Failures¶
When a test fails, please adhere to the following process:
-
Default Action: Modify the Test:
- Your primary assumption should be that the source code is behaving as intended and the test requires correction to align with this behavior.
- Present a detailed plan outlining the proposed modifications to the test file(s).
- As per the "Important Process Requirements," ask for explicit approval before implementing these changes to the test(s).
-
Exception: Suspected Source Code Error:
- If, after analysis, you have strong evidence or a compelling hypothesis that the source code itself is incorrect and is the root cause of the test failure:
- Provide a clear and detailed explanation supporting your suspicion that the source code is at fault.
- Present a detailed plan that includes the necessary modifications to the source code. This plan may also include adjustments to the test file(s) if they also need to be updated in conjunction with the source code fix.
- Clearly state that your proposed plan involves changing the source code due to a suspected bug.
- As per the "Important Process Requirements," ask for explicit approval before implementing any changes to the source code.
- If, after analysis, you have strong evidence or a compelling hypothesis that the source code itself is incorrect and is the root cause of the test failure:
This protocol for handling test failures should always be followed in conjunction with the general requirement to present a detailed plan and await explicit user approval before any code modification.
Dependencies & Setup¶
Dependency Injection¶
- Use the
Injectorclass for injecting DataSource, Repository and UseCases - Register services in feature modules:
void initializeModule() { Injector.I ..registerLazySingleton<Repository>(() => RepositoryImpl()) ..registerLazySingleton<UseCase>(() => UseCaseImpl()) ..registerSingleton<DataSource>(DataSourceImpl()); } - Unregister services when no longer needed:
void disposeModule() { Injector.I ..unregister<Repository>() ..unregister<UseCase>(); ..unregister<DataSource>(); }
Design Patterns¶
- Don't place logic in UI layer
Flutter-Specific Guidance¶
State Management¶
- Use Riverpod for state management
- Use
@riverpodfor declaring providers - Use class-based providers for screens state management
- Application State:
- Lives in presentation layer
- Extends Equatable for App States
- Uses @riverpod for async operations
- Returns AsyncValue
for async state - Widget State:
- Ephemeral state stays in StatefulWidget
- Use select() for performance
- Avoid watch() on large objects
- Don't Mix App State with Ephemeral State
Widget Strategy¶
State Dependant Widgets separation from Raw Widgets¶
- Raw Widgets:
- Stateless
- No business logic
- No state management
- Receive all data via parameters
- State Dependant Widgets:
- Handle business logic/state
- Connect to providers
- Manage navigation
- Separate Raw Widgets from State Widgets
Navigation Rules¶
- Use GoRouter.of(context)
- Place navigation on the business logic layer (providers/bloc)
- Avoid:
- Navigator.of(context)
- context.push()
Others¶
- Don't use hardcoded text
Error Handling¶
- Null Safety:
// Correct String? value; // some code that may or may not assign a value to 'value' if (value != null) { final safeValue = value!.toString(); // use safeValue } // Incorrect String? value; // some code that may or may not assign a value to 'value' final unsafeValue = value!.toString();
Monitoring¶
- Use dart:developer for logging
- Add logs as needed
- Use
logErrormethod for error logging - Use
logWarningmethod for warning logging - Use
logInfomethod for info logging
Performance¶
Performance Best Practices¶
- Widget Optimization:
- Use const constructors for static widgets
- Implement keys in lists properly
- Cache expensive computations
- Use select() for provider subscriptions
- Use const for static widgets
- Use final for state-dependent widgets
- Use final for immutable variables
- Don't extract Widget as class functions
Design Patterns¶
Widget Examples¶
Broker Widget Pattern¶
class AnotherSquadWidget extends StatefulWidget {
// ignore: public_member_api_docs
const AnotherSquadWidget({super.key});
@override
State<AnotherSquadWidget> createState() => _AnotherSquadWidgetState();
}
class _AnotherSquadWidgetState extends State<AnotherSquadWidget> {
Widget? expected;
@override
void initState() {
super.initState();
Broker.shared.callWidget(
route: const ICDestination(route: 'widget/another_squad_widget'),
callback: (response) async {
if (response.status == ICStatusResponses.ok) {
setState(() {
expected = response.data;
});
}
if (response.status == ICStatusResponses.errorCommunicatorNotFound) {
setState(() {
expected = const Icon(Icons.error_outline);
});
}
},
);
}
@override
Widget build(BuildContext context) {
return expected ?? const SizedBox.shrink();
}
}
State Separation Pattern¶
class SomeScreen extends ConsumerWidget {
const SomeScreen({super.key});
static const routeName = '/some';
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(someStateNotifierProvider);
return state.when<Widget>(
data: (data) {
return SomeScreenRawWidget(data: data);
},
loading: () {
return const Center(child: CircularProgressIndicator());
},
error: (error, stackTrace) {
return const Center(child: Text('Error'));
},
);
}
}
class SomeScreenRawWidget extends StatelessWidget {
const SomeScreenRawWidget({
required this.data,
super.key,
});
final List<SomeData> data;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(20),
child: ListView(
children: data
.map(
(dataItem) => ListTile(
title: Text(dataItem.titulo),
subtitle: Text('Texto: ${dataItem.text}'),
onTap: () => GoRouter.of(context).go('/prestamos/${dataItem.id}'),
trailing: const Icon(Icons.chevron_right),
),
)
.toList(),
),
),
);
}
}