Không có một chiến lược mô-đun hoá nào phù hợp với mọi dự án. Nhờ sự linh hoạt vốn có của Gradle nên bạn sẽ gặp rất ít hạn chế khi thực hiện việc tổ chức dự án. Trang này cung cấp thông tin tổng quan về một số quy tắc chung cũng như các mẫu phổ biến mà bạn có thể tận dụng khi phát triển ứng dụng Android đa mô-đun.
Nguyên tắc gắn kết chặt chẽ và ít ràng buộc
Một cách để mô tả đặc điểm cơ sở mã mô-đun là sử dụng các khái niệm ràng buộc và gắn kết. Khái niệm "ràng buộc" đo lường mức độ phụ thuộc lẫn nhau của các mô-đun. Trong ngữ cảnh này, khái niệm "gắn kết" đo lường sự liên kết về chức năng giữa các thành phần của một mô-đun. Nguyên tắc chung là chương trình của bạn nên có mức độ gắn kết cao và ràng buộc thấp:
- Ràng buộc thấp có nghĩa là mô-đun càng ít phụ thuộc vào các mô-đun khác càng tốt. Nhờ đó, những thay đổi thực hiện trên một mô-đun sẽ ít hoặc không ảnh hưởng đến các mô-đun khác. Mô-đun không nên có tri thức về hoạt động nội tại của các mô-đun khác.
- Độ gắn kết cao nghĩa là bộ mã trong mô-đun phải giống như một hệ thống. Các mô-đun này phải có trách nhiệm rõ ràng và luôn tuân thủ các giới hạn về nghiệp vụ nhất định của chúng. Hãy cùng xem xét một ứng dụng sách điện tử mẫu. Việc kết hợp các mã lập trình liên quan đến thanh toán và sách trong cùng một mô-đun có thể không phù hợp vì hai mã này là hai chức năng thuộc lĩnh vực khác nhau.
Các loại mô-đun
Tuỳ thuộc vào cấu trúc ứng dụng mà bạn có thể sắp xếp các mô-đun của mình. Dưới đây là một số loại mô-đun phổ biến mà bạn có thể ra mắt trong ứng dụng của mình khi thực hiện theo cấu trúc ứng dụng đề xuất của chúng tôi.
Mô-đun dữ liệu
Mô-đun dữ liệu thường chứa kho lưu trữ, nguồn dữ liệu và các lớp của mô hình. Một mô-đun dữ liệu có 3 trách nhiệm chính là:
- Đóng gói tất cả dữ liệu và logic hoạt động của một lĩnh vực nhất định: Mỗi mô-đun dữ liệu phải có trách nhiệm xử lý dữ liệu của một lĩnh vực nhất định. Mô-đun dữ liệu này có thể xử lý nhiều kiểu dữ liệu, miễn là các kiểu dữ liệu đó có liên quan với nhau.
- Hiển thị kho lưu trữ dưới dạng API bên ngoài: API công khai của mô-đun dữ liệu phải là kho lưu trữ vì API này chịu trách nhiệm hiển thị dữ liệu cho phần còn lại của ứng dụng.
- Ẩn tất cả chi tiết quy trình triển khai và nguồn dữ liệu từ bên ngoài:
Chỉ các kho lưu trữ ở cùng một mô-đun mới có thể truy cập vào nguồn dữ liệu.
Nguồn dữ liệu này vẫn bị ẩn đối với bên ngoài. Bạn có thể thực thi việc này bằng cách sử dụng từ khoá chế độ hiển thị
private
hoặcinternal
của Kotlin.

Mô-đun tính năng
Tính năng là một phần riêng biệt trong chức năng của ứng dụng, thường tương ứng với một màn hình hoặc một loạt màn hình có liên quan chặt chẽ, chẳng hạn như quy trình đăng ký hoặc thanh toán. Nếu ứng dụng của bạn có thanh điều hướng ở dưới cùng, thì có khả năng mỗi đích đến là một tính năng.

Các tính năng liên kết với màn hình hoặc đích đến trong ứng dụng của bạn. Do đó, có thể chúng sẽ có một giao diện người dùng liên kết và ViewModel
để xử lý logic và trạng thái. Một tính năng đơn lẻ không nhất thiết phải được giới hạn ở một thành phần hiển thị hoặc đích đến điều hướng. Mô-đun tính năng phụ thuộc vào các mô-đun dữ liệu.

Mô-đun ứng dụng
Mô-đun ứng dụng là điểm truy cập đến ứng dụng. Các mô-đun này phụ thuộc vào các mô-đun tính năng và thường cung cấp tính năng điều hướng gốc. Một mô-đun ứng dụng đơn lẻ có thể được biên dịch thành một số tệp nhị phân nhờ các biến thể bản dựng.

Nếu ứng dụng của bạn nhắm đến nhiều loại thiết bị (chẳng hạn như ô tô, thiết bị đeo thông minh hoặc TV) thì bạn có thể cân nhắc việc xác định một mô-đun ứng dụng cho mỗi loại thiết bị. Điều này giúp tách biệt các phần phụ thuộc dành riêng cho từng nền tảng.

Các mô-đun phổ biến
Các mô-đun phổ biến, còn gọi là mô-đun cốt lõi, chứa mã mà các mô-đun khác thường sử dụng. Các mô-đun này giúp giảm tình trạng thừa mã và không đại diện cho lớp cụ thể nào trong cấu trúc của một ứng dụng. Sau đây là ví dụ về các mô-đun phổ biến:
- Mô-đun giao diện người dùng: Nếu sử dụng các thành phần tuỳ chỉnh trên giao diện người dùng hoặc áp dụng các chi tiết xây dựng thương hiệu trong ứng dụng, bạn nên cân nhắc việc đóng gói bộ sưu tập tiện ích thành một mô-đun để tất cả tính năng có thể sử dụng lại. Điều này có thể giúp giao diện người dùng trở nên nhất quán trên các tính năng khác nhau. Ví dụ: nếu chủ đề của ứng dụng được tập trung vào một nơi, bạn có thể tránh việc dành hàng tấn thời gian để tái cấu trúc khi đổi mới thương hiệu.
- Mô-đun phân tích số liệu: Việc theo dõi thường được quyết định bởi các yêu cầu kinh doanh mà không cần quan tâm nhiều đến cấu trúc phần mềm. Trình theo dõi phân tích số liệu thường được sử dụng trong nhiều thành phần không liên quan. Nếu gặp phải trường hợp này, bạn nên có mô-đun phân tích số liệu chuyên dụng.
- Mô-đun mạng: Khi có nhiều mô-đun yêu cầu kết nối mạng, bạn có thể cân nhắc việc có một mô-đun chuyên cung cấp ứng dụng khách http. Việc này đặc biệt hữu ích khi ứng dụng khách yêu cầu cấu hình tuỳ chỉnh.
- Mô-đun tiện ích: Tiện ích, hay còn gọi là trình trợ giúp, thường là những đoạn mã nhỏ được sử dụng lại trên ứng dụng. Ví dụ về các tiện ích: trình trợ giúp kiểm thử, hàm định dạng đơn vị tiền tệ, trình xác thực email hoặc toán tử tuỳ chỉnh.
Giao tiếp giữa mô-đun với mô-đun
Các mô-đun hiếm khi tồn tại một cách hoàn toàn tách biệt và thường dựa vào, cũng như giao tiếp với các mô-đun khác. Việc giữ mức độ ràng buộc thấp là rất quan trọng, ngay cả khi các mô-đun hoạt động cùng nhau và trao đổi thông tin thường xuyên. Đôi khi, bạn không nên cho hai mô-đun giao tiếp trực tiếp với nhau, như trong trường hợp ràng buộc về mặt cấu trúc. Thậm chí cũng có khả năng không thực hiện được việc này, chẳng hạn như với các phần phụ thuộc tuần hoàn.

Để khắc phục vấn đề này, bạn có thể tạo một mô-đun dàn xếp giữa hai mô-đun khác. Mô-đun dàn xếp có thể theo dõi thông báo từ cả hai mô-đun và chuyển tiếp các thông báo này khi cần. Trong ứng dụng mẫu của chúng ta, màn hình thanh toán cần biết cuốn sách nào cần mua mặc dù sự kiện bắt nguồn từ một màn hình khác, thuộc về một tính năng khác. Trong trường hợp này, mô-đun dàn xếp là mô-đun sở hữu biểu đồ điều hướng (thường là mô-đun ứng dụng). Trong ví dụ này, chúng ta sử dụng tính năng điều hướng để truyền dữ liệu từ tính năng trang chủ đến tính năng thanh toán bằng thành phần Điều hướng.
navController.navigate("checkout/$bookId")
Đích đến thanh toán sẽ nhận được mã nhận dạng của sách để làm đối số, cụ thể là để tìm nạp thông tin về sách. Bạn có thể sử dụng Ô điều khiển trạng thái đã lưu để truy xuất các đối số điều hướng bên trong ViewModel
của tính năng đích.
class CheckoutViewModel(savedStateHandle: SavedStateHandle, …) : ViewModel() {
val uiState: StateFlow<CheckoutUiState> =
savedStateHandle.getStateFlow<String>("bookId", "").map { bookId ->
// produce UI state calling bookRepository.getBook(bookId)
}
…
}
Bạn không nên truyền các đối tượng dưới dạng đối số điều hướng. Thay vào đó, hãy sử dụng mã nhận dạng đơn giản mà các tính năng có thể sử dụng để truy cập và tải tài nguyên mong muốn từ lớp dữ liệu. Bằng cách này, bạn sẽ duy trì được ràng buộc thấp và không vi phạm nguyên tắc nguồn đáng tin cậy duy nhất.
Trong ví dụ bên dưới, cả hai mô-đun tính năng đều phụ thuộc vào cùng một mô-đun dữ liệu. Điều này giúp bạn giảm thiểu lượng dữ liệu mà mô-đun dàn xếp cần chuyển tiếp và duy trì ràng buộc giữa các mô-đun ở mức thấp. Thay vì truyền đối tượng, các mô-đun phải trao đổi mã nhận dạng gốc và tải tài nguyên từ một mô-đun dữ liệu dùng chung.

Các phương pháp chung hay nhất
Như đã đề cập từ đầu, có nhiều cách phù hợp để phát triển một ứng dụng nhiều mô-đun. Tương tự với việc có đa dạng cấu trúc phần mềm, có nhiều cách để mô-đun hoá ứng dụng. Tuy nhiên, các đề xuất chung sau đây có thể giúp mã của bạn dễ đọc, dễ bảo trì và dễ kiểm thử hơn.
Duy trì tính nhất quán của cấu hình
Mỗi mô-đun đều có mức hao tổn cấu hình riêng. Nếu số lượng mô-đun của bạn đạt đến một ngưỡng nhất định, thì việc quản lý tính nhất quán của cấu hình sẽ trở nên rất khó khăn. Chẳng hạn, điều quan trọng là các mô-đun phải sử dụng cùng một phiên bản của các phần phụ thuộc. Nếu bạn chỉ cần cập nhật một phiên bản phần phụ thuộc nhưng lại phải cập nhật số lượng lớn mô-đun thì đó không chỉ là sự hao công tốn sức mà còn là kẽ hở có thể phát sinh ra lỗi. Để giải quyết vấn đề này, bạn có thể sử dụng một trong các công cụ của Gradle để tập trung vào cấu hình của mình:
- Danh mục phiên bản là một danh sách kiểu phần phụ thuộc do Gradle tạo ra trong quá trình đồng bộ hoá. Đây là tâm điểm để khai báo tất cả phần phụ thuộc của bạn và có sẵn cho tất cả các mô-đun trong một dự án.
- Hãy sử dụng trình bổ trợ quy ước để chia sẻ logic bản dựng giữa các mô-đun.
Hiển thị càng ít càng tốt
Giao diện công khai của một mô-đun phải ở mức tối thiểu và chỉ hiển thị các mục thiết yếu. Điều này sẽ không làm rò rỉ chi tiết triển khai nào ra bên ngoài. Giới hạn mọi thứ trong phạm vi nhỏ nhất có thể. Dùng phạm vi hiển thị private
hoặc internal
của Kotlin để đặt các phần khai báo ở chế độ riêng tư đối với mô-đun. Khi khai báo phần phụ thuộc trong mô-đun, hãy ưu tiên implementation
hơn api
. Phần sau hiển thị các phần phụ thuộc bắc cầu cho người dùng mô-đun của bạn. Việc sử dụng phương thức triển khai có thể cải thiện thời gian xây dựng vì sẽ làm giảm số lượng mô-đun cần xây dựng lại.
Ưu tiên các mô-đun Kotlin và Java
Có 3 kiểu mô-đun thiết yếu mà Android Studio hỗ trợ:
- Mô-đun ứng dụng là điểm truy cập đến ứng dụng. Các mô-đun này có thể chứa mã nguồn, tài nguyên, tài sản và
AndroidManifest.xml
. Kết quả đầu ra của một mô-đun ứng dụng là một Android App Bundle (AAB) hoặc một Gói ứng dụng Android (APK). - Mô-đun thư viện có cùng nội dung với các mô-đun ứng dụng. Các mô-đun Android khác sử dụng các mô-đun thư viện làm phần phụ thuộc. Kết quả đầu ra của một mô-đun thư viện là một Android ARchive (AAR) có cấu trúc giống hệt với các mô-đun ứng dụng nhưng được biên dịch thành một tệp Android Archive (AAR) sau đó có thể được các mô-đun khác sử dụng làm phần phụ thuộc. Mô-đun thư viện cho phép bạn đóng gói và sử dụng lại cùng một logic và tài nguyên trên nhiều mô-đun ứng dụng.
- Thư viện Kotlin và Java không chứa tài nguyên Android, tài sản hoặc tệp kê khai nào.
Vì các mô-đun Android đi kèm với mức hao tổn, tốt nhất bạn nên sử dụng loại Kotlin hoặc Java nhiều nhất có thể.