Các mẫu mô-đun hoá phổ biến

Stay organized with collections Save and categorize content based on your preferences.

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 khớp nối thấp và độ gắn kết cao

Một cách để mô tả đặc điểm cơ sở mã mô-đun là sử dụng các thuộc tính khớp nốiđộ gắn kết. Thuộc tính khớp nối đo lường mức độ phụ thuộc lẫn nhau của các mô-đun. Trong phạm vi đề cập, thuộc tính độ gắn kết đo lường mức độ liên quan giữa các thành phần của một mô-đun chức năng. Nguyên tắc chung là bạn nên cố gắng để đạt được khớp nối thấp và độ gắn kết cao:

  • Khớp nối thấp có nghĩa là các mô-đun phải độc lập nhất có thể với các mô-đun khác. Nhờ đó, những thay đổi thực hiện trên một mô-đun sẽ có ảnh hưởng tối thiểu 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à các mô-đun phải chứa một bộ mã đóng vai trò như một hệ thống. Bạn phải xác định rõ ràng trách nhiệm của các mô-đun này và luôn tuân thủ các giới hạn về tri thức của một miền nhất định. 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 miền chức năng 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à:

  1. Đóng gói tất cả dữ liệu và logic hoạt động của một miền nhất định: Mỗi mô-đun dữ liệu phải có trách nhiệm xử lý dữ liệu đại diện cho một miền 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.
  2. 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.
  3. Ẩ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ặc internal của Kotlin.
Hình 1: Các mô-đun dữ liệu mẫu và nội dung của các mô-đun đó

Mô-đun tính năng

Tính năng là một phần chức năng của ứng dụng được tách biệt, 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.

Hình 2: Mỗi thẻ của ứng dụng này có thể được xác định là một tính năng

Các tính năng được 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ể người dù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.

Hình 3: Các mô-đun tính năng mẫu và nội dung của các mô-đun đó

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ể xây dựng.

Hình 3: Biểu đồ phần phụ thuộc mô-đun của các phiên bản sản phẩm: “bản minh hoạ” và “bản đầy đủ”

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 nền tảng cụ thể.

Hình 4: Biểu đồ phần phụ thuộc của ứng dụng Wear

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 phương pháp xây dựng thương hiệu phức tạp 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 ứng dụng của bạn được sắp xếp theo chủ đề tập trung, bạn có thể tránh được việc tái cấu trúc gây khó chịu 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 duy trì khớp nối 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ề 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.

Hình 5: Không thể giao tiếp trực tiếp, hai chiều giữa các mô-đun do các phần phụ thuộc tuần hoàn. Cần có mô-đun dàn xếp để điều phối luồng dữ liệu giữa hai mô-đun độc lập khác

Để 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 riêng 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 khớp nối 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ì khớp nối 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.

Hình 6: Hai mô-đun tính năng dựa vào mô-đun dữ liệu dùng chung

Các phương pháp chung hay nhất

Như đã đề cập từ đầu, không có một cách duy nhất phù hợp để phát triển một ứng dụng nhiều mô-đun. Giống như có nhiều 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 bạn đọc mã dễ dàng, dễ bảo trì và 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ể.