Nếu bạn từng tách một Flutter app thành vài local package, bạn biết cái cảm giác rắc rối đó: mỗi package có .dart_tool/ riêng, pubspec.lock riêng, và dependency resolution riêng. Bạn nối chúng lại bằng path: dependencies, chạy pub get trong từng folder (hoặc nhờ một tool làm hộ), rồi cầu trời transitive versions của mọi người đừng lệch nhau.

Kể từ Dart 3.6*, SDK đã có một giải pháp first-class ngay trong lõi: *pub workspaces. Một lần resolution, một lockfile, một .dart_tool/, dùng chung cho toàn bộ package trong repo. Không cần tooling bên ngoài.
Bài này sẽ đi qua workspaces là gì, cách setup, và cách tôi dùng nó trong Flutter starter template của mình — một monorepo với mười local package, zero path: dependencies, và không một file config Melos nào.
Reference implementation: github.com/kido-luci/flutter-starter-template
---
Vấn đề workspaces giải quyết
Một Flutter monorepo điển hình trông như thế này:
my_app/
├── pubspec.yaml # the app
└── packages/
├── network/
│ └── pubspec.yaml
├── theme/
│ └── pubspec.yaml
└── ...
Trước khi có workspaces, mỗi package resolve dependency độc lập. Điều đó có nghĩa là:
.dart_tool/package_config.json và pubspec.lock của riêng nó. Một lần pub get sạch trên toàn repo sẽ resolve đi resolve lại những shared dependency giống nhau.theme dùng được architecture, bạn phải viết architecture: { path: ../architecture }. Nó hoạt động, nhưng rối và dễ sai.network và storage phải đồng ý với nhau về version của dio hay injectable. Resolution độc lập có thể khiến mỗi package nằm trên một transitive set khác nhau, và chỗ duy nhất được reconcile là app ở tầng trên — đôi khi rất đau đầu.Melos đã che bớt nhiều vấn đề này với melos bootstrap, nhưng việc bootstrap vẫn chỉ là điều phối nhiều resolution độc lập. Workspaces giải quyết tận gốc vấn đề thay vì quản lý nó.
---
Dart workspace thực ra là gì
Một workspace là tập hợp các package dùng chung một dependency resolution*. Thay vì mỗi package tự resolve một mình, pub coi toàn bộ repo là một đơn vị: nó xây dựng một dependency graph duy nhất, chọn một version cho mỗi shared package, và ghi ra *một pubspec.lock và một .dart_tool/ duy nhất ở workspace root.
Chỉ cần hai phần config nhỏ:
1. Package root khai báo những package nào thuộc workspace.
2. Mỗi package member opt in bằng một dòng.
Đó là toàn bộ cơ chế. Mọi thứ còn lại — tốc độ, tính nhất quán, một lần pub get duy nhất — đều là kết quả tự nhiên từ hai khai báo đó.
Yêu cầu: Dart SDK >= 3.6.0 trong mọi package (workspaces và member opt-in key chỉ được hiểu bởi 3.6+). Tất cả member phải có SDK constraint tương thích, vì chúng được resolve cùng nhau.
---
Cách setup
1. Khai báo workspace ở root
Trong root pubspec.yaml, thêm field workspace: liệt kê mọi member package theo path. Đây là config root thực tế từ starter template của tôi:
name: flutter_starter_template
publish_to: 'none'
version: 1.0.0+1
environment:
sdk: ^3.12.0
workspace:
- packages/app_ui
- packages/analytics
- packages/config
- packages/architecture
- packages/network
- packages/app_platform
- packages/storage
- packages/sync
- packages/theme
- packages/test_utils
dependencies:
app_ui: ^0.1.0
analytics: ^0.1.0
config: ^0.1.0
architecture: ^0.1.0
network: ^0.1.0
app_platform: ^0.1.0
storage: ^0.1.0
sync: ^0.1.0
theme: ^0.1.0
flutter:
sdk: flutter
# ...the rest of the app's dependencies
Hai điều đáng chú ý:
config: ^0.1.0), không phải bằng path:. Trong một workspace, pub tự động resolve những tên đó sang local member tương ứng.2. Opt từng member vào workspace
Mỗi member package thêm một key duy nhất: resolution: workspace. Đây là packages/network:
name: network
description: Dio HTTP client building blocks for the Flutter starter template.
version: 0.1.0
publish_to: none
environment:
sdk: ^3.12.0
resolution: workspace
dependencies:
config: ^0.1.0 # ← another local package, by name. No path:.
dio: ^5.9.2
firebase_performance: ^0.11.4+1
injectable: ^3.0.0
retrofit: ^4.9.2
dev_dependencies:
flutter_test:
sdk: flutter
build_runner: ^2.15.0
injectable_generator: ^3.0.2
test_utils: ^0.1.0 # ← yet another local package
resolution: workspace là cách opt in. Nó nói với pub: đừng resolve tôi một mình — hãy resolve tôi như một phần của workspace mà package này thuộc về. Pub tìm workspace bằng cách đi ngược lên cây thư mục cho đến khi gặp root pubspec.yaml có liệt kê package này.
3. Chạy pub get một lần duy nhất
flutter pub get # or: dart pub get
Chạy từ root. Pub resolve toàn bộ repo trong một lần và ghi ra:
my_app/
├── .dart_tool/ # one, at the root
├── pubspec.lock # one, at the root
├── pubspec.yaml # workspace: [...]
└── packages/
├── network/
│ └── pubspec.yaml # resolution: workspace
└── ...
Không có .dart_tool/ per-package. Không có lockfile per-package. Một resolution cho tất cả.
---
Những gì bạn nhận được
Một version nhất quán cho mọi shared dependency
Đây là điểm mấu chốt. Vì toàn bộ repo resolve cùng nhau, network, storage, sync, và app không thể nào chạy trên các version khác nhau của injectable hay dio. Pub chọn một version thỏa mãn mọi constraint từ mọi member, hoặc nó báo lỗi rõ ràng và chỉ ra constraint nào xung đột. Version drift giữa các package bị loại bỏ hoàn toàn về mặt cấu trúc.
Local package theo tên, không theo path
Việc nối các package với nhau trở nên đơn giản. Trong template của tôi, theme phụ thuộc vào analytics và architecture; test_utils phụ thuộc vào analytics, app_platform, và storage; network phụ thuộc vào config. Tất cả đều chỉ là:
dependencies:
analytics: ^0.1.0
architecture: ^0.1.0
Không có path: cần maintain khi bạn di chuyển folder. Pub tự map tên sang workspace member.
Một lần pub get duy nhất, nhanh hơn
Một resolution thay vì N lần. Trên một repo với mười package dùng chung phần lớn dependency graph, sự khác biệt là rõ ràng — cả trên máy của bạn lẫn trong CI, nơi một lần pub get ở root làm ấm cache cho toàn bộ workspace.
Chạy test và analysis trên toàn repo
Một analysis_options.yaml dùng chung ở root áp dụng cho mọi package (của tôi có very_good_analysis), và bạn có thể analyze hoặc test toàn bộ workspace từ một chỗ:
dart analyze
dart test # runs across workspace members
---
Yêu cầu và những điều cần lưu ý
environment.sdk của member phải cho phép 3.6 trở lên, và các constraint phải tương thích với nhau — chúng được resolve như một đơn vị. (Template của tôi pin ^3.12.0 ở toàn bộ.)package A trên dio 4.x trong khi package B đòi dio 5.x — đó giờ là một resolution error cứng, không phải một lockfile khác nhau âm thầm. Đây chính là mục đích, nhưng nó có thể lộ ra những xung đột tiềm ẩn lần đầu bạn migrate.resolution: workspace là bắt buộc trên member. Quên ở một package và pub sẽ cố resolve package đó standalone rồi phàn nàn rằng nó không thuộc workspace nào (hoặc resolve nó riêng, điều đó phá vỡ toàn bộ mục đích).dependencies của riêng nó. Nếu bạn muốn một "container thuần" không có app code, bạn có thể làm root thành một package tối giản với vai trò duy nhất là host workspace: — nhưng để app là root (như tôi làm) cũng hoàn toàn ổn.pubspec.lock và .dart_tool/ per-package; bản canonical giờ nằm ở root. Cập nhật .gitignore cho phù hợp.---
Workspaces vs. Melos
Workspaces không làm Melos lỗi thời — chúng thay thế phần Melos kém nhất và để lại phần Melos làm tốt.
| Vấn đề | Pub workspaces | Melos |
|---|---|---|
| Shared dependency resolution | ✅ Built in | ⚠️ Bootstrap orchestration |
Single lockfile / .dart_tool | ✅ Yes | ❌ Per-package |
Local deps không cần path: | ✅ Yes | ❌ Still needed |
| Chạy script qua nhiều package | ❌ Not its job | ✅ Strong |
| Versioning / changelog / publishing phối hợp | ❌ Not its job | ✅ Strong |
Sự phân chia rõ ràng: để SDK lo resolution, để Melos (nếu bạn cần) lo task running và release automation. Melos hiện đại thực ra đã tích hợp với pub workspaces vì lý do này. Starter template của tôi không có file melos.yaml nào cả — native workspaces đáp ứng mọi thứ tôi cần cho resolution, và tôi chạy script bằng các lệnh dart/flutter thông thường.
---
Tổng kết
Pub workspaces biến một Flutter monorepo từ "N package tôi phải giữ đồng bộ" thành "một repo resolve như một đơn vị." Setup chỉ là hai dòng config — workspace: ở root, resolution: workspace trong mỗi member — và phần thưởng là version nhất quán, không còn path: dependencies, và một lần pub get nhanh duy nhất.
Nếu bạn muốn xem trên một project thực tế với nhiều package, toàn bộ setup nằm ở đây:
👉 github.com/kido-luci/flutter-starter-template
Nhìn vào root pubspec.yaml để thấy danh sách workspace:, rồi mở bất kỳ package nào trong packages/ để thấy resolution: workspace và các local dependency không cần path trong thực tế.
Happy coding : )
Loading…