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
| 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
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
| 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
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
- •
lib/feature/<name>/domain/entity/- Define domain models - •
lib/feature/<name>/domain/repository/- Repository contracts - •
lib/feature/<name>/application/- Define UseCases - •
lib/feature/<name>/data/source/- Data sources - •
lib/feature/<name>/data/repository/- Repository implementations - •
lib/feature/<name>/presentation/- Cubits and views - •
lib/feature/<name>/dependency.dart- DI registration - • Add
register<Feature>()toinitDependencies() - • Add tests mirroring lib structure
Adding New UseCase
- • Define Params class in
domain/entity/ - • Extend appropriate
FutureApplication*type - • Inject Repository via constructor
- • Use Result pattern for returns
- • Add proper logging with context (Panther)
- • Handle all error cases
- • Register in
dependency.dart - • Write unit tests
Adding New Cubit
- • Create
bloc/subfolder - • Create
state.dartwithpart of 'cubit.dart' - • Extend
Cubit<State>withPantherBlocMixin - • Define
initial()factory for state - • Use
Equatablefor state - • Implement
copyWithwith sentinel pattern - • Use
withEventLoggerfor async operations - • Register as factory in
dependency.dart
Adding New Service (Hexagonal)
- • Create
lib/service/<name>/folder - • Define port interface in
port/<name>_port.dart - • Define transport interface in
transport/<name>_transport.dart - • Implement transport (e.g., Firebase, gRPC) in
transport/ - • Create manager in
manager/for state orchestration - • Implement facade in
application/facade/ - • Define models in
model/ - • Register in
dependency.dart
Adding Common Widget
- • Create in
lib/common/presentation/widget/ - • Use design tokens from
theme/(AppSpacing, AppRadius, etc.) - • Access semantic colors via
Theme.of(context).extension<AppColors>() - • Keep widget stateless when possible
- • Document public API with dartdoc comments