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

Cấu trúc ứng dụng là nền tảng của một ứng dụng Android chất lượng cao. Một cấu trúc được xác định rõ ràng cho phép bạn tạo một ứng dụng có thể mở rộng và duy trì, có thể thích ứng với hệ sinh thái ngày càng mở rộng của các thiết bị Android, bao gồm điện thoại, máy tính bảng, thiết bị có thể gập lại, thiết bị ChromeOS, màn hình ô tô và XR.

Thành phần ứng dụng

Một ứng dụng Android thông thường bao gồm nhiều thành phần ứng dụng, chẳng hạn như dịch vụ, nhà cung cấp nội dungbroadcast receiver. Bạn khai báo các thành phần này trong tệp kê khai ứng dụng.

Giao diện người dùng của một ứng dụng cũng là một thành phần. Trước đây, giao diện người dùng được tạo bằng nhiều hoạt động. Tuy nhiên, các ứng dụng hiện đại sử dụng cấu trúc hoạt động đơn. Một Activity duy nhất đóng vai trò là vùng chứa cho các màn hình được triển khai dưới dạng mảnh hoặc đích đến Jetpack Compose.

Nhiều kiểu dáng

Các ứng dụng có thể chạy trên nhiều kiểu dáng, không chỉ điện thoại mà còn cả máy tính bảng, thiết bị có thể gập lại, thiết bị ChromeOS và nhiều thiết bị khác. Ứng dụng không thể giả định hướng dọc hoặc hướng ngang. Các thay đổi về cấu hình, chẳng hạn như xoay thiết bị hoặc gập và mở thiết bị có thể gập lại, buộc ứng dụng của bạn phải kết hợp lại giao diện người dùng, điều này ảnh hưởng đến dữ liệu và trạng thái của ứng dụng.

Các quy tắc ràng buộc về tài nguyên

Thiết bị di động (ngay cả thiết bị màn hình lớn) đều 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ể dừng một số quy trình ứng dụng để tạo không gian cho các quy trình mới.

Điều kiện ra mắt linh hoạt

Trong môi trường bị hạn chế về tài nguyên, các thành phần của ứng dụng có thể được chạy riêng lẻ và không theo thứ tự; hơn nữa, 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. Do đó, đừng lưu trữ 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. Các thành phần ứng dụng của bạn phải độc lập, không phụ thuộc vào nhau.

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

Nếu không thể 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ô. Một cấu trúc ứng dụng được thiết kế hợp lý sẽ 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ó.

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

Thiết kế cấu trúc ứng dụng theo một vài nguyên tắc cụ thể.

Nguyên tắc quan trọng nhất 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.

Vai trò chính của Activity hoặc Fragment là lưu trữ giao diện người dùng của ứng dụng. Hệ điều hành Android kiểm soát vòng đời của các thành phần này, thường xuyên huỷ và tạo lại chúng để phản hồi các thao tác của người dùng như xoay màn hình hoặc các sự kiện hệ thống như thiếu bộ nhớ.

Bản chất tạm thời này khiến chúng không phù hợp để lưu trữ dữ liệu hoặc trạng thái ứng dụng. Nếu bạn lưu trữ dữ liệu trong một Activity hoặc Fragment, thì dữ liệu đó sẽ bị mất khi thành phần được tạo lại. Để đảm bảo tính liên tục của dữ liệu và mang lại trải nghiệm ổn định cho người dùng, đừng giao phó trạng thái cho các thành phần giao diện người dùng này.

Bố cục thích ứng (Adaptive Layouts)

Ứng dụng của bạn phải xử lý các thay đổi về cấu hình một cách linh hoạt, chẳng hạn như thay đổi hướng thiết bị hoặc thay đổi kích thước cửa sổ ứng dụng. Triển khai bố cục chuẩn thích ứng để mang lại trải nghiệm tối ưu cho người dùng trên nhiều hệ số hình dạng.

Đ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 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 của bạn tiếp tục hoạt động trong các trường hợp khi kết nối mạng bị gián đoạn hoặc không hoạt động.

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

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 loại dữ liệu đó. 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; để 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 nhiều lợi ích:

  • 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
  • Bảo vệ dữ liệu để các loại khác không thể can thiệp vào
  • 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

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.

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

Nguyên tắc về nguồn đáng tin cậy duy nhất thường được dùng 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, thường là từ thành phần mẹ đến thành phần con. 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 duy trì 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.

Theo các nguyên tắc cấu trúc phổ biến, mỗi ứng dụng phải có ít nhất 2 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 của ứng dụng Android hiện đại sử dụng các kỹ thuật sau (cùng với các kỹ thuật khác):

  • Kiến trúc thích ứ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
  • 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
  • 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 bài viết Nội dung đề xuất về cấu trúc Android.

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 bao gồm 2 loại cấu trúc:

  • 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 Jetpack Compose để hỗ trợ bố cục thích ứng.
  • Các phần tử giữ trạng thái (chẳng hạn như ViewModel) chứa dữ liệu, hiển thị dữ liệu đó cho giao diện người dùng và xử lý logic
Trong một cấu trúc thông thường, các phần tử 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 phần tử giữ trạng thái. Phần tử này phụ thuộc vào các lớp (class) 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.

Đối với giao diện người dùng thích ứng, các phần tử giữ trạng thái như đối tượng ViewModel sẽ hiển thị trạng thái giao diện người dùng thích ứng với nhiều lớp kích thước cửa sổ. Bạn có thể dùng currentWindowAdaptiveInfo() để lấy trạng thái giao diện người dùng này. Sau đó, các thành phần như NavigationSuiteScaffold có thể sử dụng thông tin này để tự động chuyển đổi giữa các mẫu điều hướng khác nhau (ví dụ: NavigationBar, NavigationRail hoặc NavigationDrawer) dựa trên không gian màn hình có sẵn.

Để tìm hiểu thêm, hãy xem trang về 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 bao gồm 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 lưu trữ 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, hãy xem trang về lớp dữ liệu.

Lớp miền

Lớp miền là một 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 đóng gói logic nghiệp vụ phức tạp hoặc logic nghiệp vụ đơn giản hơn được nhiều mô hình hiển thị dùng lại. Lớp miền 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. Chỉ 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 trong lớp miền thường được gọi là trường hợp sử dụng hoặc trình tương tác. Mỗi trường hợp sử dụng phải chịu trách nhiệm về một chức năng duy nhất. Ví dụ: ứng dụng của bạn có thể có một lớp GetTimeZoneUseCase nếu nhiều mô hình hiển thị 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, 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 công cụ đị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. Các mẫu này cũng cho phép bạn nhanh chóng chuyển đổi giữa việc triển khai bản chính thức và kiểm thử.

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.

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. Các điểm truy cập 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 đều có thời gian tồn tại ngắn, tuỳ thuộc vào mức độ tương tác của người dùng với thiết bị và dung lượng của hệ thống.

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 thành phần ứng dụng 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 trong ứng dụng.

Đừ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 được đề xuất sẽ giúp bạn.

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

Không tạo lối tắt để hiển thị thông tin chi tiết về hoạt động triển khai nội bộ. 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. Hãy để 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.

Sử dụng bố cục chính tắc và các mẫu thiết kế ứng dụng.

Các thư viện Jetpack Compose cung cấp các API mạnh mẽ để tạo giao diện người dùng thích ứng. Sử dụng bố cục chuẩn trong ứng dụng để tối ưu hoá trải nghiệm người dùng trên nhiều kiểu dáng và kích thước màn hình. Xem thư viện về các mẫu thiết kế ứng dụng để chọn bố cục phù hợp nhất với trường hợp sử dụng của bạn.

Duy trì trạng thái giao diện người dùng khi cấu hình thay đổi.

Khi thiết kế bố cục thích ứng, hãy duy trì trạng thái giao diện người dùng trong các thay đổi về cấu hình, chẳng hạn như thay đổi kích thước màn hình, gập và thay đổi hướng. Cấu trúc của bạn phải xác minh rằng trạng thái hiện tại của người dùng được duy trì, mang lại trải nghiệm liền mạch.

Thiết kế các thành phần giao diện người dùng có thể tái sử dụng và kết hợp.

Tạo các thành phần giao diện người dùng có thể sử dụng lại và kết hợp để hỗ trợ thiết kế thích ứng. Điều này cho phép bạn kết hợp và sắp xếp lại các thành phần cho phù hợp với nhiều kích thước màn hình và tư thế mà không cần tái cấu trúc đáng kể.

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ử.

Một API xác định rõ để tìm nạp dữ liệu từ mạng giúp bạn dễ dàng kiểm thử 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 hàm 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ử 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ì loại dữ liệu đó 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 này 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ớ rằng 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ã.
  • Giúp ích cho việc tham gia. 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.
  • Dễ dàng kiểm thử 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 tổ chức 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 minh hoạ cấu trúc ứng dụng tốt: