[{"data":1,"prerenderedAt":87},["ShallowReactive",2],{"tutorial-architettura-a-feature-in-flutter-moduli-confini-e-regole-di-dipendenza":3,"comments-tutorial-architettura-a-feature-in-flutter-moduli-confini-e-regole-di-dipendenza":86},{"id":4,"title":5,"slug":6,"excerpt":7,"intro":8,"cover_image":9,"cover_remote_url":10,"cover_credit":11,"video_url":15,"video":16,"difficulty":20,"estimated_minutes":21,"flutter_version":22,"status":23,"published_at":24,"meta_title":25,"meta_description":26,"category":27,"author":31,"steps":34},101,"Architettura a feature in Flutter: moduli, confini e regole di dipendenza","architettura-a-feature-in-flutter-moduli-confini-e-regole-di-dipendenza","Struttura un'app Flutter complessa con architettura feature-first: layer domain\u002Fdata\u002Fpresentation, API pubblica per ogni modulo, composition root e regole di import verificate automaticamente in CI.","Quando un'app Flutter supera le poche decine di schermate, la classica divisione per tipo (`screens\u002F`, `models\u002F`, `services\u002F`) smette di funzionare: ogni modifica tocca mezza codebase, i merge conflict esplodono e nessuno sa più chi può chiamare cosa.\n\nLa risposta è un'**architettura a feature** (feature-first) con **confini espliciti tra moduli**: ogni funzionalità è una cartella autonoma con i suoi layer interni, espone una piccola API pubblica e nasconde tutto il resto. I confini non restano però nella documentazione: in questo tutorial li rendiamo *eseguibili*, con regole di lint e un controllo automatico in CI che fa fallire la build se qualcuno importa il file sbagliato.\n\nAlla fine avrai:\n\n- una struttura `lib\u002Ffeatures\u002F\u003Cfeature>\u002F{domain,data,presentation}` più `core\u002F` e `shared\u002F`;\n- un **barrel file** per feature che funge da facciata pubblica;\n- una **composition root** che collega le implementazioni ai contratti;\n- routing modulare con `go_router` delegato alle singole feature;\n- regole di dipendenza applicate con `import_lint` e uno script di verifica.\n\n> Prerequisiti: buona conoscenza di Dart moderno (classi sealed, `abstract interface class`), di un gestore di stato (useremo Riverpod) e dei fondamenti di testing.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Ftutorials\u002Ffccd8fa5-a280-4859-a3ec-f598ce722d01.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1545943745-70ddfbca80d1?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3ODk5NjUxMTV8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"Sonder Bridge Photography","https:\u002F\u002Funsplash.com\u002F@sonderbridgephotography","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fgrey-concrete-multi-storey-building-x04-pgPs-Ug","https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=b6wB6LRKozI",{"youtube_id":17,"duration_seconds":18,"published_at":19},"b6wB6LRKozI",159,"2026-09-21T10:20:00+00:00","advanced",60,"3.x (Dart 3.3+)","published","2026-09-21T04:31:55+00:00","Architettura a feature in Flutter: moduli e confini","Guida avanzata all'architettura feature-first in Flutter: layer, API pubblica dei moduli, composition root e regole di dipendenza verificate in CI.",{"id":28,"name":29,"slug":30},3,"Best practice","best-practice",{"id":32,"name":33},1,"Flutter Bot",[35,43,51,57,64,71,79],{"id":36,"position":32,"title":37,"body":38,"code_snippet":39,"code_language":40,"expected_result":41,"demo_url":42,"video_url":42},674,"Definire la struttura feature-first e le regole di dipendenza","Il primo passo è decidere **dove vive il codice** e **chi può importare chi**. Adottiamo tre zone:\n\n- `lib\u002Fcore\u002F`: infrastruttura trasversale senza logica di business (HTTP client, logger, errori, risultati, DI). Non conosce nessuna feature.\n- `lib\u002Fshared\u002F`: widget e utility di UI riutilizzabili (design system). Non conosce nessuna feature.\n- `lib\u002Ffeatures\u002F\u003Cnome>\u002F`: una cartella per funzionalità, con i layer `domain`, `data`, `presentation` e un barrel file `\u003Cnome>.dart` che ne rappresenta l'**API pubblica**.\n\nLe regole (che renderemo eseguibili nel passo 6) sono:\n\n1. `domain` non importa `data` né `presentation`, **né `package:flutter\u002F*`**: è Dart puro e testabile in isolamento.\n2. `data` può importare `domain` e `core`, mai `presentation`.\n3. `presentation` può importare `domain` e `shared`, mai `data` (accede ai dati solo tramite i contratti del dominio).\n4. Una feature **non importa file interni di un'altra feature**: può usare solo il suo barrel `features\u002F\u003Caltra>\u002F\u003Caltra>.dart`.\n5. Solo la composition root (`lib\u002Fapp\u002F`) può vedere le implementazioni concrete di tutte le feature.\n\nQuesta gerarchia produce un grafo aciclico: se domani `orders` diventa un package separato, basta spostare la cartella.","lib\u002F\n├── app\u002F                      # composition root: entrypoint, DI, router, theme\n│   ├── app.dart\n│   ├── bootstrap.dart\n│   └── router.dart\n├── core\u002F                     # infrastruttura trasversale, zero business logic\n│   ├── error\u002Ffailure.dart\n│   ├── network\u002Fapi_client.dart\n│   └── result.dart\n├── shared\u002F                   # design system e widget riutilizzabili\n│   └── widgets\u002Fapp_error_view.dart\n└── features\u002F\n    ├── orders\u002F\n    │   ├── orders.dart            # BARREL: unica API pubblica della feature\n    │   ├── orders_module.dart     # rotte + override di provider da esporre all'app\n    │   ├── domain\u002F\n    │   │   ├── entities\u002Forder.dart\n    │   │   ├── repositories\u002Forder_repository.dart\n    │   │   └── usecases\u002Fget_orders.dart\n    │   ├── data\u002F\n    │   │   ├── dto\u002Forder_dto.dart\n    │   │   ├── datasources\u002Forder_remote_data_source.dart\n    │   │   └── repositories\u002Forder_repository_impl.dart\n    │   └── presentation\u002F\n    │       ├── controllers\u002Forders_controller.dart\n    │       └── screens\u002Forders_screen.dart\n    └── auth\u002F\n        └── auth.dart","text","Una struttura di cartelle chiara e un contratto scritto su chi può importare cosa: la base di tutti i passi successivi.",null,{"id":44,"position":45,"title":46,"body":47,"code_snippet":48,"code_language":49,"expected_result":50,"demo_url":42,"video_url":42},675,2,"Modellare il dominio della feature: entità, contratti e use case","Il `domain` è il cuore del modulo e deve essere **Dart puro**: nessun `package:flutter`, nessun `dio`, nessun `json`. Contiene:\n\n- **entità** immutabili che rappresentano il linguaggio del business;\n- **contratti** (`abstract interface class`) dei repository: descrivono *cosa* serve, non *come* ottenerlo;\n- **use case**: unità di intenzione applicativa, utili per rendere esplicite le operazioni e per intercettare policy trasversali (permessi, analytics).\n\nUsiamo anche un tipo `Result` in `core\u002F` per evitare eccezioni non tipizzate che attraversano i confini: il dominio dichiara i suoi errori, la presentation li gestisce esaustivamente grazie alle classi `sealed`.\n\n> Regola pratica: se una classe del dominio ha bisogno di `BuildContext` o di una libreria HTTP, sta sbagliando layer.","\u002F\u002F lib\u002Fcore\u002Fresult.dart\nsealed class Result\u003CT> {\n  const Result();\n}\n\nfinal class Ok\u003CT> extends Result\u003CT> {\n  const Ok(this.value);\n  final T value;\n}\n\nfinal class Err\u003CT> extends Result\u003CT> {\n  const Err(this.failure);\n  final Failure failure;\n}\n\n\u002F\u002F lib\u002Fcore\u002Ferror\u002Ffailure.dart\nsealed class Failure {\n  const Failure(this.message);\n  final String message;\n}\n\nfinal class NetworkFailure extends Failure {\n  const NetworkFailure([super.message = 'Connessione non disponibile']);\n}\n\nfinal class ServerFailure extends Failure {\n  const ServerFailure(super.message, {required this.statusCode});\n  final int statusCode;\n}\n\n\u002F\u002F lib\u002Ffeatures\u002Forders\u002Fdomain\u002Fentities\u002Forder.dart\nenum OrderStatus { pending, paid, shipped, cancelled }\n\nfinal class Order {\n  const Order({\n    required this.id,\n    required this.customer,\n    required this.total,\n    required this.status,\n  });\n\n  final String id;\n  final String customer;\n  final double total;\n  final OrderStatus status;\n\n  bool get isShippable => status == OrderStatus.paid;\n}\n\n\u002F\u002F lib\u002Ffeatures\u002Forders\u002Fdomain\u002Frepositories\u002Forder_repository.dart\nabstract interface class OrderRepository {\n  Future\u003CResult\u003CList\u003COrder>>> fetchOrders({int page = 1});\n  Future\u003CResult\u003COrder>> markAsShipped(String orderId);\n}\n\n\u002F\u002F lib\u002Ffeatures\u002Forders\u002Fdomain\u002Fusecases\u002Fget_orders.dart\nfinal class GetOrders {\n  const GetOrders(this._repository);\n  final OrderRepository _repository;\n\n  Future\u003CResult\u003CList\u003COrder>>> call({int page = 1}) {\n    assert(page > 0, 'La pagina parte da 1');\n    return _repository.fetchOrders(page: page);\n  }\n}","dart","Il package `domain` compila senza alcuna dipendenza da Flutter: puoi eseguirne i test con `dart test` e riutilizzarlo su server o CLI.",{"id":52,"position":28,"title":53,"body":54,"code_snippet":55,"code_language":49,"expected_result":56,"demo_url":42,"video_url":42},676,"Implementare il layer data: DTO, data source e mapping verso il dominio","Il layer `data` è l'unico che conosce il formato dei dati esterni. Tre accorgimenti fondamentali per mantenere il confine:\n\n1. **I DTO non escono mai dalla feature.** Le API di rete parlano `OrderDto`, il repository converte in `Order` e restituisce solo entità di dominio.\n2. **Le eccezioni non escono mai dal repository.** `DioException`, `SocketException` e simili vengono tradotte in `Failure` tipizzate.\n3. **Il data source è a sua volta un'interfaccia**, così nei test puoi sostituirlo senza toccare HTTP.\n\nIn questo modo un cambio di backend (REST → GraphQL) tocca solo `data\u002F`, mentre `domain\u002F` e `presentation\u002F` restano intatti: è esattamente questo il valore dei confini.","\u002F\u002F lib\u002Ffeatures\u002Forders\u002Fdata\u002Fdto\u002Forder_dto.dart\nimport '..\u002F..\u002Fdomain\u002Fentities\u002Forder.dart';\n\nfinal class OrderDto {\n  const OrderDto({\n    required this.id,\n    required this.customerName,\n    required this.totalCents,\n    required this.status,\n  });\n\n  factory OrderDto.fromJson(Map\u003CString, dynamic> json) => OrderDto(\n        id: json['id'] as String,\n        customerName: json['customer_name'] as String,\n        totalCents: json['total_cents'] as int,\n        status: json['status'] as String,\n      );\n\n  final String id;\n  final String customerName;\n  final int totalCents;\n  final String status;\n\n  Order toDomain() => Order(\n        id: id,\n        customer: customerName,\n        total: totalCents \u002F 100,\n        status: OrderStatus.values.firstWhere(\n          (s) => s.name == status,\n          orElse: () => OrderStatus.pending,\n        ),\n      );\n}\n\n\u002F\u002F lib\u002Ffeatures\u002Forders\u002Fdata\u002Fdatasources\u002Forder_remote_data_source.dart\nimport '..\u002F..\u002F..\u002F..\u002Fcore\u002Fnetwork\u002Fapi_client.dart';\nimport '..\u002Fdto\u002Forder_dto.dart';\n\nabstract interface class OrderRemoteDataSource {\n  Future\u003CList\u003COrderDto>> getOrders({required int page});\n  Future\u003COrderDto> ship(String orderId);\n}\n\nfinal class HttpOrderRemoteDataSource implements OrderRemoteDataSource {\n  const HttpOrderRemoteDataSource(this._client);\n  final ApiClient _client;\n\n  @override\n  Future\u003CList\u003COrderDto>> getOrders({required int page}) async {\n    final json = await _client.getList('\u002Forders', query: {'page': page});\n    return json.map(OrderDto.fromJson).toList(growable: false);\n  }\n\n  @override\n  Future\u003COrderDto> ship(String orderId) async {\n    final json = await _client.post('\u002Forders\u002F$orderId\u002Fship');\n    return OrderDto.fromJson(json);\n  }\n}\n\n\u002F\u002F lib\u002Ffeatures\u002Forders\u002Fdata\u002Frepositories\u002Forder_repository_impl.dart\nimport 'dart:io';\nimport '..\u002F..\u002F..\u002F..\u002Fcore\u002Ferror\u002Ffailure.dart';\nimport '..\u002F..\u002F..\u002F..\u002Fcore\u002Fresult.dart';\nimport '..\u002F..\u002Fdomain\u002Fentities\u002Forder.dart';\nimport '..\u002F..\u002Fdomain\u002Frepositories\u002Forder_repository.dart';\nimport '..\u002Fdatasources\u002Forder_remote_data_source.dart';\n\nfinal class OrderRepositoryImpl implements OrderRepository {\n  const OrderRepositoryImpl(this._remote);\n  final OrderRemoteDataSource _remote;\n\n  @override\n  Future\u003CResult\u003CList\u003COrder>>> fetchOrders({int page = 1}) =>\n      _guard(() async {\n        final dtos = await _remote.getOrders(page: page);\n        return dtos.map((dto) => dto.toDomain()).toList(growable: false);\n      });\n\n  @override\n  Future\u003CResult\u003COrder>> markAsShipped(String orderId) =>\n      _guard(() async => (await _remote.ship(orderId)).toDomain());\n\n  Future\u003CResult\u003CT>> _guard\u003CT>(Future\u003CT> Function() action) async {\n    try {\n      return Ok(await action());\n    } on SocketException {\n      return const Err(NetworkFailure());\n    } on HttpException catch (e) {\n      return Err(ServerFailure(e.message, statusCode: 500));\n    }\n  }\n}","Il repository restituisce sempre `Result\u003C...>` con entità di dominio: nessun DTO e nessuna eccezione di rete attraversa il confine della feature.",{"id":58,"position":59,"title":60,"body":61,"code_snippet":62,"code_language":49,"expected_result":63,"demo_url":42,"video_url":42},677,4,"Presentation isolata e barrel file: definire l'API pubblica del modulo","La presentation dipende **solo** dal dominio. Il controller riceve lo use case via provider e trasforma i `Result` in uno stato UI `sealed`, gestibile esaustivamente con `switch`.\n\nIl pezzo chiave del passo è il **barrel file** `orders.dart`: è l'unico punto di ingresso della feature per il resto dell'app. Esporta la schermata (o meglio la rotta), le entità che davvero servono all'esterno e nulla di più. Repository concreti, DTO, data source e controller restano invisibili.\n\n> Se un file non è elencato nel barrel, nessun'altra feature deve poterlo importare. Nel passo 6 lo faremo rispettare al linter.","\u002F\u002F lib\u002Ffeatures\u002Forders\u002Fpresentation\u002Fcontrollers\u002Forders_controller.dart\nimport 'package:riverpod_annotation\u002Friverpod_annotation.dart';\nimport '..\u002F..\u002F..\u002F..\u002Fcore\u002Ferror\u002Ffailure.dart';\nimport '..\u002F..\u002F..\u002F..\u002Fcore\u002Fresult.dart';\nimport '..\u002F..\u002Fdomain\u002Fentities\u002Forder.dart';\nimport '..\u002F..\u002Fdomain\u002Fusecases\u002Fget_orders.dart';\n\nsealed class OrdersState {\n  const OrdersState();\n}\n\nfinal class OrdersLoading extends OrdersState {\n  const OrdersLoading();\n}\n\nfinal class OrdersLoaded extends OrdersState {\n  const OrdersLoaded(this.orders);\n  final List\u003COrder> orders;\n}\n\nfinal class OrdersError extends OrdersState {\n  const OrdersError(this.failure);\n  final Failure failure;\n}\n\n\u002F\u002F Il provider dello use case viene sovrascritto dalla composition root.\nfinal getOrdersProvider = Provider\u003CGetOrders>(\n  (ref) => throw UnimplementedError('Override in bootstrap()'),\n);\n\nfinal ordersControllerProvider =\n    AsyncNotifierProvider\u003COrdersController, OrdersState>(OrdersController.new);\n\nfinal class OrdersController extends AsyncNotifier\u003COrdersState> {\n  @override\n  Future\u003COrdersState> build() => _load();\n\n  Future\u003COrdersState> _load() async {\n    final result = await ref.read(getOrdersProvider)();\n    return switch (result) {\n      Ok(:final value) => OrdersLoaded(value),\n      Err(:final failure) => OrdersError(failure),\n    };\n  }\n\n  Future\u003Cvoid> refresh() async {\n    state = const AsyncLoading();\n    state = AsyncData(await _load());\n  }\n}\n\n\u002F\u002F lib\u002Ffeatures\u002Forders\u002Forders.dart  --> API PUBBLICA DELLA FEATURE\nexport 'domain\u002Fentities\u002Forder.dart' show Order, OrderStatus;\nexport 'orders_module.dart' show ordersRoutes, ordersOverrides;\n\u002F\u002F NON esportiamo: dto, datasources, repositories impl, controller interni.","Dall'esterno la feature è una scatola nera: `import 'features\u002Forders\u002Forders.dart';` mostra solo `Order`, `OrderStatus`, le rotte e gli override di DI.",{"id":65,"position":66,"title":67,"body":68,"code_snippet":69,"code_language":49,"expected_result":70,"demo_url":42,"video_url":42},678,5,"Composition root: collegare i moduli senza accoppiarli tra loro","I moduli non si conoscono: qualcuno deve però istanziare `OrderRepositoryImpl` e iniettarlo. Quel qualcuno è la **composition root** in `lib\u002Fapp\u002F`, l'unico posto autorizzato a vedere le implementazioni concrete.\n\nOgni feature espone due cose nel suo `*_module.dart`:\n\n- `ordersOverrides`: la lista di `Override` di Riverpod che lega contratti e implementazioni;\n- `ordersRoutes`: le sue `RouteBase` per `go_router`, così il router dell'app non contiene percorsi hard-coded di ogni schermata.\n\nAggiungere una nuova feature diventa un'operazione di due righe nel bootstrap, senza toccare i moduli esistenti: è il test definitivo della qualità dei confini.\n\n**Comunicazione tra feature.** Se `orders` ha bisogno dell'utente loggato, non importa la repository di `auth`: dichiara un proprio contratto nel suo dominio (es. `CurrentUserProvider`) e la composition root lo soddisfa con un adapter che usa l'API pubblica di `auth`. La dipendenza resta *verso l'astrazione*, mai verso l'implementazione altrui.","\u002F\u002F lib\u002Ffeatures\u002Forders\u002Forders_module.dart\nimport 'package:flutter_riverpod\u002Fflutter_riverpod.dart';\nimport 'package:go_router\u002Fgo_router.dart';\nimport '..\u002F..\u002Fcore\u002Fnetwork\u002Fapi_client.dart';\nimport 'data\u002Fdatasources\u002Forder_remote_data_source.dart';\nimport 'data\u002Frepositories\u002Forder_repository_impl.dart';\nimport 'domain\u002Frepositories\u002Forder_repository.dart';\nimport 'domain\u002Fusecases\u002Fget_orders.dart';\nimport 'presentation\u002Fcontrollers\u002Forders_controller.dart';\nimport 'presentation\u002Fscreens\u002Forders_screen.dart';\n\nfinal _orderRepositoryProvider = Provider\u003COrderRepository>(\n  (ref) => OrderRepositoryImpl(\n    HttpOrderRemoteDataSource(ref.watch(apiClientProvider)),\n  ),\n);\n\n\u002F\u002F\u002F Override che la composition root deve installare.\nList\u003COverride> get ordersOverrides => [\n      getOrdersProvider.overrideWith(\n        (ref) => GetOrders(ref.watch(_orderRepositoryProvider)),\n      ),\n    ];\n\n\u002F\u002F\u002F Rotte esposte dalla feature.\nList\u003CRouteBase> get ordersRoutes => [\n      GoRoute(\n        path: '\u002Forders',\n        name: 'orders',\n        builder: (_, __) => const OrdersScreen(),\n      ),\n    ];\n\n\u002F\u002F lib\u002Fapp\u002Fbootstrap.dart\nimport 'package:flutter\u002Fwidgets.dart';\nimport 'package:flutter_riverpod\u002Fflutter_riverpod.dart';\nimport '..\u002Ffeatures\u002Fauth\u002Fauth.dart';\nimport '..\u002Ffeatures\u002Forders\u002Forders.dart';\nimport 'app.dart';\n\nFuture\u003Cvoid> bootstrap() async {\n  WidgetsFlutterBinding.ensureInitialized();\n\n  runApp(\n    ProviderScope(\n      overrides: [\n        ...authOverrides,\n        ...ordersOverrides,\n      ],\n      child: const App(),\n    ),\n  );\n}\n\n\u002F\u002F lib\u002Fapp\u002Frouter.dart\nimport 'package:go_router\u002Fgo_router.dart';\nimport '..\u002Ffeatures\u002Fauth\u002Fauth.dart';\nimport '..\u002Ffeatures\u002Forders\u002Forders.dart';\n\nfinal appRouter = GoRouter(\n  initialLocation: '\u002Forders',\n  routes: [\n    ...authRoutes,\n    ...ordersRoutes,\n  ],\n);","L'app parte e mostra la schermata ordini; rimuovendo due righe dal bootstrap e dal router la feature si disattiva completamente senza errori di compilazione altrove.",{"id":72,"position":73,"title":74,"body":75,"code_snippet":76,"code_language":77,"expected_result":78,"demo_url":42,"video_url":42},679,6,"Rendere i confini eseguibili con import_lint e un controllo in CI","Le convenzioni che nessuno verifica decadono in poche settimane. Trasformiamole quindi in **errori di analisi**.\n\nInstalla `import_lint` (basato su `custom_lint`) e descrivi le regole in `import_lint.yaml`. Le direttive `target`\u002F`from`\u002F`not_allow_imports` permettono di esprimere esattamente la nostra gerarchia: dominio senza Flutter, presentation senza data, feature che si parlano solo attraverso i barrel.\n\nAggiungi poi in CI il comando `dart run custom_lint` (che restituisce exit code diverso da zero in caso di violazione). Per i team che non vogliono dipendenze extra, lo script Dart in fondo è un guardiano minimale da eseguire con `dart run tool\u002Fcheck_boundaries.dart`.","# pubspec.yaml (dev_dependencies)\ndev_dependencies:\n  custom_lint: ^0.6.0\n  import_lint: ^2.0.0\n\n# analysis_options.yaml\nanalyzer:\n  plugins:\n    - custom_lint\n\n# import_lint.yaml\nimport_lint:\n  severity: error\n  rules:\n    domain_is_pure:\n      target: \"package:my_app\u002Ffeatures\u002F**\u002Fdomain\u002F**.dart\"\n      from: \"package:my_app\u002F**\"\n      not_allow_imports:\n        - \"package:flutter\u002F**\"\n        - \"package:my_app\u002Ffeatures\u002F**\u002Fdata\u002F**.dart\"\n        - \"package:my_app\u002Ffeatures\u002F**\u002Fpresentation\u002F**.dart\"\n\n    presentation_never_touches_data:\n      target: \"package:my_app\u002Ffeatures\u002F**\u002Fpresentation\u002F**.dart\"\n      from: \"package:my_app\u002F**\"\n      not_allow_imports:\n        - \"package:my_app\u002Ffeatures\u002F**\u002Fdata\u002F**.dart\"\n\n    core_and_shared_are_feature_agnostic:\n      target: \"package:my_app\u002F{core,shared}\u002F**.dart\"\n      from: \"package:my_app\u002F**\"\n      not_allow_imports:\n        - \"package:my_app\u002Ffeatures\u002F**\"\n\n    cross_feature_only_via_barrel:\n      target: \"package:my_app\u002Ffeatures\u002F**\"\n      from: \"package:my_app\u002Ffeatures\u002F**\"\n      not_allow_imports:\n        - \"package:my_app\u002Ffeatures\u002F*\u002F{domain,data,presentation}\u002F**.dart\"\n      exclude_imports:\n        - \"package:my_app\u002Ffeatures\u002F*\u002F[a-z_]*.dart\"","yaml","Un import `package:my_app\u002Ffeatures\u002Forders\u002Fdata\u002F...` dentro `auth` viene segnalato come errore dall'IDE e fa fallire `dart run custom_lint` in pipeline.",{"id":80,"position":81,"title":82,"body":83,"code_snippet":84,"code_language":49,"expected_result":85,"demo_url":42,"video_url":42},680,7,"Verificare l'architettura con test: contract test e guardiano delle dipendenze","Ultimo tassello: i test che proteggono i confini nel tempo.\n\n1. **Contract test del repository**: un unico set di test viene eseguito su ogni implementazione del contratto (HTTP, cache, fake). Se domani aggiungi `CachedOrderRepository`, riusi gli stessi test e sei certo che il comportamento visto dal dominio non cambia.\n2. **Test di architettura**: uno script che scandisce i sorgenti e fallisce se trova import proibiti. È la rete di sicurezza anche per chi non ha il plugin di lint attivo nell'IDE.\n3. **Test della presentation con fake**: sostituisci `getOrdersProvider` con uno use case che usa un `FakeOrderRepository` in memoria — nessun mock di Dio, nessun JSON, test velocissimi.\n\nEsegui il tutto con `flutter test` e collega il comando alla pipeline: da questo momento l'architettura non è più una convenzione, è un vincolo verificato a ogni push.","\u002F\u002F test\u002Farchitecture\u002Fboundaries_test.dart\nimport 'dart:io';\nimport 'package:test\u002Ftest.dart';\n\nvoid main() {\n  final dartFiles = Directory('lib')\n      .listSync(recursive: true)\n      .whereType\u003CFile>()\n      .where((f) => f.path.endsWith('.dart'));\n\n  test('il domain non dipende da Flutter né da data\u002Fpresentation', () {\n    final violations = \u003CString>[];\n\n    for (final file in dartFiles) {\n      final path = file.path.replaceAll(r'\\', '\u002F');\n      if (!path.contains('\u002Fdomain\u002F')) continue;\n\n      for (final line in file.readAsLinesSync()) {\n        if (!line.trimLeft().startsWith('import')) continue;\n        final forbidden = line.contains('package:flutter\u002F') ||\n            line.contains('\u002Fdata\u002F') ||\n            line.contains('\u002Fpresentation\u002F');\n        if (forbidden) violations.add('$path -> ${line.trim()}');\n      }\n    }\n\n    expect(violations, isEmpty, reason: violations.join('\\n'));\n  });\n\n  test('le feature dialogano solo tramite il barrel file', () {\n    final pattern =\n        RegExp(r\"import\\s+'package:my_app\u002Ffeatures\u002F(\\w+)\u002F(domain|data|presentation)\u002F\");\n    final violations = \u003CString>[];\n\n    for (final file in dartFiles) {\n      final path = file.path.replaceAll(r'\\', '\u002F');\n      final owner = RegExp(r'lib\u002Ffeatures\u002F(\\w+)\u002F').firstMatch(path)?.group(1);\n      if (owner == null) continue;\n\n      for (final line in file.readAsLinesSync()) {\n        final match = pattern.firstMatch(line);\n        if (match != null && match.group(1) != owner) {\n          violations.add('$path -> ${line.trim()}');\n        }\n      }\n    }\n\n    expect(violations, isEmpty, reason: violations.join('\\n'));\n  });\n}\n\n\u002F\u002F test\u002Ffeatures\u002Forders\u002Forder_repository_contract.dart\nimport 'package:my_app\u002Ffeatures\u002Forders\u002Forders.dart';\nimport 'package:test\u002Ftest.dart';\n\n\u002F\u002F\u002F Suite riutilizzabile per QUALSIASI implementazione del contratto.\nvoid runOrderRepositoryContract(\n  String name,\n  OrderRepositoryFactory build,\n) {\n  group('OrderRepository contract: $name', () {\n    test('fetchOrders restituisce entità di dominio', () async {\n      final repo = await build();\n      final result = await repo.fetchOrders();\n      expect(result, isA\u003COk\u003CList\u003COrder>>>());\n    });\n\n    test('markAsShipped su id inesistente restituisce un Failure', () async {\n      final repo = await build();\n      final result = await repo.markAsShipped('missing');\n      expect(result, isA\u003CErr\u003COrder>>());\n    });\n  });\n}","`flutter test` verde su contract test e test di architettura: ogni violazione dei confini viene intercettata prima della code review.",[],1792316930008]