0
0
Lập trình
Harry Tran
Harry Tran106580903228332612117

🧱 Phá vỡ Monolith: Hướng dẫn từng bước để Modular hóa ứng dụng Android - Phần 1

Đăng vào 1 tháng trước

• 11 phút đọc

🧱 Phá vỡ Monolith: Hướng dẫn từng bước để Modular hóa ứng dụng Android - Phần 1

Trong phần đầu tiên của bài viết này, chúng ta sẽ tập trung vào:

  • Bản vẽ: quy trình tư duy rõ ràng, kế hoạch tổng thể, và thứ tự di chuyển từng bước an toàn để bạn và nhóm có thể modular hóa một cách tự tin.
  • Triển khai các plugin quy ước để quản lý logic xây dựng Gradle.
  • Triển khai module feature_bookmarks.

Tại sao nên Modular hóa?

Lợi ích:

  • Hiệu suất xây dựng → Chỉ biên dịch những gì đã thay đổi; các xây dựng địa phương và CI nhanh hơn.
  • 👥 Tốc độ đội nhóm → Làm việc song song; cô lập quyền sở hữu tính năng; ít xung đột khi gộp.
  • 🏛 Thực thi kiến trúc → Giữ cho các ranh giới giữa UI, miền và dữ liệu luôn sạch.
  • 🔁 Tính tái sử dụng → Hệ thống thiết kế chung, khách hàng và mô hình trở thành thư viện ổn định.
  • 🧩 Đảm bảo tương lai → Một bước gần hơn tới Dynamic Feature Delivery.

Khi nào không nên modular hóa: ứng dụng tối thiểu, dự án cá nhân, hoặc nguyên mẫu mà chi phí > lợi ích.

Điểm xuất phát: Monolith

Mô-đun hiện tại của chúng ta trông như thế này:

Copy
:app/
├── build.gradle.kts (Tệp Gradle cấp mô-đun cho :app)
├── src/
│   ├── androidTest/
│   │   └── java/
│   │       └── com/vsay/pintereststylegriddemo/
│   │           └── (Các bài kiểm tra có thể chạy, ví dụ: cho UI hoặc điều hướng)
│   ├── main/
│   │   ├── AndroidManifest.xml
│   │   ├── java/
│   │   │   └── com/vsay/pintereststylegriddemo/
│   │   │       ├── MainApplication.kt      (Nếu sử dụng Hilt, @HiltAndroidApp)
│   │   │       ├── MainActivity.kt         (Điểm vào, chứa AppWithTopBar)
│   │   │       │
... (tổ chức cấu trúc thư mục tiếp theo)

Mục tiêu: Bản đồ mô-đun thực tiễn

Chúng ta sẽ hướng tới điều này:

Copy
:app                       // điểm vào (Activity, AppNavHost, thanh dưới, @HiltAndroidApp)
:core-domain               // mô hình, trường hợp sử dụng, giao diện kho dữ liệu (pure Kotlin)
:core-data                 // Retrofit/Room, DTOs, bộ chuyển đổi, repo impls (các mô-đun Hilt cho dữ liệu)
:core-ui                   // chủ đề, kiểu chữ, các thành phần Compose chung
:core-navigation           // hợp đồng tuyến đường, hằng số liên kết sâu
:core-common               // (MỚI) tiện ích chung, lớp cơ sở, hằng số không phải UI/miền

:feature-home              // UI + VM + định nghĩa đồ thị nav
:feature-detail            // UI + VM + định nghĩa đồ thị nav
:feature-profile           // UI + VM + định nghĩa đồ thị nav (bao gồm các đồ thị lồng nhau)
:feature-bookmark          // UI + VM + định nghĩa đồ thị nav

// Tùy chọn, nhưng được khuyến nghị cho các dự án lớn hơn:
:core-testing              // (MỚI - cho các tiện ích kiểm tra chung)
  • Nguyên tắc: Các tính năng phụ thuộc vào bên trong (trên :core-*), không bao giờ phụ thuộc vào nhau. :app phụ thuộc vào tất cả các tính năng và kết nối chúng lại với nhau.

🧭 Quy trình tư duy trước khi chạm đến mã

Trước khi bạn di chuyển một tệp nào, hãy đồng thuận về cách modular hóa sẽ hoạt động. Vội vàng vào việc tách các mô-đun mà không có quy tắc là cách nhanh nhất dẫn đến hỗn loạn. Những nguyên tắc này hoạt động như một la bàn: khi đã đồng thuận, các di chuyển tệp thực tế trở nên cơ học. Nếu không có chúng, modular hóa có thể dễ dàng biến thành tái cấu trúc, lùi bước hoặc lãng phí công sức.

1. Ổn định miền trước

Ý nghĩa:

Trích xuất mô hình miền (ví dụ, User, Image) và giao diện kho dữ liệu (ImageRepository, UserRepository) vào :core-domain. Giữ nó pure Kotlin không có phụ thuộc Android.

Tại sao trước?

  • Hợp đồng ổn định → Mỗi tính năng và lớp dữ liệu phụ thuộc vào các loại miền. Nếu bạn thay đổi chúng trong quá trình di chuyển, tác động sẽ phá vỡ mọi thứ.
  • Tốc độ xây dựng → Các mô-đun pure Kotlin biên dịch nhanh nhất. Khóa chúng lại có nghĩa là hầu hết các thay đổi sẽ không làm vô hiệu hóa chúng.
  • Khả năng kiểm tra → Với miền được cô lập, bạn có thể viết các bài kiểm tra nhanh, chỉ chạy trên JVM cho các quy tắc kinh doanh quan trọng nhất của bạn.

👉 Lý do: Hãy nghĩ về miền như một “API công khai” của logic kinh doanh ứng dụng của bạn. Ổn định nó trước giống như đổ nền móng của một ngôi nhà trước khi di chuyển các bức tường.

2. Trích xuất những thứ tái sử dụng nhiều nhất tiếp theo

Ý nghĩa:

Kéo ra hệ thống thiết kế (màu sắc, kiểu chữ, chủ đề), các thành phần Compose UI tái sử dụng (ví dụ, AppTopBar, ErrorView), và các hằng số điều hướng vào :core-ui:core-navigation.

Tại sao bây giờ?

  • Tái sử dụng cao → Hầu hết các tính năng đều nhập chúng. Để chúng lại trong :app có nghĩa là các tính năng vẫn gắn kết với monolith.
  • Ngăn chặn sự sao chép → Nếu bạn modular hóa tính năng trước, bạn sẽ kết thúc việc sao chép các thành phần trước khi trích xuất chúng.
  • Tiết kiệm xây dựng → Các thành phần UI thay đổi thường xuyên; cách ly chúng có nghĩa là các biên dịch lại sẽ không lan tỏa vào các mô-đun không liên quan.

👉 Lý do: Các hợp đồng UI và đường đi chung giống như “ngôn ngữ chung” của ứng dụng. Trích xuất chúng sớm giúp các tính năng có một từ điển chung để giao tiếp.

3. Chọn thứ tự di chuyển an toàn

Ý nghĩa:

Không di chuyển mọi thứ cùng một lúc. Thay vào đó, hãy tách các tính năng theo thứ tự giảm thiểu rủi ro: Bookmark -> Detail → Profile → Home.

Tại sao thứ tự này?

  • Bookmark → Màn hình composable đơn giản.
  • Detail → Một tính năng lá (chỉ tiêu thụ một ID). Ít phụ thuộc, bán kính tác động tối thiểu.
  • Profile → Cho thấy các đồ thị lồng nhau (Profile → Settings → EditProfile), hữu ích cho việc thử nghiệm tính năng điều hướng.
  • Home → Tính năng trung tâm (feeds, thông báo, phân trang). Phụ thuộc vào nhiều thứ, vì vậy hãy để cuối cùng.

👉 Lý do: Bắt đầu với các tính năng có rủi ro thấp, phụ thuộc thấp để xác thực thiết lập của bạn. Để lại “tính năng trung tâm” cho đến cuối để tránh chặn tiến trình ở nơi khác.

4. Giữ ranh giới điều hướng rõ ràng

Ý nghĩa:

Mỗi tính năng định nghĩa mở rộng NavGraph của riêng mình, nhưng các hằng số đường đi sống trong :core-navigation. :app là nơi duy nhất mà mọi thứ được kết nối với nhau.

Tại sao theo cách này?

  • Đóng gói → Các tính năng không cần biết nội bộ của nhau, chỉ hợp đồng chung.
  • Linh hoạt → Dễ dàng thay thế các tính năng vào/ra (ví dụ, thử nghiệm AB, giao hàng động).
  • Đúng đắn → Tránh các lỗi điều hướng “chuỗi” mong manh bằng cách trung tâm hóa các hợp đồng đường đi.

👉 Lý do: Điều hướng là keo dính, không phải một tính năng. Giữ cho các ranh giới rõ ràng có nghĩa là ứng dụng của bạn mở rộng như các viên gạch LEGO — mỗi tính năng là tự chứa và có thể tái sử dụng.

5. Chuẩn bị ranh giới Tiêm phụ thuộc (DI)

Ý nghĩa:

  • Giao diện (hợp đồng) trong :core-domain.
  • Cài đặt + ràng buộc Hilt trong :core-data.
  • Mô-đun tính năng chỉ phụ thuộc vào các giao diện.

Tại sao mô hình này?

  • Không có phụ thuộc vòng tròn:core-domain không phụ thuộc vào bất kỳ thứ gì.
  • Cô lập tính năng → Các tính năng chỉ tiêm những gì chúng cần (giao diện), không phải toàn bộ cài đặt.
  • Linh hoạt → Dễ dàng thay thế các cài đặt (mạng so với giả lập, DB so với bộ nhớ) cho kiểm tra hoặc thử nghiệm.

👉 Lý do: Đây là sách giáo khoa về Đảo ngược phụ thuộc. Bằng cách làm cho các tính năng phụ thuộc vào các trừu tượng, bạn khóa lại các ranh giới sạch và có được khả năng bảo trì lâu dài.

🛠 Danh sách kiểm tra trước khi di chuyển

Trước khi bạn tách một mô-đun đơn, hãy thiết lập các rào cản này. Bỏ qua chúng dẫn đến độ trôi cấu hình, xung đột tài nguyên và viết lại đau đớn sau này. Ngoài ra, trước khi tạo các mô-đun mới, hãy đảm bảo rằng những điều này có sẵn trong dự án mô-đun đơn hiện tại của bạn. Nếu có, thật tuyệt! Nếu không, bây giờ là thời gian để thiết lập chúng.

Danh sách phiên bản (libs.versions.toml)

  • Hành động: Tạo/cập nhật gradle/libs.versions.toml để tập trung tất cả các phiên bản phụ thuộc (Compose, Kotlin, Hilt, Retrofit, Room, v.v.)
  • Lý do: Tập trung các phiên bản phụ thuộc (Compose BOM, Retrofit, Room) và đảm bảo tính nhất quán phiên bản trên tất cả các mô-đun trong tương lai và đơn giản hóa việc cập nhật. Ngăn chặn xung đột phiên bản khi nhiều mô-đun khai báo cùng một phụ thuộc.
Copy
toml
[versions]
kotlin = "1.9.22"
composeCompiler = "1.5.8"
composeBom = "2024.02.02"
# ... các phiên bản khác

[libraries]
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
# ... các thư viện khác

[plugins]
# ... các plugin gradle

💻⚙️🔧 Plugin quy ước hoặc buildSrc

Có hai tùy chọn để quản lý logic xây dựng Gradle, plugin quy ước hoặc buildSrc. Chúng ta sẽ chọn plugin quy ước vì những lý do dưới đây:

Plugin quy ước:

  • Plugin quy ước là công cụ giúp bạn quản lý các cấu hình xây dựng của các mô-đun này một cách nhất quán và hiệu quả.
  • Đây là một đoạn mã (thường được viết bằng Kotlin hoặc Groovy) áp dụng một tập hợp các cấu hình đã được định nghĩa trước cho một dự án Gradle (một mô-đun).
  • Ví dụ, bạn có thể có một plugin android-library-convention.gradle.kts thiết lập mọi thứ mà một mô-đun thư viện Android điển hình cần.

Ưu điểm:

  • Tách biệt: build-logic là một xây dựng độc lập, không liên kết với classpath dự án chính. Thay đổi không kích hoạt biên dịch lại toàn bộ như buildSrc.
  • Tách biệt rõ ràng các mối quan tâm: Logic xây dựng được tách biệt gọn gàng vào một xây dựng plugin quy ước riêng, không liên quan đến mã nguồn ứng dụng/thư viện của bạn. Điều này thường sạch sẽ hơn so với buildSrc.
  • Tránh những vấn đề của buildSrc: buildSrc có một classpath đặc biệt có thể dẫn đến các vấn đề hoặc xung đột tinh vi. Một xây dựng được bao gồm thông thường thường có một classpath sạch hơn và được cô lập hơn.
  • Khả năng kiểm tra (Nâng cao): Các plugin quy ước trong một xây dựng được bao gồm dễ dàng kiểm tra bằng các bài kiểm tra đơn vị hơn là logic trực tiếp trong buildSrc.
  • Có thể mở rộng: Dễ dàng tách thành nhiều plugin quy ước (ứng dụng Android, thư viện, tính năng, Compose, kiểm thử, v.v.).
  • Được Google khuyến nghị: AndroidX và Nowinandroid sử dụng các plugin quy ước cho các dự án modular quy mô lớn.

Nhược điểm:

  • Một chút phức tạp trong việc thiết lập hơn buildSrc.

Triển khai Plugin quy ước

Bước 1: Tạo Thư mục cho Xây dựng Plugin. Tại gốc dự án PinterestStyleGridDemo của bạn, hãy tạo một thư mục mới: convention-plugins

Bước 2: Khởi tạo Xây dựng Plugin quy ước

  • Tạo convention-plugins/settings.gradle.kts:
Copy
rootProject.name = "pinterest-convention-plugins"

dependencyResolutionManagement {
    versionCatalogs {
        create("libs") { // tạo catalog tên "libs" cho xây dựng này
            from(files("../gradle/libs.versions.toml")) // Chỉ đến tệp TOML trong dự án gốc
        }
    }
}

Bước 3: thêm convention-plugins vào root settings.gradle.kts

Copy
// các mã khác
rootProject.name = "PinterestStyleGridDemo"
include(":app")
includeBuild("convention-plugins")

Triển khai mô-đun feature_bookmarks

Tạo mô-đun thư viện Android :feature-bookmarks trong dự án

  • Tên thư mục/mô-đun → feature-bookmarks
  • Trong cấu hình Gradle, → phải có tiền tố bằng :.

... (tiếp tục với phần chi tiết về việc di chuyển mã và cấu hình)

💡 Kết luận

  • Việc tạo mô-đun tính năng thành công: Chúng ta đã cô lập chức năng ‘bookmarks’ vào thư viện mô-đun riêng của nó (:feature_bookmarks). Đây là bước cơ bản trong việc modular hóa ứng dụng của bạn, cho phép phân tách tốt hơn các mối quan tâm.
  • Logic xây dựng tập trung với các Plugin quy ước: Chúng ta đã thiết lập một mô-đun plugin quy ước (cụ thể là AndroidLibraryConventionPlugin.kt) để định nghĩa và áp dụng các cấu hình xây dựng chung (cài đặt thư viện Android, tùy chọn Kotlin, khả năng tương thích Java, phụ thuộc chung) trên các mô-đun thư viện.

Để xem toàn bộ triển khai và khám phá mã trong ngữ cảnh, bạn có thể tìm thấy toàn bộ dự án trên GitHub: PinterestStyleGridDemo Repository.

Hãy theo dõi tôi trên Medium để có thêm nội dung về Android.

Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào