Back to Blog
flutterdartpackages

Dart Workspaces: hỗ trợ monorepo native cho Flutter

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.

Dart_Workspaces_Blog.png

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à:

  • Resolution bị lặp lại. Mỗi package có .dart_tool/package_config.jsonpubspec.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.
  • Path dependencies nhan nhản khắp nơi. Để 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.
  • Version drift. Không có gì buộc networkstorage 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ú ý:

  • Root package (chính là app) cũng là một phần của workspace — nó vừa host workspace vừa consume các member.
  • App phụ thuộc vào các package của chính nó theo tên và version constraint (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 analyticsarchitecture; 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 ý

  • SDK 3.6+ ở khắp nơi. Mọi constraint 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ộ.)
  • Một resolution nghĩa là shared constraints. Bạn không thể giữ 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).
  • Root cũng là một package. Workspace root là một package bình thường với 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.
  • Vị trí lockfile thay đổi. Nếu bạn đang migrate, hãy xóa các file pubspec.lock.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 workspacesMelos
    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 : )

    Enjoyed this read?

    1likes

    Found this helpful?

    Support my work.

    If this post saved you time or taught you something, you can buy me a coffee. Every bit keeps me writing and shipping.

    Support my work.

    If this post saved you time or taught you something, you can buy me a coffee. Every bit keeps me writing and shipping.

    TP BankVietnam

    Scan with any VN banking app

    Show QRHide QR
    Scan to support via Vietnamese bank (VietQR)Scan to pay
    PayPalGlobal

    Pay with your PayPal account

    Support via PayPal
    Show QRHide QR
    Scan to support via PayPalScan to pay
    GitHubGlobal

    Visa / Mastercard · one-time or monthly

    Sponsor on GitHub
    Show QRHide QR
    Scan to sponsor on GitHubScan to pay
    Comments

    Loading…

    END OF SHEET — LET'S BUILD

    Got a hard problem? Let's talk.