17th-dart-patterns

star 0

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.

SeventeenthEarth By SeventeenthEarth schedule Updated 1/24/2026

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

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

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>/:

// 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

// 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

// 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

// 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

// 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)

// 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

// 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)

// 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

// 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

// 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

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

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)

// 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

// 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

// 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

Type Pattern Example
UseCase <action>_<entity>.dart get_user.dart, create_order.dart
Repository Interface <entity>_repository.dart user_repository.dart
Repository Impl <entity>_repository_impl.dart user_repository_impl.dart
Cubit cubit.dart (in bloc folder) bloc/cubit.dart
State state.dart (part of cubit) bloc/state.dart
View view.dart view.dart
Test <original>_test.dart get_user_test.dart
Dependency dependency.dart dependency.dart
Mapper <entity>_mapper.dart user_status_mapper.dart

Class Names

Type Pattern Example
UseCase <Action><Entity> GetUser, CreateOrder
Repository (Interface) <Entity>Repository UserRepository
Repository (Impl) <Entity>RepositoryImpl UserRepositoryImpl
Cubit <Feature><Screen>Cubit UserDetailCubit
State <Feature><Screen>State UserDetailState
DataSource <Entity>DataSource UserRemoteDataSource
Mapper map<From>To<To> (function) mapUserStatusToProto

Folder Names

Type Pattern Example
Feature snake_case, singular user, order, authentication
Screen subfolder snake_case detail, list, create
Bloc subfolder bloc/ detail/bloc/

9. Domain Models

Entity with Factory Constructor

// 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

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

// 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

// 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

// 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

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

// 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

// 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

// 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

Layer Purpose Example
Port Application-facing contract AuthStatusPort
Transport Infrastructure adapter contract AuthStreamTransport
Facade Port implementation using Manager AuthStatusFacade
Manager Orchestrates transport + state AuthStreamManager

12. Common Presentation Layer

Structure

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

// 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

// 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

// 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
Install via CLI
npx skills add https://github.com/SeventeenthEarth/glm-worker-mcp --skill 17th-dart-patterns
Repository Details
star Stars 0
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator
SeventeenthEarth
SeventeenthEarth Explore all skills →