-
Folder tree. Create:
lib/features/<name>/
data/{datasources,models,repositories}/
domain/{entities,repositories,usecases}/
presentation/{bloc,pages,widgets}/
-
Domain entity — domain/entities/<name>.dart. Plain class <Name> extends Equatable; NO Flutter imports, NO fromJson (entities are pure). Fields final, override props.
-
Repository contract — domain/repositories/<name>_repository.dart. abstract class <Name>Repository whose methods return the entity directly and throw a Failure on error (lib/core/error/failure.dart: NetworkFailure/ServerFailure/UnknownFailure). NO Either, NO Result<T>.
-
Use cases — domain/usecases/<verb>_<name>.dart. One class per action; constructor takes the repo, call() delegates:
class Get<Name>s {
Get<Name>s(this._repo);
final <Name>Repository _repo;
Future<List<<Name>>> call() => _repo.get<Name>s();
}
-
Data model — data/models/<name>_model.dart. class <Name>Model extends Equatable with hand-written factory fromJson (map snake_case JSON keys to camelCase by hand), toJson, toEntity(), and props. NO @JsonSerializable, NO part '*.g.dart'. Live example: lib/features/auth/data/models/auth_token_model.dart.
-
Remote datasource — data/datasources/<name>_remote_datasource.dart. Call ApiClient (get/post/put/delete<T>(path, parse: ..., query:/body: ...) → ApiResult<T>); never touch Dio directly. Add each path as a static const to lib/core/network/api_endpoints.dart — create that file if it does not exist yet (plain abstract final class ApiEndpoints { static const ... }). See 04_how_to_add_new_api.md.
-
Repository impl — data/repositories/<name>_repository_impl.dart. Implements the contract; wrap datasource calls in try/catch and convert the typed ApiException to a Failure via e.toFailure(), then map models to entities:
@override
Future<List<<Name>>> get<Name>s() async {
try {
final res = await _ds.get<Name>s();
return res.map((m) => m.toEntity()).toList();
} on ApiException catch (e) {
throw e.toFailure();
}
}
-
Bloc events — presentation/bloc/<name>_event.dart. sealed/base <Name>Event extends Equatable; one event per user action (e.g. <Name>LoadRequested).
-
Bloc states — presentation/bloc/<name>_state.dart. <Name>Initial / <Name>Loading / <Name>Loaded(data) / <Name>Error(Failure), all Equatable.
-
Bloc — presentation/bloc/<name>_bloc.dart. extends Bloc<<Name>Event, <Name>State>; each handler emits Loading, calls the use case inside try/catch, emits Loaded on success and Error(f) on Failure catch (f). NO .fold().
-
Pages & widgets — presentation/pages/, presentation/widgets/. Reuse lib/shared/ui/ (AppButton, AppCard, AppTextField, AppTopBar, EmptyState/ErrorState/LoadingState, …). Use design tokens only: AppSpacing/AppRadii, context.appColors, Theme.of(context).textTheme.* — never raw colors/spacing and no AppTextStyles class. RTL-safe (EdgeInsetsDirectional/start/end). The page exposes static const path.
-
Manual DI — in configureDependencies() (lib/core/di/injection.dart), add hand-written lines: registerLazySingleton for datasource, repository, and each use case; registerFactory for the bloc:
getIt
..registerLazySingleton<<Name>RemoteDataSource>(() => <Name>RemoteDataSource(getIt()))
..registerLazySingleton<<Name>Repository>(() => <Name>RepositoryImpl(getIt()))
..registerFactory<<Name>Bloc>(() => <Name>Bloc(Get<Name>s(getIt())));
NO annotations, NO injection.config.dart.
-
Route + l10n. Add the route in lib/core/router/app_router.dart using the page's static const path. Add every user-facing string to both lib/l10n/app_en.arb (template) and lib/l10n/app_ar.arb, then run flutter gen-l10n; access via context.l10n.<key>. Never edit generated l10n in lib/core/l10n/. See 05_how_to_add_new_language.md.