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

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.

Một nguồn dữ liệu đáng tin cậy duy nhất

Khi xác định một loại dữ liệu mới trong ứng dụng, bạn nên chỉ định một Nguồn chính xác duy nhất (SSOT) cho nó. SSOT là chủ sở hữu của dữ liệu đó, và chỉ SSOT mới có thể sửa đổi hoặc thay đổi dữ liệu đó. Để đạt được điều này, SSOT hiển thị dữ liệu bằng cách sử dụng một kiểu bất biến, và để có thể sửa đổi dữ liệu, SSOT sẽ hiển thị các hàm hoặc nhận các sự kiện mà loại khác có thể gọi.

Mẫu này mang lại khá nhiều lợi ích:

  • Nó tập trung tất cả các thay đổi đối với một loại dữ liệu cụ thể ở cùng một nơi.
  • Nó cũng bảo vệ dữ liệu để các loại khác không thể can thiệp vào.
  • Nó giúp cho các thay đổi đối với dữ liệu dễ dàng theo dõi hơn. Từ đó dễ phát hiện lỗi hơn.

Trong một ứng dụng ưu tiên ngoại tuyến, nguồn đáng tin cậy cho dữ liệu ứng dụng thường là cơ sở dữ liệu. Trong một số trường hợp khác, nguồn đáng tin cậy có thể là ViewModel, hoặc thậm chí là giao diện người dùng.

Luồng dữ liệu một chiều

Nguyên tắc về nguồn đáng tin duy nhất thường dùng trong hướng dẫn của chúng tôi với mẫu Luồng dữ liệu một chiều (UDF). Trong UDF, trạng thái chỉ chạy theo một hướng. Các sự kiện sửa đổi luồng dữ liệu theo hướng ngược lại.

Trong Android, trạng thái hoặc dữ liệu thường chuyển từ các kiểu hệ phân cấp có phạm vi cao hơn sang phạm vi thấp hơn. Các sự kiện thường được kích hoạt từ loại có phạm vi thấp hơn cho đến khi chúng đạt đến SSOT cho loại dữ liệu tương ứng. Chẳng hạn như dữ liệu ứng dụng thường truyền từ nguồn dữ liệu tới giao diện người dùng. Các sự kiện người dùng, chẳng hạn như các lượt nhấn nút chuyển từ giao diện người dùng đến SSOT, nơi dữ liệu ứng dụng được sửa đổi và hiển thị theo kiểu bất biến.

Mẫu này đảm bảo tính nhất quán của dữ liệu tốt hơn, ít xảy ra lỗi hơn, dễ dàng hơn khi gỡ lỗi và mang lại mọi lợi ích của mẫu SSOT.

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.

Cấu trúc ứng dụng hiện đại

Cấu trúc ứng dụng hiện đại này khuyến khích sử dụng các kỹ thuật sau đây, cùng với các kỹ thuật khác:

  • Cấu trúc phản ứng và phân lớp.
  • Luồng dữ liệu một chiều (UDF) trong tất cả các lớp của ứng dụng.
  • Một lớp giao diện người dùng có các phần tử giữ trạng thái để quản lý sự phức tạp của giao diện người dùng.
  • Coroutine và luồng (flow).
  • Các phương pháp hay nhất để chèn phần phụ thuộc.

Để biết thêm thông tin, hãy xem các phần sau đây, các trang khác về Cấu trúc trong mục lục và trang đề xuất chứa thông tin tóm tắt về những phương pháp hay nhất quan trọng nhất.

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 thông qua cây phần phụ thuộc, đưa ra sự đảm bảo về 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 thuộc khung Android.

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

Lập trình là một lĩnh vực sáng tạo và việc xây dựng ứ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 sẽ giúp cơ sở mã của bạn mạnh mẽ hơn, kiểm thử được và duy trì được trong dài hạn:

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ừ 2 mô-đun này ở cùng một nơi hoặc phân phối mã kết nối mạng trên toàn bộ cơ sở mã, việc kiểm thử một cách hiệu quả sẽ trở nên khó khăn hơn nhiều — nếu không muốn nói là không thể.

Các loại dữ liệu chịu trách nhiệm về chính sách đồng thời chúng.

Nếu một loại dữ liệu đang thực hiện công việc chặn lâu dài, thì nó phải chịu trách nhiệm di chuyển phần tính toán đó sang luồng phù hợp. Loại cụ thể đó biết kiểu tính toán mà nó đang thực hiện và được thực thi trong luồng nào. Các loại phải an toàn cho luồng chính (main-safe), nghĩa là an toàn khi gọi từ luồng chính mà không bị chặn.

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ớ là không phải tất cả người dùng đều muốn kết nối diễn ra liên tục với tốc độ cao, và ngay cả khi muốn, họ vẫn có thể nhận được kết nối kém ở những nơi đông đúc.

Lợi ích của Cấu trúc

Việc triển khai một Cấu trúc tốt trong ứng dụng mang lại rất nhiều lợi ích cho nhóm dự án và nhóm kỹ thuật:

  • Cải thiện khả năng bảo trì, chất lượng và tính mạnh mẽ của ứng dụng nói chung.
  • Cho phép ứng dụng mở rộng quy mô. Nhiều người và nhiều nhóm hơn có thể đóng góp cho cùng một cơ sở mã với ít xung đột mã.
  • Điều này hữu ích cho việc tích hợp. Khi Cấu trúc mang lại sự nhất quán cho dự án của bạn, các thành viên mới trong nhóm có thể nhanh chóng nắm bắt thông tin và hiệu quả hơn trong khoảng thời gian ngắn hơn.
  • Việc thử nghiệm sẽ dễ dàng hơn. Một cấu trúc tốt khuyến khích các kiểu đơn giản, thường dễ kiểm thử hơn.
  • Bạn có thể điều tra lỗi một cách có phương pháp bằng các quy trình được xác định rõ.

Ngoài ra, việc đầu tư vào Cấu trúc cũng tác động trực tiếp đến người dùng. Người dùng được hưởng lợi từ một ứng dụng ổn định hơn và nhiều tính năng hơn do nhóm kỹ thuật làm việc hiệu quả hơn. Tuy nhiên, việc sử dụng Cấu trúc cũng đòi hỏi bạn phải đầu tư trước. Để giúp bạn diễn giải điều này cho những người khác trong công ty hiểu, hãy xem các nghiên cứu điển hình dưới đây qua chia sẻ của các công ty khác về câu chuyện thành công của họ khi sở hữu một cấu trúc ứng dụng tối ưu.

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 mẫu đó để xem hướng dẫn này trong thực tế: