[{"data":1,"prerenderedAt":79},["ShallowReactive",2],{"tutorial-confini-tra-moduli-in-un-monorepo-flutter-contratti-regole-di-dipendenza-e-verifica-automatica":3,"comments-tutorial-confini-tra-moduli-in-un-monorepo-flutter-contratti-regole-di-dipendenza-e-verifica-automatica":78},{"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},118,"Confini tra moduli in un monorepo Flutter: contratti, regole di dipendenza e verifica automatica","confini-tra-moduli-in-un-monorepo-flutter-contratti-regole-di-dipendenza-e-verifica-automatica","Guida avanzata per far rispettare i confini architetturali in un monorepo Flutter: package di contratto, regole di dipendenza, lint personalizzati e script di verifica in CI con Melos.","Quando un'app Flutter cresce, il problema non è più \"come scrivo una feature\" ma \"come impedisco che le feature si parlino tra loro di nascosto\". In un monorepo con decine di package interni, un singolo `import` sbagliato basta a trasformare l'architettura in un grafo di dipendenze circolari.\n\nIn questo tutorial costruiamo un sistema di **confini verificabili**: definiamo contratti espliciti tra moduli, limitiamo cosa ogni package può importare, e automatizziamo i controlli in CI con Melos, `dart analyze` e uno script di validazione del grafo delle dipendenze.\n\n> Prerequisiti: conoscenza di Flutter, package Dart, Melos e almeno un'architettura a layer (data\u002Fdomain\u002Fpresentation).","https:\u002F\u002Fflutter.it\u002Fstorage\u002Ftutorials\u002Fe346cd73-7170-4ab8-bcd5-82eaa04aec7c.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1614138720709-fe98e88b1888?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3OTE0MzQwMDB8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"Parsoa Khorsand","https:\u002F\u002Funsplash.com\u002F@parsoakhorsand","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fwhite-and-black-glass-walled-building-kBg79SoMb6o","https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=WOalyAKR3c0",{"youtube_id":17,"duration_seconds":18,"published_at":19},"WOalyAKR3c0",133,"2026-10-08T11:43:42+00:00","advanced",55,"3.x","published","2026-10-08T04:33:20+00:00","Confini tra moduli in un monorepo Flutter","Tutorial avanzato: contratti tra package, regole di dipendenza, lint custom e verifica automatica del grafo in CI con Melos in un monorepo Flutter.",{"id":28,"name":29,"slug":30},3,"Best practice","best-practice",{"id":32,"name":33},1,"Flutter Bot",[35,43,51,57,64,71],{"id":36,"position":32,"title":37,"body":38,"code_snippet":39,"code_language":40,"expected_result":41,"demo_url":42,"video_url":42},790,"Definire i livelli e il grafo delle dipendenze consentite","Prima del codice serve una **policy**. Definiamo quattro livelli con direzione di dipendenza unica (dall'alto verso il basso):\n\n| Livello | Package | Può dipendere da |\n|---|---|---|\n| `app` | `app` | tutto |\n| `feature` | `feature_cart`, `feature_catalog` | `contracts`, `core` |\n| `contracts` | `contracts_cart` | `core` |\n| `core` | `core_ui`, `core_network` | nulla di interno |\n\nRegole non negoziabili:\n\n1. Una feature **non** importa mai un'altra feature.\n2. Una feature dipende solo dal *contratto* di un'altra feature, mai dalla sua implementazione.\n3. `core` non conosce nessuna feature.\n\nScriviamo la policy in un file machine-readable: sarà la fonte di verità per lo script di verifica del passo 4.","# tool\u002Farchitecture.yaml\nlayers:\n  core:\n    packages: [core_ui, core_network]\n    allowed: []\n  contracts:\n    packages: [contracts_cart, contracts_catalog]\n    allowed: [core]\n  feature:\n    packages: [feature_cart, feature_catalog]\n    allowed: [core, contracts]\n  app:\n    packages: [app]\n    allowed: [core, contracts, feature]","yaml","Hai un file di policy versionato che descrive in modo esplicito e verificabile il grafo architetturale ammesso.",null,{"id":44,"position":45,"title":46,"body":47,"code_snippet":48,"code_language":49,"expected_result":50,"demo_url":42,"video_url":42},791,2,"Creare il package di contratto tra due feature","Il contratto è un package **senza implementazione**: solo interfacce, modelli immutabili ed eventi. È ciò che `feature_catalog` può conoscere di `feature_cart`.\n\nCrea il package con `melos exec` oppure manualmente:\n\n```bash\nmkdir -p packages\u002Fcontracts_cart\ncd packages\u002Fcontracts_cart && dart create -t package .\n```\n\nNel `pubspec.yaml` del contratto consenti solo dipendenze `core` (o nessuna). L'implementazione reale vivrà in `feature_cart`, che dichiara `implements`.","\u002F\u002F packages\u002Fcontracts_cart\u002Flib\u002Fcontracts_cart.dart\nimport 'package:meta\u002Fmeta.dart';\n\n@immutable\nclass CartLine {\n  const CartLine({required this.sku, required this.quantity});\n  final String sku;\n  final int quantity;\n}\n\n\u002F\u002F\u002F Contratto pubblico esposto dal modulo carrello.\n\u002F\u002F\u002F Nessun altro modulo conosce l'implementazione.\nabstract interface class CartFacade {\n  Future\u003Cvoid> add(CartLine line);\n  Future\u003Cvoid> remove(String sku);\n  Stream\u003CList\u003CCartLine>> watch();\n}\n\n\u002F\u002F\u002F Rotte dichiarate dal modulo, usate dall'app per il wiring.\nabstract final class CartRoutes {\n  static const cart = '\u002Fcart';\n}","dart","`contracts_cart` compila isolatamente con `dart analyze` e non dipende da alcun package di feature.",{"id":52,"position":28,"title":53,"body":54,"code_snippet":55,"code_language":49,"expected_result":56,"demo_url":42,"video_url":42},792,"Implementare il contratto e registrare il modulo nell'app","`feature_cart` dipende dal proprio contratto e lo implementa. L'app è l'**unico** punto in cui implementazione e interfaccia si incontrano: qui avviene il binding nel container DI (get_it, Riverpod o un semplice registry).\n\nNota chiave: `feature_catalog` riceverà `CartFacade` via DI, quindi nel suo `pubspec.yaml` comparirà solo `contracts_cart`. Se qualcuno aggiunge `feature_cart` tra le sue dipendenze, lo script del passo 4 fallirà.","\u002F\u002F packages\u002Ffeature_cart\u002Flib\u002Fsrc\u002Fcart_facade_impl.dart\nimport 'package:contracts_cart\u002Fcontracts_cart.dart';\n\nfinal class CartFacadeImpl implements CartFacade {\n  CartFacadeImpl(this._repo);\n  final CartRepository _repo;\n\n  @override\n  Future\u003Cvoid> add(CartLine line) => _repo.upsert(line);\n\n  @override\n  Future\u003Cvoid> remove(String sku) => _repo.delete(sku);\n\n  @override\n  Stream\u003CList\u003CCartLine>> watch() => _repo.stream();\n}\n\n\u002F\u002F apps\u002Fapp\u002Flib\u002Fdi\u002Fmodules.dart  (unico punto di composizione)\nvoid registerModules(GetIt di) {\n  di.registerLazySingleton\u003CCartFacade>(\n    () => CartFacadeImpl(di\u003CCartRepository>()),\n  );\n}\n\n\u002F\u002F packages\u002Ffeature_catalog\u002Flib\u002Fsrc\u002Fproduct_page.dart\n\u002F\u002F dipende SOLO dall'interfaccia, mai da feature_cart\nclass AddToCartButton extends StatelessWidget {\n  const AddToCartButton({super.key, required this.facade, required this.sku});\n  final CartFacade facade;\n  final String sku;\n\n  @override\n  Widget build(BuildContext context) => FilledButton(\n        onPressed: () => facade.add(CartLine(sku: sku, quantity: 1)),\n        child: const Text('Aggiungi al carrello'),\n      );\n}","Il catalogo usa il carrello senza conoscerne l'implementazione: sostituire `CartFacadeImpl` con un fake nei test non richiede modifiche al catalogo.",{"id":58,"position":59,"title":60,"body":61,"code_snippet":62,"code_language":49,"expected_result":63,"demo_url":42,"video_url":42},793,4,"Validare il grafo delle dipendenze con uno script Dart","Le convenzioni non rispettate diventano debito tecnico. Scriviamo uno script che legge `tool\u002Farchitecture.yaml`, analizza ogni `pubspec.yaml` dei package interni e fallisce (`exitCode = 1`) se trova una dipendenza vietata o una feature che ne importa un'altra.\n\nEseguilo con `dart run tool\u002Fcheck_architecture.dart` dalla root del monorepo.","\u002F\u002F tool\u002Fcheck_architecture.dart\nimport 'dart:io';\nimport 'package:yaml\u002Fyaml.dart';\n\nvoid main() {\n  final policy = loadYaml(File('tool\u002Farchitecture.yaml').readAsStringSync());\n  final layerOf = \u003CString, String>{};\n  final allowed = \u003CString, Set\u003CString>>{};\n\n  for (final entry in (policy['layers'] as YamlMap).entries) {\n    final layer = entry.key as String;\n    allowed[layer] = {...(entry.value['allowed'] as YamlList).cast\u003CString>()};\n    for (final p in (entry.value['packages'] as YamlList).cast\u003CString>()) {\n      layerOf[p] = layer;\n    }\n  }\n\n  final errors = \u003CString>[];\n  for (final dir in Directory('packages').listSync().whereType\u003CDirectory>()) {\n    final file = File('${dir.path}\u002Fpubspec.yaml');\n    if (!file.existsSync()) continue;\n    final pubspec = loadYaml(file.readAsStringSync()) as YamlMap;\n    final name = pubspec['name'] as String;\n    final layer = layerOf[name];\n    if (layer == null) {\n      errors.add('[$name] non è dichiarato in architecture.yaml');\n      continue;\n    }\n    final deps = (pubspec['dependencies'] as YamlMap?)?.keys.cast\u003CString>() ?? const \u003CString>[];\n    for (final dep in deps) {\n      final depLayer = layerOf[dep];\n      if (depLayer == null) continue; \u002F\u002F pacchetto esterno: ok\n      if (!allowed[layer]!.contains(depLayer)) {\n        errors.add('[$name] ($layer) non può dipendere da $dep ($depLayer)');\n      }\n    }\n  }\n\n  if (errors.isEmpty) {\n    stdout.writeln('✅ Confini architetturali rispettati');\n    return;\n  }\n  stderr.writeln('❌ Violazioni:\\n - ${errors.join('\\n - ')}');\n  exitCode = 1;\n}","Aggiungendo `feature_cart` alle dipendenze di `feature_catalog` lo script esce con codice 1 e stampa la violazione.",{"id":65,"position":66,"title":67,"body":68,"code_snippet":69,"code_language":40,"expected_result":70,"demo_url":42,"video_url":42},794,5,"Blindare gli import interni con lint e API pubbliche minime","Il grafo dei `pubspec` non basta: dentro un package si può ancora importare `package:feature_cart\u002Fsrc\u002F...`. Due difese complementari:\n\n1. **Un solo file barrel** per package, con tutto il resto sotto `lib\u002Fsrc\u002F`: Dart segnala già `implementation_imports` per gli import di `src\u002F` altrui — promuovilo a errore.\n2. Attiva regole severe in un `analysis_options` condiviso, ereditato da ogni package.\n\nAggiungi anche `public_member_api_docs` sui package di contratto: se un'API è pubblica deve essere documentata, il che scoraggia l'esposizione accidentale di dettagli interni.","# analysis_options.yaml (root, condiviso)\ninclude: package:very_good_analysis\u002Fanalysis_options.yaml\n\nanalyzer:\n  language:\n    strict-casts: true\n    strict-raw-types: true\n  errors:\n    implementation_imports: error\n    depend_on_referenced_packages: error\n    public_member_api_docs: ignore\n  exclude:\n    - \"**\u002F*.g.dart\"\n\n# packages\u002Fcontracts_cart\u002Fanalysis_options.yaml\ninclude: ..\u002F..\u002Fanalysis_options.yaml\nanalyzer:\n  errors:\n    public_member_api_docs: error","`dart analyze` fallisce se un package importa i file `src\u002F` di un altro package o dipende da un package non dichiarato.",{"id":72,"position":73,"title":74,"body":75,"code_snippet":76,"code_language":40,"expected_result":77,"demo_url":42,"video_url":42},795,6,"Automatizzare tutto in CI con Melos","L'ultimo tassello è rendere i controlli non aggirabili. Definiamo gli script Melos e li colleghiamo a una GitHub Action che gira su ogni PR.\n\nUsa `melos exec --diff=origin\u002Fmain` per analizzare e testare solo i package toccati dalla PR: in un monorepo grande riduce drasticamente i tempi di build mantenendo il controllo architetturale globale (che invece gira sempre, perché costa pochi secondi).","# melos.yaml\nname: shop_monorepo\npackages:\n  - apps\u002F**\n  - packages\u002F**\n\nscripts:\n  arch:\n    run: dart run tool\u002Fcheck_architecture.dart\n    description: Verifica i confini tra moduli\n  analyze:\n    run: melos exec --diff=origin\u002Fmain -- dart analyze --fatal-infos .\n  test:\n    run: melos exec --diff=origin\u002Fmain --dir-exists=test -- flutter test\n  ci:\n    run: melos run arch && melos run analyze && melos run test\n\n# .github\u002Fworkflows\u002Fci.yaml\n# jobs:\n#   verify:\n#     steps:\n#       - uses: actions\u002Fcheckout@v4\n#         with: { fetch-depth: 0 }\n#       - uses: subosito\u002Fflutter-action@v2\n#       - run: dart pub global activate melos && melos bootstrap\n#       - run: melos run ci","Ogni pull request esegue `melos run ci`: una violazione dei confini blocca il merge prima che l'architettura degradi.",[],1792316927142]