Perché le dipendenze meritano attenzione
In quasi tutti i progetti Flutter il file pubspec.yaml cresce fino a 30-40 pacchetti. Finché tutto funziona nessuno lo guarda; poi arriva il giorno in cui flutter pub get fallisce con un messaggio del tipo "version solving failed", oppure la build della CI usa una versione diversa da quella locale e un widget smette di comportarsi come previsto.
Capire come pub risolve le versioni e quali strumenti abbiamo per governarle è una competenza da senior tanto quanto saper scrivere un CustomPainter. In questo articolo vediamo constraint semantici, lock file, override, comandi di manutenzione e le pub workspaces, disponibili da Dart 3.6 (Flutter 3.27) per i monorepo.
Anatomia del pubspec.yaml
name: my_app
description: Esempio di gestione dipendenze
publish_to: none # fondamentale per le app: evita pubblicazioni accidentali
version: 1.4.2+37 # versione semantica + build number
environment:
sdk: ^3.6.0
flutter: '>=3.27.0'
dependencies:
flutter:
sdk: flutter
dio: ^5.7.0
go_router: ^14.6.0
shared_preferences: ^2.3.0
dev_dependencies:
flutter_test:
sdk: flutter
build_runner: ^2.4.13
flutter_lints: ^5.0.0
Tre dettagli che spesso vengono trascurati:
publish_to: nonesu ogni applicazione. Senza questa riga undart pub publishdistratto può finire su pub.dev.environmentdefinisce i vincoli su SDK Dart e Flutter: sono il primo motivo di fallimento della risoluzione quando si aggiorna un pacchetto moderno su un SDK vecchio.dev_dependenciesnon finiscono nel bundle finale: code generation, linter e test vanno lì, sempre.
Come leggere i version constraint
Dart usa il semantic versioning (MAJOR.MINOR.PATCH) e il caret (^) è la forma idiomatica:
| Constraint | Significato |
|---|---|
^5.7.0 |
>=5.7.0 <6.0.0 |
^0.3.1 |
>=0.3.1 <0.4.0 (sotto 1.0.0 il minor è breaking) |
>=2.0.0 <2.5.0 |
intervallo esplicito |
2.3.1 |
versione esatta (da evitare se non strettamente necessario) |
any |
qualsiasi versione (da evitare sempre) |
La regola pratica:
- App: constraint con caret. Sono abbastanza stretti da proteggere dalle breaking change e abbastanza larghi da ricevere patch e fix di sicurezza.
- Package pubblicati: constraint il più larghi possibile (ma non
any), perché ogni vincolo stretto che imponi diventa un vincolo per tutti i tuoi utenti e aumenta la probabilità di conflitti.
Fissare una versione esatta in un'app sembra "più sicuro", ma in realtà è dannoso: basta un altro pacchetto che dipenda da una versione diversa dello stesso package per bloccare completamente la risoluzione.
pubspec.lock: cosa è e quando committarlo
pubspec.yaml dichiara gli intervalli, pubspec.lock registra le versioni esatte effettivamente risolte, incluse tutte le dipendenze transitive.
- Nelle applicazioni il lock file va committato: garantisce che sviluppatori, CI e build di release usino esattamente le stesse versioni. È la differenza tra una build riproducibile e un "sul mio computer funziona".
- Nei package riutilizzabili il lock file non va committato (è nel
.gitignoredel template didart create): il consumatore risolverà le versioni nel proprio contesto.
Quando pubspec.lock esiste, flutter pub get prova a rispettarlo; solo se i constraint non sono più soddisfacibili pub ricalcola.
I comandi che servono davvero
# Aggiungere dipendenze senza editare a mano il file
flutter pub add dio
flutter pub add dev:mocktail
flutter pub add 'go_router:^14.6.0'
# Scaricare le dipendenze rispettando il lock file
flutter pub get
# Aggiornare alle versioni più alte compatibili con i constraint attuali
flutter pub upgrade
# Report di ciò che è obsoleto (e cosa è discontinuato)
flutter pub outdated
# Aggiornare i constraint stessi verso le major successive
flutter pub upgrade --major-versions
# Albero delle dipendenze in forma compatta
flutter pub deps -s compact
# Risolvere alle versioni MINIME consentite dai constraint
flutter pub downgrade
Due comandi meritano una nota.
flutter pub outdated distingue tre colonne: Current (quella nel lock), Upgradable (la più alta compatibile con i constraint) e Resolvable/Latest. Se Current e Upgradable differiscono ti basta pub upgrade; se la differenza è con Latest devi alzare il constraint, cioè accettare una major con possibili breaking change.
flutter pub downgrade è il trucco per verificare i lower bound dichiarati. Se il tuo package dichiara ^1.2.0 ma usa un'API introdotta nella 1.5.0, con downgrade la compilazione fallisce e lo scopri prima dei tuoi utenti. Ottimo step da inserire in CI per i package pubblicati.
Dipendenze non da pub.dev
dependencies:
# pacchetto locale nello stesso repository
core_models:
path: ../packages/core_models
# repository Git (utile per fork o fix non ancora pubblicati)
awesome_widget:
git:
url: https://github.com/org/awesome_widget.git
ref: v2.1.0 # usa un tag o uno SHA, mai 'main'
path: packages/awesome_widget # se è un monorepo
# repository pub privato
internal_sdk:
hosted:
name: internal_sdk
url: https://pub.azienda.it
version: ^1.0.0
Regola d'oro per le dipendenze Git: ancorare sempre a un tag o a uno SHA. Puntare a un branch significa che la build di oggi può differire da quella di domani, anche con il lock file (che memorizza comunque lo SHA risolto, ma che verrà aggiornato al primo pub upgrade).
Attenzione: un package che dipende da path o git non può essere pubblicato su pub.dev.
dependency_overrides: l'antidolorifico, non la cura
Quando due pacchetti richiedono versioni incompatibili di una dipendenza transitiva, pub si arrende. L'escape hatch è:
dependency_overrides:
analyzer: ^6.4.1
meta: 1.15.0
Cosa succede davvero: pub ignora tutti gli altri constraint su quel package e usa la versione indicata. Può funzionare benissimo, oppure far esplodere l'app a runtime con NoSuchMethodError, perché stai forzando un'API che qualche libreria non si aspetta.
Linee guida:
- Usalo solo nel pubspec della root (nei package pubblicati viene ignorato dai consumatori e genera un warning in fase di publish).
- Aggiungi sempre un commento con la data e il link alla issue upstream.
- Trattalo come debito tecnico: inserisci un promemoria per rimuoverlo quando esce la versione fixata.
Prima di ricorrere all'override, prova a capire chi impone il vincolo:
flutter pub deps -s compact | grep analyzer
Spesso il colpevole è una dev_dependency di code generation che puoi semplicemente aggiornare.
Pub workspaces: un solo lock file per tutto il monorepo
Da Dart 3.6 / Flutter 3.27 pub supporta nativamente i monorepo con le workspaces. L'idea: più package nello stesso repository condividono una singola risoluzione delle dipendenze, quindi un solo pubspec.lock e un solo .dart_tool/package_config.json nella root.
Struttura tipica:
my_monorepo/
pubspec.yaml <- la root della workspace
apps/
mobile/pubspec.yaml
packages/
core/pubspec.yaml
design_system/pubspec.yaml
Root pubspec.yaml:
name: _my_monorepo
publish_to: none
environment:
sdk: ^3.6.0
workspace:
- apps/mobile
- packages/core
- packages/design_system
Ogni package membro dichiara l'appartenenza:
name: core
publish_to: none
environment:
sdk: ^3.6.0
resolution: workspace
dependencies:
meta: ^1.15.0
E i riferimenti incrociati restano espressi come dipendenze normali: pub risolve automaticamente verso il package locale se il nome appartiene alla workspace.
# apps/mobile/pubspec.yaml
dependencies:
flutter:
sdk: flutter
core: ^1.0.0
design_system: ^0.2.0
I vantaggi concreti:
- Un solo
flutter pub getdalla root aggiorna tutti i package. - Coerenza garantita: impossibile che l'app usi
dio 5.7e un package internodio 5.4. - CI più veloce: una sola cache delle dipendenze, un solo passo di risoluzione.
Le workspaces non sostituiscono completamente strumenti come Melos (che resta utile per orchestrare script, versioning e changelog su N package), ma eliminano la parte più fragile: il bootstrap e il linking dei pacchetti locali.
Strategia di manutenzione
Un flusso sostenibile per un team:
- Settimanale:
flutter pub outdatedin CI, con report pubblicato. Serve a non accumulare ritardo. - Patch e minor: aggiornamento automatizzato (Dependabot o Renovate supportano pub) con la suite di test come gate.
- Major: una alla volta, su branch dedicato, leggendo il CHANGELOG e il file di migrazione. Mai cinque major in un'unica PR.
- Prima di ogni release:
flutter pub getpulito su CI partendo dal lock file committato, maipub upgradein pipeline di produzione. - Audit periodico: controlla nei risultati di
pub outdatedi pacchetti marcati come discontinued e verifica su pub.dev punteggio, data dell'ultima pubblicazione e numero di issue aperte prima di introdurre una nuova dipendenza.
Checklist finale
- Constraint con
^nelle app, il più larghi possibile nei package pubblicati. pubspec.lockcommittato nelle app, ignorato nei package.- Dipendenze Git ancorate a tag o SHA.
dependency_overridessolo temporanei, documentati e nella root.pub downgradein CI per i package, a verifica dei lower bound.- Monorepo su Dart 3.6+: valuta
resolution: workspaceal posto del linking manuale.
Una gestione disciplinata delle dipendenze non rende l'app più veloce né più bella, ma elimina un'intera categoria di problemi che tipicamente si manifestano nel momento peggiore: il giorno della release.