Dependency Manager. An approach to multiple repositories in Flutter
Introduction
Bởi vì mã nguồn bắt đầu tăng lên, trong một môi trường nơi bạn đang phát triển rất nhiều dự án, việc có một vài dependencies/libraries được chia sẽ giữa các dự án là chuyện hết sức bình thường. Điều này có nghĩa là nếu bạn không muốn copy/paste các tính năng tương tự, chúng ta cần tạo các packages độc lập cái có thể được tái sử dụng trong nhiều dự án khác nhau, một cách thức cái đã được thảo luận trong bài viết này: Modular Flutter Apps - Design and Considerations.
Trong một công ty tôi đã làm việc, chúng tôi đã sử dụng rất nhiều repositories trong quá trình phát triển của mình, nhằm tạo nhiều ứng dụng sử dụng Flutter Web. Tuy nhiên cách thức này hình thành một vấn đề mới - Nếu mỗi package có những versions(phiên bản) khác nhau, chúng ta quản lý version và tái sử dụng chúng như thế nào cho tất cả các packages và projects?
Trong bài viết này, chúng ta sẽ thảo luận về một số cách thức có thể để làm thế nào chúng ta có thể quản lý các dependencies thông quan nhiều các repositories và trình bày về cách thức mà công ty tôi đã sử dụng.
Để phục vụ mục đích này, chúng ta sẽ xem xét một ứng dụng gọi là app_a cái sẽ phụ thuộc vào 3 libraries là lib_a, lib_b, và lib_c. Thêm nữa, cả lib_a và lib_b cùng phụ thuộc vào lib_c.
Approach 1: Path Dependencies
Cách thức đơn giản nhất để thiết lập project này là tạo một project folder cái mà chúng ta đặt tất cả các libraries và applications, do đó chúng ta có thể tham chiếu chúng theo các đường dẫn liên quan. Để cái này có thể làm việc được, chúng ta sẽ cần tạo một folder structure tuyệt đối/chính xác, cái chúng ta có thể tham chiếu tới mỗi project.
.
├── app_a
├── lib_a
├── lib_b
└── lib_c
Bây giờ chúng ta có thể thêm vào các dependencies cho pubspec.yaml sử dụng các tham số path:
name: app_a
version: 1.0.0+1
dependencies:
# Other dependencies
lib_a:
path: ../lib_a
lib_b:
path: ../lib_b
lib_c:
path: ../lib_c
Áp dụng tương tự với lib_a và lib_b:
name: lib_a
version: 1.0.0+1
dependencies:
# Other dependencies
lib_c:
path: ../lib_c
Nếu chúng ta chạy flutter pub get, chúng ta có thể lấy được các dependencies thành công và nếu chúng ta chạy ứng dụng của mình thông qua flutter run -d chrome nó sẽ khởi chạy ứng dụng của chúng ta. Tuy nhiên, cách thức này có một vài vấn đề khi chúng ta sử dụng nó trong một môi trường nơi mà chúng ta làm việc với cả một team.
Đầu tiên, với cách thức này mỗi nhà phát triển sẽ phải cập nhật các versions một cách chính xác cho mỗi library trong quá trình phát triển. Điều này có thể dẫn tới các tình huống nơi mà hai nhà phát triển đang làm việc trên app_a có thể sử dụng các phiên bản khác nhau của lib_c, điều này không chỉ dẫn tới các xung đột mà còn các các vấn đề phát triển khác bởi vì mỗi nhà phát triển sẽ chạy một phiên bản khác nhau của cùng một ứng dụng.
Tóm lại, cách thức này có thể lỗi bởi vì chúng ta không thể quy định việc đặt version một cách nghiêm ngặt cho các thư viện khác nhau. Và với lý do này, chúng ta sẽ xem xét một đề xuất khác là submodules.
Approach 2: Submodules
Với submodules chúng ta có thể sử dụng các lợi ích của một path dependency kèm theo phiên bản tham chiếu chính xác, bởi vì mỗi submodule có thể chỉ tới một tham chiếu git cụ thể.
Khi thêm một submodule, chúng ta đang thêm vào một git repository cho repository chính của mình điều này có nghĩa là mỗi cái sẽ có commit tree của riêng nó. Chúng ta có thể quy định về commit, branch, hay tag nào sẽ được tham chiếu tới - cái chúng ta sẽ sử dụng cho mỗi submodule. Điều đó có nghĩa là chúng ta có được điều mình muốn là các nhà phát triển khác nhau có thể sử dụng cùng một phiên bản của một library.
Hãy bắt đầu bằng việc thêm vào git submodule cho lib_a và lib_b.
git submodule add https://github.com/Vanethos/dm_lib_c.git
Cái này tạo ra một folder mới gọi là dm_lib_c trong thư mục gốc của các thư viện của chúng ta và thêm vào một file .gitmodules. Trong file này, chúng ta có thể chỉ định git branch hoặc tham chiếu nào đó mà chúng ta muốn sử dụng. Trong ví dụ mẫu này, chúng ta sẽ sử dụng master branch cho lib_c.
[submodule “dm_lib_c”]
path = dm_lib_c
url = https://github.com/Vanethos/dm_lib_c.git
branch = master
Bởi vì submodule này được thêm vào như là một đường dẫn mới(new git path), chúng ta có thể tham chiếu tới nó trong pubspec.yaml file như là một đường dẫn tham chiếu thông thường:
# ...
dependencies:
# ...
lib_c:
path: ./dm_lib_c
Chúng ta sử dụng cách thức tương tự trong app_a nhằm thêm vào lib_a, lib_b và lib_c như là các dependencies, và đối với mỗi các chúng ta chỉ rõ branch nào cái chúng ta cần sử dụng trong gitmodules file:
[submodule "dm_lib_a"]
path = dm_lib_a
url = https://github.com/Vanethos/dm_lib_a.git
branch = submodules
[submodule "dm_lib_b"]
path = dm_lib_b
url = https://github.com/Vanethos/dm_lib_b.git
branch = submodules
[submodule "dm_lib_c"]
path = dm_lib_c
url = https://github.com/Vanethos/dm_lib_c.git
branch = master
Và cập nhật pubspec.yaml file như bên dưới:
name: app_a
version: 1.0.0+1
dependencies:
#...
lib_a:
path: ./dm_lib_a
lib_b:
path: ./dm_lib_b
lib_c:
path: ./dm_lib_c
Cuối cùng, chúng ta có thể chạy flutter pub get trong app_a của mình để lấy về các dependencies. Tuy nhiên, khi chúng ta thực hiện điều này chúng ta sẽ thấy một thông báo lỗi như sau:
Because app_a depends on lib_b from path which depends on lib_c from path, lib_c from path dm_lib_b/dm_lib_c is required.
So, because app_a depends on lib_c from path dm_lib_c, version solving failed.
Running "flutter pub get" in app_a...
pub get failed (1; So, because app_a depends on lib_c from path dm_lib_c, version solving failed.)
Vậy điều gì đang xảy ra ở đây?
Khi pubspec cố gắng nhằm thực hiện các công việc với các dependencies nó có một nguyên tắc: Tất cả các dependencies phải có cùng một version, path, hoặc git reference. Điều này giải thích vì sao khi thêm vào một dependency mới cho các projects của mình, chúng ta thường thêm nó với kí thự ^ - điều này cho phép pubspec tìm một phiên bản phù hợp cho phần phụ thuộc trong một tập hợp các nguyên tắc nghiêm ngặt cái chúng ta có thể tìm thấy ở tài liệu chính thức.
Trong thực tế, khi chúng ta thêm các submodules vào mỗi thư viện, chúng ta đã chỉ rõ path cho các dependencies của mình. Hãy xem xét một các kĩ hơn vào cấu trúc project hiện tại của chúng ta khi mà các modules đã được thêm vào app_a:
.
├── dm_lib_a
│ └── dm_lib_c
├── dm_lib_b
│ └── dm_lib_c
└── dm_lib_c
app_a sẽ có một dependency là lib_c với đường dẫn /dm_lib_c, tuy nhiên, lib_a sẽ có đường dẫn tới dependency của lib_c là /dm_lib_a/dm_lib_c, điều này dẫn tới xung đột trong pubspec bởi vì nó không thể xác định phiên bản nào sẽ được sử dụng.
Mặc dù thực tế là cách thức này nghe có vẻ tốt bởi vì chúng ta có thể định nghĩa một các chính xác cho mỗi libraries nhưng một cách vô hình nó tạo ra các vấn đề cho việc quản lý các dependencies cho pubspec.
Tuy nhiên, cách thức này mang đến cho chúng ta một ý tưởng để làm thế nào chúng ta có thể giải quyết các vấn đề của mình - đó chính là việc sử dụng các git references cho mỗi package. Hãy khám phá các thức đó.
Approach 3: Using git references
Như đã thấy trong Dart dependencies documentation, chúng ta có thể thêm vào một dependency cho pubspec.yaml thông qua một git reference:
ref có thể là bất cứ git reference này - một tag, một branch, hoặc một commit. Trong trường hợp của ví dụ này, chúng ta đang sử dụng các tham chiếu tới branch nhằm định danh cho ứng dụng của mình, do đó chúng ta có thể sử dụng các bên dưới nhằm định danh lib_c trong pubspec.yaml của lib_a và lib_c.
name: lib_a
version: 1.0.0+1
dependencies:
lib_c:
git:
url: https://github.com/Vanethos/dm_lib_c.git
ref: git_reference_tag
Trong ứng dụng app_a chúng ta có thể tham chiếu tới tất các cả thư viện khác:
name: app_a
version: 1.0.0+1
dependencies:
lib_a:
git:
url: https://github.com/Vanethos/dm_lib_a.git
ref: git_reference_tag_a
lib_b:
git:
url: https://github.com/Vanethos/dm_lib_b.git
ref: git_reference_tag_b
lib_c:
git:
url: https://github.com/Vanethos/dm_lib_c.git
ref: git_reference_tag
Và nếu chúng ta thử chạy ứng dụng của mình, ứng dụng của chúng ta có thể lấy tất cả các dependencies, compile và run. Như vậy chúng ta đã vượt qua được vấn đề của cách thức trên để có thể biên dịch và chạy được ứng dụng cũng như đã ứng dụng được ý tưởng của nó cho việc sử dụng git references nhằm lựa chọn chính xác các phiên bản cho mỗi thư viện.
Với cách thức này mỗi nhà phát triển sẽ có thể chạy và làm việc trên cùng một phiên bản của các thư viện trên ứng dụng, điều này giảm thiểu xung đột và lỗi trong quá trình phát triển sản phẩm.
Dù sao thì có một vấn đề mới với cách thức này. Như chúng ta đã đề cập trong cách thức trước, tất cả các dependencies phải có cùng reference(tham chiếu), điều đó có nghĩa rằng nếu chúng ta cần cập nhật get reference cho bất cứ thư viện nào, chúng ta sẽ cần tạo một commit trong mỗi thư viện mà tham chiếu tới nó. Điều này có thể phát sinh một vấn đề là nếu chúng ta đang làm việc tỏng một dự án có hơn 20 thư viện(libraries) cái có thể có rất nhiều phần phụ thuộc lẫn nhau và liên tục được cập nhất. Quá trình đánh dấu hoặc cập nhật mỗi thư viện có thể gây tốn kém thời gian và dễ gặp lỗi, cái có thể dẫn tới việc chúng ta phải huỷ bỏ giải pháp này.
Một cách thức chúng ta có thể giải quyết vấn đề này là đặt các phần phụ thuộc/thư viện(dependencies/libraries) trong một remote repository - Sẽ được giới thiệu trong Dependency Manager.
Proposed Solution - Dependency Manager
Xuất phát từ các cách thức trên, chúng ta giờ đây biết về hai thứ:
- Nếu chúng ta sử dụng git reference, tất cả các nhà phát triển trong cùng một nhóm sẽ sử dụng được cùng một phiên bản cho mỗi thư viện.
- Vấn đề với cách thức này đó là mỗi lần có một cập nhật mới cho mỗi repository cụ thể, chúng ta phải cập nhật toàn bộ file pubspec với phiên bản mới đó.
Vậy điều gì sẽ xảy ra nếu có một cách thức để có một remote repository nơi chúng ta sẽ kiểm soát tất cả các dependenciees của mình? Đó là ý tưởng chính đằng sau dependency manager.
Dependency Manager(còn được biết đến như là Bill of Materials), là một repository bên ngoài, nơi chúng ta quy định các phiên bản cho tất cả các thư viện cái chúng ta sử dụng. Chúng ta có thể có nhiều Dependency Managers nếu cần, nhưng chúng ta có thể sử dụng những nguyên tắc chính sau đây:
- Nếu chúng ta có các libraries cái được sử dụng trong các projects khác nhau chúng ta sẽ sử dụng một Dependency Manager riêng cho chúng.
- Đối với mỗi ứng dụng, chúng ta có một dependency manager riêng.
Nhưng làm thế nào chúng ta tạo được dependency manager?
Đầu tiên, trong pubspec.yaml file chúng ta thêm vào tất cả các dependencies cái chúng ta muốn:
name: dependency_manager
version: 1.0.0+1
dependencies:
lib_a:
git:
url: https://github.com/Vanethos/dm_lib_a.git
ref: git_reference_tag_a
lib_b:
git:
url: https://github.com/Vanethos/dm_lib_b.git
ref: git_reference_tag_b
lib_c:
git:
url: https://github.com/Vanethos/dm_lib_c.git
ref: git_reference_tag
Rồi bên trong thư mục lib chúng ta phải tạo một dart file với mục đích là đưa ra tất cả các dependencies của mình, làm cho chúng có thể sẵn sàng cho tất cả cac projects cái phụ thuộc vào cái này.
library dependency_manager;
export 'package:lib_a/lib_a.dart';
export 'package:lib_b/lib_b.dart';
export 'package:lib_c/lib_c.dart';
Bằng cách thực hiện này, khi một file sử dụng(import) package:dependency_manager/dependency_manager.dart nó sẽ tự động có quyền truy cập tới lib_a/lib_a.dart, lib_b/lib_b.dart, và lib_c/lib_c.dart.
Cuối cùng, để tạo cho các depeendencies sẵn sàng, chúng ta thêm vào dependency_manager như là một git reference cho mỗi library, ví dụ:
name: lib_a
version: 0.0.1
dependencies:
flutter:
sdk: flutter
dependency_manager:
git:
url: https://github.com/Vanethos/dm_dependency_manager.git
ref: master
Có điều gì đó kì lạ trong quá trình xử lý này - Chúng ta có library của mình như là một dependency cho chính nó và chúng ta sẽ có rất nhiều mã nguồn của các dependencies cái chúng ta sẽ không sử dụng. Đây là nơi Tree Shaking xảy ra - tất cả mã nguồn cái chúng ta sẽ không sử dụng sẽ được loại bỏ khi chúng ta biên dịch thành mã nhị nhân(các binaries) của mình.
Để dependency_manager của chúng ta làm việc, tất cả các libraries của chúng ta cần được chỉ tới cùng một tham chiếu của nó do đó sẽ không có xung đột. Như một ví dụ - thông qua việc chúng ta sử dụng remote dependencies, cái xảy ra nếu chúng ta làm việc cục bộ trên một tính năng của lib_b và cần xem xét nó được đối xử như thế nào khi tích hợp vào app_a? Nếu chúng ta trung thành với cấu trúc folder bởi việc quy định trong path của các dependencies, chúng ta có thể làm như bên dưới:
name: app_a
version: 0.0.1
dependencies:
flutter:
sdk: flutter
dependency_manager:
git:
url: https://github.com/Vanethos/dm_dependency_manager.git
ref: master
dependency_overrides:
lib_b:
path: ./dm_lib_b
Khi pubspec biên dịch các dependencies, nó sẽ lấy về tất cả chúng cho dependency_manager trừ lib_b cái sẽ tham chiếu từ path.
Có một lợi thế lớn khi làm việc với các nhóm phân phối(distributed teams) - Khi một vài nhà phát triển đagn làm việc trên cùng một ứng dụng, nếu một trong số họ tạo một PR(Pull Request), cô/anh ta có thể sử dụng dependency_overrides nhằm chỉ rõ PR branch, điều đó giúp cho một reviewer bất kì có thể biên dịch ứng dụng trên máy của họ một cách dễ dàng hơn.
name: app_a
version: 0.0.1
dependencies:
flutter:
sdk: flutter
dependency_manager:
git:
url: https://github.com/Vanethos/dm_dependency_manager.git
ref: master
dependency_overrides:
lib_b:
git:
url: https://github.com/Vanethos/dm_lib_b.git
ref: feature/PR-1-adds_calculator
Trong ví dụ này, khi chúng ta sử dụng dependency_overrides, chúng ta sẽ có thể biên dịch ứng dụng với tính năng mới cái được phát triển trên nhánh feature/PR-1-adds_calculator.
Nếu chúng ta đang thay đổi rất nhiều libraries, chúng ta cũng có thể sử dụng dependency_overrideds với dependency_manager.
Đầu tiên, chúng ta tạo một branch mới với cùng tên chức năng: feature/PR-1-adds_calculator. Trong nó, chúng ta chỉ rõ ref cho tất cả các branches chung ta đang làm việc trên đó, ví dụ, chúng ta sử thay đổi lib_a và lib_b.
name: dependency_manager
version: 1.0.0+1
dependencies:
lib_a:
git:
url: https://github.com/Vanethos/dm_lib_a.git
ref: feature/PR-1-adds_calculator
lib_b:
git:
url: https://github.com/Vanethos/dm_lib_b.git
ref: feature/PR-1-adds_calculator
lib_c:
git:
url: https://github.com/Vanethos/dm_lib_c.git
ref: dm_ref_c
Rồi, khi tạo một PR, chúng ta cần nói với các nhà phát triển khác rằng họ có thể sử dụng theo dependency_overrides
dependency_overrides:
dependency_manager:
git:
url: https://github.com/Vanethos/dm_dependency_manager.git
ref: feature/PR-1-adds_calculator
Với một overrride, chúng ta cơ bản quy định rằng cả lib_a và lib_b sẽ nhằm tới nhánh feature/PR-1-adds_calculator cái chúng ta đã làm việc trên đó. Điều này giúp cho quá trình build và deploy trở nên dễ dàng hơn nếu cần.
Downsides of using a Dependency Manager
Mặc dù chúng ta đã xem xét rất nhiều về dependency_manager, vẫn có một số vấn đề chúng ta cần biết.
Cái đầu tiên đó là bởi vì chúng ta đang sử dụng rất nhiều git reference, khi chúng ta cố gắng lấy các packages mới thông qua flutter pub get, nó sẽ tốn thời gian hơn bởi vì pubspec cần phải xác định tất cả các dependencies, git references và tải chúng.
Thứ hai, đó là vì chúng ta đang sử dụng branch reference cho Dependency Manager, flutter pub get có thể không lấy về phiên bản cuối cùng, điều này có nghĩa là chúng ta cần nâng cấp các packages. Tuy nhiên, điều này có thể gây ra một vấn đề - có thể chúng ta cần lấy phiên bản gần nhất của Dependency Manager nhưng chúng ta không muốn cập nhật tất cả các packages khác - Do đó làm thế nào chúng ta có thể giải quyết vấn đề này? Bằng cách chỉ trực tiếp tới package cái chúng ta đang cập nhật: flutter upgrade dependency_manager.
Với câu lệnh này chúng ta sẽ chỉ nâng cấp dependency_manager.
Cuối cùng, như chúng ta đã thấy với get references, mỗi lần chúng ta hoàn thành một feature mới trên một library chúng ta sẽ cần phát hành một phiên bản mới cho package đó, điều đó có nghĩa là chúng ta cần thực hiện:
- Tạo một tag mới trong repository tương ứng.
- Cập nhật Dependency Manager với tag gần nhất. Quá trình xử lý này có thể tốn thời gian và gây ra lỗi, đó là lý do tại sao nó được đề xuất rằng chúng ta nên tạo các scripts để tự động thực hiện điều này.
Conclusion
Như chúng ta đã thấy, có rất nhiều cách cho chúng ta để quản lý các dependencies trong pubspec, và tất nhiên một số có thể phù hợp với các dự án nhỏ, nó sẽ trở nên khó khăn nhằm tạo ra một giải pháp nhằm làm tăng khả năng mở rộng và tái sử dụng mã nguồn trong một codebase lớn.
Giải pháp chúng ta đề cập trong bài viết này là một giải pháp thay thế nhằm quản lý các dependencies cho mình bằng cách sử dụng một remote repository, và dĩ nhiên nó gây ra một số vấn đề nhất là với các câu lệnh flutter pub get, chúng ta cảm thấy rằng giải pháp này phù hợp với mục đích của mình.
Có một giải pháp khác không được đề cập trong bài viết này - tạo một pub server cho riêng mình cái chúng ta có thể sử dụng để tổ chức các dependencies. Điều này sẽ gây ra các vấn đề khác, như là thời gian thiết lập cho server, và chi phí để duy trì nó, và nó sẽ không thể luôn giải quyết cho toàn bộ vấn đề - như làm thế nào chúng ta có thể cập nhật các dependencies cho toàn bộ các libraries? và làm thế nào chúng ta quy định các dependencies khi một ai đó muốn kiểm thử bằng tay một PR?
Một ý nghĩ cuối - Những loại vấn đề sẽ tăng lên khi chúng ta bắt đầu tạo các ứng dụng cái sẽ được phát triển và bảo trì trong một khoảng thời gian dài. Nó là khó nhằm tạo ra một giải pháp tốt, vì vậy đó là lý do tại sao tôi khuyến nghị cá nhân cho việc chia sẻ hiểu biết và thảo luận về nó một cách cởi mở - Bạn đã bao giờ gặp vấn đề tương tự như này chưa? Bạn đã làm gì để giải quyết vấn đề đó?
Link tham chiếu tới github repositories các bạn có thể tìm thấy ở đây: dm_dependency_manager, dm_app_a, dm_lib_a, dm_lib_b, dm_lib_c.
Source
https://gpalma.pt/blog/dependency_manager/
Reference
Note
Những bài đăng trên viblo của mình nếu có phần Source thì đây là một bài dịch từ chính nguồn được dẫn link tới bài gốc ở phần này. Đây là những bài viết mình chọn lọc + tìm kiếm + tổng hợp từ Google trong quá trình xử lý issues khi làm dự án thực tế + có ích và thú vị đối với bản thân mình. => Dịch lại như một bài viết để lục lọi lại khi cần thiết. Do đó khi đọc bài viết xin mọi người lưu ý:
1. Các bạn có thể di chuyển đến phần source để đọc bài gốc(extremely recommend).
2. Bài viết được dịch lại => Không thể tránh khỏi được việc hiểu sai, thiếu xót, nhầm lẫn do sự khác biệt về ngôn ngữ, ngữ cảnh cũng như sự hiểu biết của người dịch => Rất mong các bạn có thể để lại comments nhằm làm hoàn chỉnh vấn đề.
3. Bài dịch chỉ mang tính chất tham khảo + mang đúng ý nghĩa của một translated article.
4. Hy vọng bài viết có chút giúp ích cho các bạn(I hope so!). =)))))))
All rights reserved