AgentSkillsCN

17th-dart-patterns

Flutter/Dart项目开发的模式与规范。 涵盖清洁架构、Cubit/BLoC、Result类型、测试、DI(GetIt)、gRPC、Repository模式。 在编写代码前参考此技能,以保持代码风格的一致性。

SKILL.md
--- frontmatter
name: 17th-dart-patterns
description: |
  Flutter/Dart project development patterns and conventions.
  Covers Clean Architecture, Cubit/BLoC, Result type, Testing, DI (GetIt), gRPC, Repository patterns.
  Reference this skill before writing code to maintain consistent patterns.

Flutter/Dart Development Patterns

This document defines patterns and conventions for writing consistent Flutter/Dart code. Uses Panther library for logging and error handling (company-wide shared package).


1. Clean Architecture

Layer Structure

code
lib/feature/<feature>/
├── domain/           # Core business logic (framework-agnostic)
│   ├── entity/       # Domain models, value objects
│   ├── model/        # Domain models (alternative naming)
│   └── repository/   # Repository contracts (abstract classes)
├── application/      # Use cases, business rules
│   ├── <action>_<entity>.dart  # UseCase implementations
│   ├── model/        # Application-level models
│   └── port/         # Port interfaces for external services
├── data/             # Data access implementations
│   ├── repository/   # Repository implementations
│   ├── source/       # Data sources (remote, local)
│   └── mapper/       # DTO ↔ Domain conversion
└── presentation/     # UI layer
    ├── <screen>/
    │   ├── bloc/     # Cubit/Bloc + State
    │   └── view.dart # Screen widget
    ├── model/        # View models, UI-specific data
    └── widget/       # Reusable widgets

Dependency Direction

code
presentation → application → domain ← data
                    ↑
                  data (implements domain interfaces)
  • domain: No Flutter, gRPC, Firebase, or external framework dependencies
  • application: Only imports domain, no infrastructure packages
  • data: Implements domain interfaces, uses gRPC/external packages
  • presentation: Calls application, uses Flutter packages

Feature Dependency Registration

Each feature has a dependency.dart file in lib/feature/<feature>/:

dart
// lib/feature/<feature>/dependency.dart
void register<Feature>() {
  GetIt.I
    // Application (Use Cases)
    ..registerFactory(() => Get<Entity>(GetIt.I()))
    ..registerFactory(() => Create<Entity>(GetIt.I()))
    // Presentation (Cubits)
    ..registerFactory(
      () => <Feature>Cubit(
        GetIt.I<SomeUseCase>(),
      ),
    )
    // Data
    ..registerLazySingleton<<Entity>Repository>(
      () => <Entity>RepositoryImpl(
        GetIt.I(),
        GetIt.I<GlobalLoggerProvider>(),
      ),
    );
}

2. Result Type Pattern

Sealed Result Class

dart
// lib/service/result/result.dart

sealed class Result<T> {
  const Result();

  Result<R> map<R>(R Function(T value) mapper) {
    return switch (this) {
      Success(:final value) => Success(mapper(value)),
      Failure(:final error) => Failure(error),
    };
  }

  R fold<R>({
    required R Function(DomainError error) onFailure,
    required R Function(T value) onSuccess,
  }) {
    return switch (this) {
      Success(:final value) => onSuccess(value),
      Failure(:final error) => onFailure(error),
    };
  }

  T getOrThrow() {
    return switch (this) {
      Success(:final value) => value,
      Failure(:final error) => throw error,
    };
  }
}

final class Success<T> extends Result<T> {
  const Success(this.value);
  final T value;
}

final class Failure<T> extends Result<T> {
  const Failure(this.error);
  final DomainError error;
}

Result Pattern Matching

dart
// Switch expression pattern (preferred)
final result = await useCase(params, logger);
switch (result) {
  case Success(:final value):
    // Handle success
    return value;
  case Failure(:final error):
    // Handle error
    return null;
}

// Fold method pattern
result.fold(
  onSuccess: (value) => Success(value),
  onFailure: (error) => Failure(toDataException(error)),
);

3. UseCase Pattern

Application Type Definitions

dart
// lib/service/type/application.dart

typedef ResultFuture<T> = Future<Result<T>>;

abstract class FutureApplicationWithParams<T, P> {
  const FutureApplicationWithParams();
  ResultFuture<T> call(P params, EventLogger logger);
}

abstract class FutureApplication<T> {
  const FutureApplication();
  ResultFuture<T> call(EventLogger logger);
}

abstract class StreamApplication<T> {
  const StreamApplication();
  Stream<T> call(EventLogger logger);
}

UseCase Implementation

dart
// lib/feature/<feature>/application/<action>_<entity>.dart

class Get<Entity> extends FutureApplicationWithParams<<Entity>Result, <Entity>Params> {
  const Get<Entity>(this._repository);

  final <Entity>Repository _repository;

  @override
  ResultFuture<<Entity>Result> call(
    <Entity>Params params,
    EventLogger logger,
  ) async {
    final context = loggerContextFromEventLogger(logger).child('Get<Entity>');
    try {
      final result = await _repository.get<Entity>(
        context: context,
        params: params,
      );

      return result.fold(
        onSuccess: (value) {
          if (!_isValidResult(value)) {
            logger.error(
              'Get<Entity> returned invalid payload',
              StateError('Invalid Get<Entity> response'),
              methodName: 'call',
              className: 'Get<Entity>',
            );
            return const Failure(
              DataException('Invalid response', AppStatus.invalidServerResponse),
            );
          }
          return Success(value);
        },
        onFailure: (error) {
          final dataError = toDataException(error);
          logger.warning(
            'Get<Entity> failed: ${dataError.message}',
            methodName: 'call',
            className: 'Get<Entity>',
          );
          return Failure(dataError);
        },
      );
    } on Exception catch (error, stackTrace) {
      logger.error(
        'Unexpected error while calling Get<Entity>',
        error,
        methodName: 'call',
        className: 'Get<Entity>',
        stackTrace: stackTrace,
      );
      return Failure(
        DataException(
          'Unexpected error: $error',
          AppStatus.unknownError,
          error: error,
          stackTrace: stackTrace,
        ),
      );
    }
  }
}

4. Cubit/BLoC Pattern

State Definition (using part of)

dart
// lib/feature/<feature>/presentation/<screen>/bloc/state.dart
part of 'cubit.dart';

class <Feature><Screen>State extends Equatable {
  const <Feature><Screen>State({
    required this.status,
    required this.isLoading,
    this.data,
    this.errorKind,
  });

  factory <Feature><Screen>State.initial() => const <Feature><Screen>State(
    status: LoadStatus.initial,
    isLoading: false,
  );

  final LoadStatus status;
  final bool isLoading;
  final <Entity>? data;
  final <Feature>ErrorKind? errorKind;

  bool get hasData => data != null;

  // Sentinel pattern for nullable fields in copyWith
  static const _errorSentinel = Object();

  <Feature><Screen>State copyWith({
    LoadStatus? status,
    bool? isLoading,
    <Entity>? data,
    Object? errorKind = _errorSentinel,
    bool clearData = false,
  }) {
    return <Feature><Screen>State(
      status: status ?? this.status,
      isLoading: isLoading ?? this.isLoading,
      data: clearData ? null : (data ?? this.data),
      errorKind: identical(errorKind, _errorSentinel)
          ? this.errorKind
          : errorKind as <Feature>ErrorKind?,
    );
  }

  @override
  List<Object?> get props => [status, isLoading, data, errorKind];
}

Cubit Implementation

dart
// lib/feature/<feature>/presentation/<screen>/bloc/cubit.dart
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:panther/panther_logger.dart';
import 'package:panther/panther_util.dart';

part 'state.dart';

class <Feature><Screen>Cubit extends Cubit<<Feature><Screen>State>
    with PantherBlocMixin<<Feature><Screen>State> {
  <Feature><Screen>Cubit(
    this._getEntity,
  ) : super(<Feature><Screen>State.initial());

  final Get<Entity> _getEntity;

  Future<void> load(<Entity>Params params) async {
    emit(state.copyWith(isLoading: true, errorKind: null));

    await withEventLogger(id, '<Feature>Load', (logger) async {
      final result = await _getEntity(params, logger);
      switch (result) {
        case Success(:final value):
          emit(
            state.copyWith(
              data: value,
              isLoading: false,
              status: LoadStatus.success,
            ),
          );
        case Failure(:final error):
          emit(
            state.copyWith(
              clearData: true,
              isLoading: false,
              status: LoadStatus.failure,
              errorKind: mapError(error),
            ),
          );
      }
    });
  }
}

Logger Mixin Pattern (Panther)

dart
// Use PantherBlocMixin for logging in Cubits
class <Feature>Cubit extends Cubit<<Feature>State>
    with PantherBlocMixin<<Feature>State> {
  // ...

  // Use withEventLogger for async operations
  await withEventLogger(id, 'OperationName', (logger) async {
    // logger is automatically scoped with cubit id
  });
}

5. Repository Pattern

Domain Repository Contract

dart
// lib/feature/<feature>/domain/repository/<entity>_repository.dart

abstract class <Entity>Repository {
  Future<Result<<Entity>>> get<Entity>({
    required LoggerContext context,
    required <Entity>Params params,
  });

  Future<Result<<Entity>>> create<Entity>({
    required LoggerContext context,
    required Create<Entity>Params params,
  });

  Future<Result<void>> delete<Entity>({
    required LoggerContext context,
    required Delete<Entity>Params params,
  });
}

Repository Implementation

dart
// lib/feature/<feature>/data/repository/<entity>_repository_impl.dart

class <Entity>RepositoryImpl implements <Entity>Repository {
  <Entity>RepositoryImpl(
    this._remoteDataSource,
    this._loggerProvider,
  );

  final <Entity>RemoteDataSource _remoteDataSource;
  final GlobalLoggerProvider _loggerProvider;

  @override
  Future<Result<<Entity>>> get<Entity>({
    required LoggerContext context,
    required <Entity>Params params,
  }) => withEventLoggerFromContext(
    loggerProvider: _loggerProvider,
    context: context.child('<Entity>Repository.get<Entity>'),
    action: (logger) => _remoteDataSource.get<Entity>(
      logger: logger,
      params: params,
    ),
  );
}

6. Testing Patterns

UseCase Unit Tests

dart
void main() {
  late Mock<Entity>Repository mockRepository;
  late MockEventLogger mockLogger;
  late Get<Entity> useCase;

  const params = <Entity>Params(id: 'uuid');
  const validResult = <Entity>(id: 'uuid', name: 'Test');

  setUp(() {
    mockRepository = Mock<Entity>Repository();
    mockLogger = MockEventLogger();
    setupMockEventLogger(mockLogger);
    useCase = Get<Entity>(mockRepository);
    provideDummy<Result<<Entity>>>(const Success(validResult));
  });

  test('returns success when repository returns valid result', () async {
    when(
      mockRepository.get<Entity>(
        context: anyNamed('context'),
        params: anyNamed('params'),
      ),
    ).thenAnswer((_) async => const Success(validResult));

    final result = await useCase(params, mockLogger);

    expect(result, isA<Success<<Entity>>>());
    expect((result as Success<<Entity>>).value, validResult);
  });

  test('converts DomainError to DataException failure', () async {
    const domainError = DomainError('not found', AppStatus.dbNotFound);
    when(
      mockRepository.get<Entity>(
        context: anyNamed('context'),
        params: anyNamed('params'),
      ),
    ).thenAnswer((_) async => const Failure(domainError));

    final result = await useCase(params, mockLogger);

    expect(result, isA<Failure<<Entity>>>());
    final error = (result as Failure<<Entity>>).error as DataException;
    expect(error.statusCode, AppStatus.dbNotFound);
  });
}

Cubit Tests with Stub Pattern

dart
class _StubGet<Entity> extends Get<Entity> {
  _StubGet<Entity>(this._result) : super(Mock<Entity>Repository());

  Result<<Entity>> _result;
  int callCount = 0;

  @override
  Future<Result<<Entity>>> call(<Entity>Params params, EventLogger logger) async {
    callCount += 1;
    return _result;
  }

  set result(Result<<Entity>> value) => _result = value;
}

void main() {
  group('<Feature>Cubit', () {
    late _StubGet<Entity> getEntity;
    late <Feature>Cubit cubit;
    late MockGlobalLoggerProvider loggerProvider;
    late MockEventLogger eventLogger;

    setUp(() async {
      await GetIt.I.reset();
      loggerProvider = MockGlobalLoggerProvider();
      eventLogger = MockEventLogger();
      when(loggerProvider.event(any, any, eventId: anyNamed('eventId')))
          .thenReturn(eventLogger);
      setupMockEventLogger(eventLogger);
      GetIt.I.registerSingleton<GlobalLoggerProvider>(loggerProvider);

      getEntity = _StubGet<Entity>(const Success(testEntity));
      cubit = <Feature>Cubit(getEntity);
    });

    tearDown(() async {
      if (!cubit.isClosed) {
        await cubit.close();
      }
      await GetIt.I.reset();
    });

    test('emits data on successful load', () async {
      await cubit.load(params);

      expect(cubit.state.data, testEntity);
      expect(cubit.state.errorKind, isNull);
      expect(getEntity.callCount, 1);
    });
  });
}

Mock Definitions (centralized)

dart
// test/test_util/mock_definitions.dart
@GenerateMocks([
  <Entity>Repository,
  EventLogger,
  GlobalLoggerProvider,
  // ... other interfaces
])
void main() {}

// Generate with: dart run build_runner build

7. Dependency Injection (GetIt)

Global Initialization

dart
// lib/infrastructure/dependency_injector/initialize.dart

Future<void> initDependencies() async {
  // 1. Initialize Firebase
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );

  // 2. Initialize external services
  final firebaseAuth = FirebaseAuth.instance;

  // 3. Initialize logging (Panther)
  final logManager = FileLogManager(fileManagerParams);
  await logManager.init();

  // 4. Initialize SharedPreferences
  final sharedPreferences = await SharedPreferences.getInstance();

  // 5. Register global singletons
  GetIt.I
    ..registerLazySingleton<FileLogManager>(() => logManager)
    ..registerLazySingleton<GlobalLoggerProvider>(
      () => GlobalLoggerProvider.init(GetIt.I(), logLevel: logLevel),
    )
    ..registerLazySingleton(() => firebaseAuth)
    ..registerLazySingleton(() => sharedPreferences);

  // 6. Register gRPC infrastructure
  GetIt.I
    ..registerLazySingleton<RequestIdProvider>(RequestIdProvider.new)
    ..registerLazySingletonAsync<ClientChannel>(
      GrpcClientChannelFactory.create,
    );

  await GetIt.I.isReady<ClientChannel>();

  // 7. Register features
  registerAuthentication();
  registerFeatureA();
  registerFeatureB();
  // ... other features
}

Registration Patterns

dart
// Singleton - shared instance
GetIt.I.registerLazySingleton<<Entity>Repository>(
  () => <Entity>RepositoryImpl(GetIt.I(), GetIt.I<GlobalLoggerProvider>()),
);

// Factory - new instance each time
GetIt.I.registerFactory(() => Get<Entity>(GetIt.I()));

// Factory with explicit type
GetIt.I.registerFactory<Validate<Entity>Input>(Validate<Entity>Input.new);

// Async singleton
GetIt.I.registerLazySingletonAsync<ClientChannel>(
  GrpcClientChannelFactory.create,
);
await GetIt.I.isReady<ClientChannel>();

8. Naming Conventions

File Names

TypePatternExample
UseCase<action>_<entity>.dartget_user.dart, create_order.dart
Repository Interface<entity>_repository.dartuser_repository.dart
Repository Impl<entity>_repository_impl.dartuser_repository_impl.dart
Cubitcubit.dart (in bloc folder)bloc/cubit.dart
Statestate.dart (part of cubit)bloc/state.dart
Viewview.dartview.dart
Test<original>_test.dartget_user_test.dart
Dependencydependency.dartdependency.dart
Mapper<entity>_mapper.dartuser_status_mapper.dart

Class Names

TypePatternExample
UseCase<Action><Entity>GetUser, CreateOrder
Repository (Interface)<Entity>RepositoryUserRepository
Repository (Impl)<Entity>RepositoryImplUserRepositoryImpl
Cubit<Feature><Screen>CubitUserDetailCubit
State<Feature><Screen>StateUserDetailState
DataSource<Entity>DataSourceUserRemoteDataSource
Mappermap<From>To<To> (function)mapUserStatusToProto

Folder Names

TypePatternExample
Featuresnake_case, singularuser, order, authentication
Screen subfoldersnake_casedetail, list, create
Bloc subfolderbloc/detail/bloc/

9. Domain Models

Entity with Factory Constructor

dart
// lib/feature/<feature>/domain/entity/<entity>_params.dart

class <Entity>Params {
  const <Entity>Params({
    required this.id,
    this.includeDetails = false,
  });

  final String id;
  final bool includeDetails;
}

Immutable Models with Equatable

dart
import 'package:equatable/equatable.dart';

class <Entity> extends Equatable {
  const <Entity>({
    required this.id,
    required this.name,
    required this.status,
  });

  final String id;
  final String name;
  final <Entity>Status status;

  @override
  List<Object?> get props => [id, name, status];
}

10. gRPC Integration

gRPC Client Wrapper

dart
// lib/infrastructure/grpc/client/<entity>_grpc_client.dart

class <Entity>GrpcClient {
  <Entity>GrpcClient(
    this._channel,
    this._requestIdInterceptor,
    this._authInterceptor,
    this._testHeaderInterceptor,
  );

  final ClientChannel _channel;
  final RequestIdInterceptor _requestIdInterceptor;
  final AuthInterceptor _authInterceptor;
  final TestHeaderInterceptor _testHeaderInterceptor;

  <Entity>ServiceClient get client => <Entity>ServiceClient(
    _channel,
    interceptors: [
      _requestIdInterceptor,
      _authInterceptor,
      _testHeaderInterceptor,
    ],
  );
}

Service Facade Pattern

dart
// lib/infrastructure/grpc/<entity>/<entity>_service_facade.dart

class <Entity>ServiceFacade {
  <Entity>ServiceFacade(this._client, this._requestIdProvider);

  final <Entity>GrpcClient _client;
  final RequestIdProvider _requestIdProvider;

  Future<Get<Entity>Response> get<Entity>(Get<Entity>Request request) async {
    return _client.client.get<Entity>(request);
  }
}

Data Source Implementation

dart
// lib/feature/<feature>/data/source/<entity>_remote_data_source.dart

abstract class <Entity>RemoteDataSource {
  Future<Result<<Entity>>> get<Entity>({
    required EventLogger logger,
    required <Entity>Params params,
  });
}

class <Entity>RemoteDataSourceImpl implements <Entity>RemoteDataSource {
  <Entity>RemoteDataSourceImpl(this._facade);

  final <Entity>ServiceFacade _facade;

  @override
  Future<Result<<Entity>>> get<Entity>({
    required EventLogger logger,
    required <Entity>Params params,
  }) async {
    try {
      final request = Get<Entity>Request()
        ..id = params.id;

      final response = await _facade.get<Entity>(request);

      return Success(<Entity>(
        id: response.id,
        name: response.name,
        status: map<Entity>Status(response.status),
      ));
    } on GrpcError catch (e) {
      return Failure(mapGrpcError(e));
    }
  }
}

11. Service Layer (Hexagonal Architecture)

Service Structure

code
lib/service/
├── authentication/           # Cross-cutting auth service
│   ├── port/                # Inbound ports (interfaces)
│   │   ├── auth_status_port.dart
│   │   └── user_id_provider.dart
│   ├── transport/           # Outbound adapters (implementations)
│   │   ├── auth_stream_transport.dart
│   │   └── firebase_auth_stream_transport.dart
│   ├── application/         # Application services
│   │   └── facade/          # Port implementations
│   ├── manager/             # State managers
│   ├── model/               # Service models
│   └── dependency.dart      # DI registration
├── result/                   # Result type utilities
├── logger/                   # Logging utilities (Panther integration)
├── request_id/              # Request ID generation
├── retry/                   # Retry policies
└── type/                    # Shared type definitions

Port Interface Pattern

dart
// lib/service/<service>/port/<service>_port.dart

/// Application-facing port for <service>.
abstract class <Service>Port {
  const <Service>Port();

  /// Starts the service.
  ResultFuture<void> attach(EventLogger logger);

  /// Returns event stream.
  ResultStream<<Service>Event> stream(LoggerContext context);

  /// Stops the service and cleans up resources.
  ResultFuture<void> release(EventLogger logger);

  /// Returns the latest cached snapshot, if any.
  <Service>Snapshot? getSnapshot();
}

Transport Interface Pattern

dart
// lib/service/<service>/transport/<service>_transport.dart

/// Transport abstraction for <service>.
abstract class <Service>Transport {
  const <Service>Transport();

  /// Starts streaming transport-level events.
  Stream<Transport<Service>Event> stream({required StreamLogger logger});

  /// Cancels the active stream, if any.
  Future<void> cancel({required StreamLogger logger});
}

sealed class Transport<Service>Event extends Equatable {
  const Transport<Service>Event();
}

class Transport<Service>Connected extends Transport<Service>Event {
  const Transport<Service>Connected(this.data);
  final Transport<Service>Data data;

  @override
  List<Object?> get props => [data];
}

class Transport<Service>Disconnected extends Transport<Service>Event {
  const Transport<Service>Disconnected();

  @override
  List<Object?> get props => [];
}

Facade Implementation Pattern

dart
// lib/service/<service>/application/facade/<service>_facade.dart

class <Service>Facade implements <Service>Port {
  <Service>Facade(this._manager, this._loggerProvider);

  final <Service>Manager _manager;
  final GlobalLoggerProvider _loggerProvider;

  @override
  ResultFuture<void> attach(EventLogger logger) {
    logger.debug(
      'Attaching <service> stream',
      methodName: 'attach',
      className: '<Service>Facade',
    );

    final streamLogger = _loggerProvider.stream('<Service>Facade::Stream');
    return _manager.attach(logger: streamLogger);
  }

  @override
  ResultStream<<Service>Event> stream(LoggerContext context) {
    return _manager.stream(context);
  }
}

Port vs Transport

LayerPurposeExample
PortApplication-facing contractAuthStatusPort
TransportInfrastructure adapter contractAuthStreamTransport
FacadePort implementation using ManagerAuthStatusFacade
ManagerOrchestrates transport + stateAuthStreamManager

12. Common Presentation Layer

Structure

code
lib/common/presentation/
├── asset/              # Shared asset references
│   └── app_illustrations.dart
├── bloc/               # Common BLoC utilities
│   └── safe_emit_mixin.dart
├── theme/              # App theming
│   ├── app_colors.dart      # Semantic colors (ThemeExtension)
│   ├── app_gradients.dart   # Gradient definitions
│   ├── app_radius.dart      # Border radius tokens
│   ├── app_shadows.dart     # Shadow definitions
│   ├── app_spacing.dart     # Spacing tokens
│   └── app_theme.dart       # Theme configuration
├── util/               # Presentation utilities
└── widget/             # Reusable widgets

ThemeExtension Pattern

dart
// lib/common/presentation/theme/app_colors.dart

@immutable
class AppColors extends ThemeExtension<AppColors> {
  const AppColors({
    required this.success,
    required this.onSuccess,
    required this.successContainer,
    required this.onSuccessContainer,
    required this.info,
    required this.onInfo,
    required this.infoContainer,
    required this.onInfoContainer,
    required this.warning,
    required this.onWarning,
    required this.warningContainer,
    required this.onWarningContainer,
  });

  final Color success;
  final Color onSuccess;
  final Color successContainer;
  final Color onSuccessContainer;
  // ... other colors

  @override
  AppColors copyWith({...}) { ... }

  @override
  AppColors lerp(ThemeExtension<AppColors>? other, double t) { ... }

  /// Light theme semantic colors
  static const AppColors light = AppColors(
    success: Color(0xFF22C55E),
    onSuccess: Color(0xFFFFFFFF),
    successContainer: Color(0xFFDCFCE7),
    onSuccessContainer: Color(0xFF166534),
    // ...
  );

  /// Dark theme semantic colors
  static const AppColors dark = AppColors(
    success: Color(0xFF4ADE80),
    onSuccess: Color(0xFF166534),
    // ...
  );
}

Using ThemeExtension in Widgets

dart
// Access semantic colors
final appColors = Theme.of(context).extension<AppColors>()!;

Container(
  color: appColors.successContainer,
  child: Text(
    'Success!',
    style: TextStyle(color: appColors.onSuccessContainer),
  ),
)

Design Token Classes

dart
// lib/common/presentation/theme/app_spacing.dart
abstract class AppSpacing {
  static const double xs = 4.0;
  static const double sm = 8.0;
  static const double md = 16.0;
  static const double lg = 24.0;
  static const double xl = 32.0;
}

// lib/common/presentation/theme/app_radius.dart
abstract class AppRadius {
  static const double sm = 8.0;
  static const double md = 12.0;
  static const double lg = 16.0;
  static const double full = 9999.0;
}

Quick Reference Checklist

Adding New Feature

  1. lib/feature/<name>/domain/entity/ - Define domain models
  2. lib/feature/<name>/domain/repository/ - Repository contracts
  3. lib/feature/<name>/application/ - Define UseCases
  4. lib/feature/<name>/data/source/ - Data sources
  5. lib/feature/<name>/data/repository/ - Repository implementations
  6. lib/feature/<name>/presentation/ - Cubits and views
  7. lib/feature/<name>/dependency.dart - DI registration
  8. Add register<Feature>() to initDependencies()
  9. Add tests mirroring lib structure

Adding New UseCase

  1. Define Params class in domain/entity/
  2. Extend appropriate FutureApplication* type
  3. Inject Repository via constructor
  4. Use Result pattern for returns
  5. Add proper logging with context (Panther)
  6. Handle all error cases
  7. Register in dependency.dart
  8. Write unit tests

Adding New Cubit

  1. Create bloc/ subfolder
  2. Create state.dart with part of 'cubit.dart'
  3. Extend Cubit<State> with PantherBlocMixin
  4. Define initial() factory for state
  5. Use Equatable for state
  6. Implement copyWith with sentinel pattern
  7. Use withEventLogger for async operations
  8. Register as factory in dependency.dart

Adding New Service (Hexagonal)

  1. Create lib/service/<name>/ folder
  2. Define port interface in port/<name>_port.dart
  3. Define transport interface in transport/<name>_transport.dart
  4. Implement transport (e.g., Firebase, gRPC) in transport/
  5. Create manager in manager/ for state orchestration
  6. Implement facade in application/facade/
  7. Define models in model/
  8. Register in dependency.dart

Adding Common Widget

  1. Create in lib/common/presentation/widget/
  2. Use design tokens from theme/ (AppSpacing, AppRadius, etc.)
  3. Access semantic colors via Theme.of(context).extension<AppColors>()
  4. Keep widget stateless when possible
  5. Document public API with dartdoc comments