Hướng dẫn về cấu trúc ứng dụng

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

Hướng dẫn này trình bày các phương pháp hay nhất và cấu trúc được đề xuất để tạo ứng dụng mạnh mẽ và chất lượng cao.

Trải nghiệm người dùng trong ứng dụng di động

Một ứng dụng Android thông thường chứa nhiều thành phần ứng dụng (component), bao gồm hoạt động (activity), mảnh (fragment), dịch vụ (service), nhà cung cấp nội dung (content provider) và broadcast receiver. Bạn khai báo hầu hết các thành phần ứng dụng này trong tệp kê khai ứng dụng. Sau đó, Hệ điều hành Android sẽ sử dụng tệp này để quyết định cách tích hợp ứng dụng của bạn vào trải nghiệm người dùng tổng thể của thiết bị. Vì một ứng dụng Android thông thường có thể chứa nhiều thành phần và người dùng thường tương tác với nhiều ứng dụng trong một khoảng thời gian ngắn, các ứng dụng cần phải điều chỉnh cho phù hợp với nhiều loại quy trình công việc và thao tác do người dùng thực hiện.

Xin lưu ý rằng thiết bị di động cũng bị hạn chế về tài nguyên. Do đó, bất cứ lúc nào, hệ điều hành cũng có thể xoá một số quy trình ứng dụng để tạo không gian cho các quy trình mới.

Do các điều kiện của môi trường này, bạn có thể chạy các thành phần ứng dụng một cách riêng lẻ và không theo thứ tự, đồng thời hệ điều hành hoặc người dùng có thể huỷ bỏ các thành phần đó bất cứ lúc nào. Vì các sự kiện này không thuộc quyền kiểm soát của bạn nên bạn không nên lưu trữ hoặc ghi nhớ bất kỳ dữ liệu hoặc trạng thái ứng dụng nào trong các thành phần ứng dụng và các thành phần ứng dụng không được phụ thuộc lẫn nhau.

Nguyên tắc cấu trúc phổ biến

Nếu không nên sử dụng các thành phần ứng dụng để lưu trữ dữ liệu và trạng thái của ứng dụng, vậy bạn nên thiết kế ứng dụng như thế nào?

Khi các ứng dụng Android phát triển về kích thước, quan trọng là bạn phải xác định một cấu trúc cho phép ứng dụng mở rộng quy mô, tăng tính mạnh mẽ của ứng dụng và làm cho ứng dụng dễ dàng kiểm thử hơn.

Cấu trúc ứng dụng xác định ranh giới giữa các phần của ứng dụng và trách nhiệm từng phần nên có. Để đáp ứng các nhu cầu được đề cập ở trên, bạn nên thiết kế cấu trúc ứng dụng theo một vài nguyên tắc cụ thể.

Tách biệt các mối lo ngại

Nguyên tắc quan trọng nhất cần tuân thủ là tách biệt các mối lo ngại. Có một lỗi thường gặp là viết tất cả mã trong Activity hoặc Fragment. Các lớp dựa trên giao diện người dùng này chỉ nên chứa logic xử lý các tương tác với giao diện người dùng và hệ điều hành. Bằng cách giữ các lớp này tinh gọn nhất có thể, bạn có thể tránh nhiều vấn đề liên quan đến vòng đời của thành phần và cải thiện khả năng kiểm thử của các lớp này.

Xin lưu ý rằng bạn không sở hữu cách triển khai ActivityFragment; thay vào đó, đây chỉ là các lớp keo đại diện cho hợp đồng giữa hệ điều hành Android và ứng dụng. Hệ điều hành có thể phá huỷ chúng bất kỳ lúc nào dựa trên mức độ tương tác của người dùng hoặc do các điều kiện hệ thống như thiếu bộ nhớ. Để mang lại trải nghiệm người dùng vừa ý và trải nghiệm bảo trì ứng dụng dễ quản lý hơn, tốt nhất là bạn nên giảm thiểu phần phụ thuộc vào những trải nghiệm đó.

Điều khiển giao diện người dùng qua các mô hình dữ liệu

Một nguyên tắc quan trọng khác là bạn nên điều khiển giao diện người dùng qua các mô hình dữ liệu, tốt nhất là các mô hình liên tục. Mô hình dữ liệu đại diện cho dữ liệu của một ứng dụng. Chúng độc lập với các thành phần giao diện người dùng và các thành phần khác trong ứng dụng. Có nghĩa là các tệp đó không gắn liền với vòng đời thành phần ứng dụng và giao diện người dùng, nhưng vẫn bị huỷ khi hệ điều hành quyết định xoá quy trình này của ứng dụng khỏi bộ nhớ.

Các mô hình liên tục rất phù hợp vì những lý do sau đây:

  • Người dùng sẽ không bị mất dữ liệu nếu hệ điều hành Android huỷ bỏ ứng dụng của bạn để giải phóng tài nguyên.

  • Ứng dụng tiếp tục hoạt động trong các trường hợp khi kết nối mạng không ổn định hoặc không hoạt động.

Nếu cấu trúc ứng dụng dựa trên các lớp mô hình dữ liệu, bạn sẽ giúp ứng dụng trở nên dễ kiểm thử và mạnh mẽ hơn.

Phần này trình bày cách cấu trúc ứng dụng của bạn theo các phương pháp hay nhất được đề xuất.

Hãy xem xét các nguyên tắc cấu trúc phổ biến đã đề cập trong phần trước, mỗi ứng dụng phải có ít nhất hai lớp:

  • Lớp giao diện người dùng hiển thị dữ liệu ứng dụng trên màn hình.
  • Lớp dữ liệu chứa logic nghiệp vụ của ứng dụng và hiển thị dữ liệu ứng dụng.

Bạn có thể thêm một lớp bổ sung có tên là lớp miền (domain layer) để đơn giản hoá và sử dụng lại các lượt tương tác giữa giao diện người dùng và các lớp dữ liệu.

Trong một cấu trúc ứng dụng thông thường, lớp giao diện người dùng lấy dữ liệu ứng dụng từ lớp dữ liệu hoặc từ lớp miền không bắt buộc, nằm giữa lớp giao diện người dùng và lớp dữ liệu.
Hình 1. Sơ đồ về một cấu trúc ứng dụng thông thường.

Lớp giao diện người dùng

Vai trò của lớp giao diện người dùng (hoặc lớp bản trình bày) là hiển thị dữ liệu ứng dụng trên màn hình. Bất cứ khi nào dữ liệu thay đổi, do sự tương tác của người dùng (chẳng hạn như nhấn một nút) hoặc đầu vào bên ngoài (chẳng hạn như phản hồi mạng), giao diện người dùng sẽ cập nhật để phản ánh các thay đổi đó.

Lớp giao diện người dùng gồm hai nội dung:

  • Các thành phần trên giao diện người dùng hiển thị dữ liệu trên màn hình. Bạn tạo các phần tử này bằng cách sử dụng các hàm View (Thành phần hiển thị) hoặc Jetpack Compose.
  • Các chủ thể trạng thái (chẳng hạn như các lớp ViewModel) chứa dữ liệu, hiển thị thông tin đó tới giao diện người dùng và xử lý logic.
Trong một cấu trúc thông thường, các thành phần trên giao diện người dùng của lớp giao diện người dùng phụ thuộc vào
    chủ thể trạng thái, điều này phụ thuộc vào các lớp từ lớp dữ liệu hoặc
    lớp miền không bắt buộc.
Hình 2. Vai trò của lớp giao diện người dùng trong cấu trúc ứng dụng.

Để tìm hiểu thêm về lớp này, hãy xem trang lớp giao diện người dùng.

Lớp dữ liệu

Lớp dữ liệu của ứng dụng chứa logic nghiệp vụ. Logic nghiệp vụ là yếu tố tạo ra giá trị cho ứng dụng — logic này được tạo ra từ các quy tắc xác định cách ứng dụng tạo, lưu trữ và thay đổi dữ liệu.

Lớp dữ liệu được tạo thành từ các kho lưu trữ, mỗi kho dữ liệu có thể chứa từ 0 đến nhiều nguồn dữ liệu. Bạn nên tạo một lớp kho lưu trữ cho từng loại dữ liệu khác nhau mà bạn xử lý trong ứng dụng. Ví dụ: bạn có thể tạo một lớp MoviesRepository cho dữ liệu liên quan đến phim hoặc một lớp PaymentsRepository cho dữ liệu liên quan đến các khoản thanh toán.

Trong một cấu trúc thông thường, kho lưu trữ của lớp dữ liệu cung cấp dữ liệu cho phần còn lại của ứng dụng và phụ thuộc vào các nguồn dữ liệu.
Hình 3. Vai trò của lớp dữ liệu trong cấu trúc ứng dụng.

Các lớp kho lưu trữ chịu trách nhiệm về:

  • Hiển thị dữ liệu cho phần còn lại của ứng dụng.
  • Tập trung các thay đổi vào dữ liệu.
  • Giải quyết xung đột giữa nhiều nguồn dữ liệu.
  • Tóm tắt các nguồn dữ liệu từ phần còn lại của ứng dụng.
  • Chứa logic nghiệp vụ.

Mỗi lớp nguồn dữ liệu nên có trách nhiệm làm việc với chỉ một nguồn dữ liệu duy nhất, có thể là một tệp, nguồn mạng hoặc cơ sở dữ liệu cục bộ. Các lớp nguồn dữ liệu là cầu nối giữa ứng dụng và hệ thống để thao tác dữ liệu.

Để tìm hiểu thêm về lớp này, hãy xem trang về lớp dữ liệu.

Lớp miền

Lớp miền là lớp không bắt buộc nằm giữa giao diện người dùng và các lớp dữ liệu.

Lớp miền chịu trách nhiệm về việc tổng hợp các logic nghiệp vụ phức tạp, hoặc logic nghiệp vụ đơn giản được sử dụng lại trong nhiều ViewModel. Lớp này là không bắt buộc vì không phải ứng dụng nào cũng có những yêu cầu này. Bạn chỉ nên sử dụng thuộc tính này khi cần, ví dụ: để xử lý độ phức tạp hoặc ưa chuộng khả năng tái sử dụng.

Khi được đưa vào, lớp miền không bắt buộc sẽ cung cấp các phần phụ thuộc
   cho lớp giao diện người dùng và phụ thuộc vào lớp dữ liệu.
Hình 4. Vai trò của lớp miền trong cấu trúc ứng dụng.

Các lớp này thường được gọi là trường hợp sử dụng (use case) hoặctrình tương tác (interactor). Mỗi trường hợp sử dụng phải có trách nhiệm đối với một chức năng. Ví dụ: ứng dụng có thể có một lớp GetTimeZoneUseCase nếu nhiều ViewModel dựa vào múi giờ để hiển thị thông báo thích hợp trên màn hình.

Để tìm hiểu thêm về lớp này, hãy xem trang về lớp miền.

Quản lý các phần phụ thuộc giữa các thành phần

Các lớp trong ứng dụng phụ thuộc vào các lớp khác để có thể hoạt động đúng cách. Bạn có thể sử dụng một trong các mẫu thiết kế sau để thu thập các phần phụ thuộc của một lớp cụ thể:

  • Chèn phần phụ thuộc (DI): Chèn phần phụ thuộc cho phép các lớp xác định các phần phụ thuộc mà không cần tạo chúng Trong thời gian chạy, một lớp khác chịu trách nhiệm cung cấp các phần phụ thuộc này.
  • Công cụ định vị dịch vụ: Mẫu bộ định vị dịch vụ cung cấp một sổ đăng ký mà các lớp có thể lấy phần phụ thuộc thay vì tạo các phần phụ thuộc đó.

Các mẫu này cho phép bạn mở rộng mã vì chúng cung cấp các mẫu rõ ràng để quản lý các phần phụ thuộc mà không cần sao chép mã hoặc thêm độ phức tạp. Hơn nữa, các mẫu này cho phép bạn nhanh chóng chuyển đổi giữa triển khai bản chính thức và kiểm thử.

Bạn nên làm theo các mẫu chèn phụ thuộc và sử dụng thư viện Hilt trong các ứng dụng Android. Hilt tự động tạo các đối tượng bằng cách qua cây phụ thuộc, cung cấp đảm bảo thời gian biên dịch các phần phụ thuộc và tạo vùng chứa phần phụ thuộc cho các lớp khung Android.

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

Lập trình là một trường sáng tạo và việc tạo ứng dụng Android cũng không phải là ngoại lệ. Có nhiều cách để giải quyết một vấn đề; bạn có thể giao tiếp dữ liệu giữa nhiều hoạt động hoặc mảnh, truy xuất dữ liệu từ xa và duy trì dữ liệu đó cục bộ cho chế độ ngoại tuyến hoặc xử lý bất kỳ tình huống phổ biến nào khác mà các ứng dụng không bình thường gặp phải.

Mặc dù các đề xuất sau đây là không bắt buộc, nhưng trong hầu hết trường hợp, việc làm theo các đề xuất này giúp cơ sở mã của bạn mạnh mẽ hơn, có thể kiểm thử và duy trì được trong thời gian dài:

Không lưu trữ dữ liệu trong các thành phần của ứng dụng.

Tránh chỉ định các điểm truy cập của ứng dụng – chẳng hạn như các hoạt động, dịch vụ và trình thu phát sóng – dưới dạng nguồn dữ liệu. Thay vào đó, các mã này chỉ nên phối hợp với các thành phần khác để truy xuất tập hợp con dữ liệu có liên quan đến điểm truy cập đó. Mỗi thành phần ứng dụng thường khá ngắn hạn, tuỳ thuộc vào mức độ tương tác của người dùng với thiết bị cũng như tình trạng tổng thể của hệ thống hiện tại.

Giảm phần phụ thuộc trên lớp Android.

Các thành phần ứng dụng chỉ nên là các lớp dựa vào API SDK khung Android, chẳng hạn như Context hoặc Toast. Việc tách các lớp khác trong ứng dụng khỏi các lớp đó sẽ giúp khả năng kiểm thử và giảm mối liên kết trong ứng dụng của bạn.

Xác định ranh giới trách nhiệm rõ ràng giữa các mô-đun khác nhau trong ứng dụng.

Ví dụ: đừng phân tán mã tải dữ liệu từ mạng trên nhiều lớp hoặc gói trong cơ sở mã. Tương tự như vậy, không xác định nhiều trách nhiệm không liên quan — chẳng hạn như việc lưu dữ liệu vào bộ nhớ đệm và liên kết dữ liệu — trong cùng một lớp. Việc làm theo cấu trúc ứng dụng đề xuất sẽ giúp bạn làm được điều này.

Hiển thị càng ít càng tốt từ mỗi mô-đun.

Ví dụ: không nên tạo một lối tắt để hiển thị thông tin chi tiết về hoạt động triển khai nội bộ từ một mô-đun. Bạn có thể tiết kiệm một chút thời gian trong ngắn hạn, nhưng sau đó có khả năng bạn sẽ phải chịu các khoản nợ kỹ thuật nhiều lần khi cơ sở mã của bạn phát triển.

Tập trung vào cốt lõi độc đáo của ứng dụng để ứng dụng trở nên nổi bật so với các ứng dụng khác.

Đừng mất công đổi mới bằng cách viết lại mã nguyên mẫu nhiều lần. Thay vào đó, hãy tập trung thời gian và công sức vào những điều khiến ứng dụng của bạn trở nên độc đáo, đồng thời để các thư viện Jetpack và các thư viện đề xuất khác xử lý mã nguyên mẫu lặp lại.

Cân nhắc cách tách biệt từng phần trong ứng dụng với trạng thái kiểm thử.

Ví dụ: khi có một API xác định rõ để tìm nạp dữ liệu từ mạng, bạn có thể dễ dàng kiểm tra mô-đun duy trì dữ liệu đó trong cơ sở dữ liệu trên thiết bị. Thay vào đó, nếu bạn kết hợp logic từ hai mô-đun này ở cùng một nơi hoặc phân phối mã mạng của bạn trên toàn bộ cơ sở mã, việc kiểm thử hiệu sẽ trở nên khó khăn hơn nhiều (nếu có thể).

Duy trì càng nhiều dữ liệu mới và phù hợp càng tốt.

Nhờ đó, người dùng có thể sử dụng chức năng của ứng dụng ngay cả khi thiết bị của họ đang ở chế độ ngoại tuyến. Hãy nhớ rằng không phải tất cả người dùng đều thích sự kết nối liên tục, tốc độ cao và ngay cả khi họ thích, họ vẫn có thể nhận được kết nối tệ ở những nơi đông đúc.

Mẫu

Các mẫu sau đây của Google thể hiện cấu trúc ứng dụng tốt. Hãy khám phá các ứng dụng đó để xem hướng dẫn này trong thực tế: