Saltar a contenido

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: /lib folder
  • Features structure: /lib/src/features/<feature_name>/
  • data/ - Models and repositories
  • domain/ - Entities and use cases
  • presentation/ - Widgets, screens and state management

Naming Conventions

  • Data Layer:
  • Models: {Name}Model in data/models/
  • Repositories: {Name}Repository in data/repositories/
  • Domain Layer:
  • Entities: No suffix (e.g., User) in domain/entities/
  • Use Cases: {Action}{Entity}UseCase in domain/usecases/
  • Presentation Layer:
  • Providers: {name}Provider in presentation/providers/
  • Widgets: {Name}Widget in presentation/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:

  1. 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).
  2. 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.

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 Injector class 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 @riverpod for 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
  • 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 logError method for error logging
  • Use logWarning method for warning logging
  • Use logInfo method 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(),
        ),
      ),
    );
  }
}