How to implement a typical feature (for example, "list products from the API and show details") using the established patterns. The canonical reference is the auth feature. Mirror it.
requirement
→ decide placement: existing layers (default) or a new module (see modules.md)
→ domain: model(s) → repository interface → service → cubit + state
→ data: DTO/parsing → repository impl (Dio / Preferences) → register in DataInit
→ domain: register service + (global) cubit in DomainInit
→ app: route (Routes enum + GoRoute) → page + widgets → BlocProvider/BlocBuilder → strings in .arb
→ tests: cubit, repository, widget
- Model:
models/<name>.dart. It's an immutable class or enum, with no JSON. (User,AppLangare examples.) - Repository interface:
repositories/<name>_repository.dart, where each method returnsFuture<ResultType<T>>.abstract class ProductRepository { Future<ResultType<List<Product>>> getProducts(); }
- Service:
services/<name>_service.dart. It orchestrates one or more repositories. It's thin today (AuthServicejust forwards), so put cross-repository and business rules here, not in cubits. - Cubit + state:
bloc/<feature>/<feature>_cubit.dartand_state.dart.- For async load/submit screens, extend
BaseCubit<T>so the state isResource<T>:class ProductsCubit extends BaseCubit<List<Product>> { final ProductService _service; ProductsCubit(this._service) : super(RSuccess(data: const [])); Future<void> load() async { isLoading(); onResult(await _service.getProducts()); } }
- For lists with local add/remove, extend
ListBlocState<T>. - Mix in
CancelableCubitMixinand wrap futures withtoCancelable(...)when a request may outlive the screen. - For multi-variant state (like
AuthState), use a sealed class and emit it as theTofResource<T>.
- For async load/submit screens, extend
- Remote calls: inject the registered
Dio(getIt<Dio>()) into the repository or a data source underdata_sources/remote/. Add path constants tonetwork/config/network_constants.dart. - Parsing: there's no codegen. Write
fromJsonby hand in a data-layer class and map it to the domain model before returning. Domain models must not know JSON. - Errors: wrap calls so every failure becomes a
Failure:try { final res = await _dio.get(NetworkConstants.productsPath); return TSuccess((res.data['products'] as List).map(ProductDto.fromJson).map((d) => d.toModel()).toList()); } on DioException catch (e) { return TError(e.toFailure()); }
- Local data: extend the
Preferencesinterface and impl for small key/value data. Anything larger needs a decision (no DB is set up). - Register:
getIt.registerLazySingleton<ProductRepository>(() => ProductRepositoryImpl(getIt()));inDataInit.
- Services:
getIt.registerLazySingleton(() => ProductService(getIt())); - A cubit is registered only if it's global (it lives for the whole app, like
AppCubit/AuthCubit). Screen cubits aren't registered. They're created in the page'sBlocProvider.
- Route: add a value to the
Routesenum and aGoRouteunder the rightShellRouteinnavigation/routers.dart. Authenticated screens go under/app, and public ones go under the first shell. UsesubPathfor nested routes. - Page:
ui/pages/<area>/<feature>/<feature>_page.dart. It provides the cubit, and a sibling_view/_formwidget renders it:BlocProvider(create: (_) => ProductsCubit(getIt())..load(), child: const ProductsView())
- Render state:
BlocBuilder<ProductsCubit, Resource<List<Product>>>and branch onRLoading/RError/RSuccess. UseFailureWidget(failure: state.exception as Failure?, onRetry: …)for errors,PrimaryButton(isLoading: …)for submits, andFormValidatorfor inputs. - Strings: add keys to both
intl_en.arbandintl_es.arb, then runcd app && dart run intl_utils:generateand useS.of(context).key. - Styling:
Dimen.*for spacing and sizes,Theme.of(context).textTheme/colorSchemeorcontext.colors. Don't hardcode values. - Analytics (optional): extend
TrackedPageonly afterAnalyticsClientis registered androuteObserveris added to GoRouter'sobservers(known-issues #4).
See testing.md. At minimum: a cubit test (states emitted for success and failure), a repository test
(DTO mapping + DioException → Failure), and a widget test for the page's loading, error, and success branches.
- No forbidden imports (modules.md).
- The repository returns
ResultType, and the cubit exposesResource. - Registrations are in the right
init.dart. Screen cubits aren't registered as singletons. - Strings are localized in all
.arbfiles, and the generated code is refreshed. - Tests are added.
melos run analyzeandmelos run formatare clean.