Nội dung đề xuất về cấu trúc Android

Trang này trình bày một số nội dung đề xuất và phương pháp hay nhất về Cấu trúc. Hãy áp dụng các nội dung đề xuất đó để cải thiện chất lượng, độ mạnh và khả năng mở rộng của ứng dụng. Các nội dung đề xuất này cũng giúp bạn bảo trì và kiểm thử ứng dụng dễ dàng hơn.

Các phương pháp hay nhất dưới đây được nhóm theo chủ đề. Mỗi nhóm sẽ có một mức ưu tiên phản ánh mức độ đề xuất riêng. Dưới đây là danh sách mức ưu tiên:

  • Strongly recommended (Rất nên dùng): Bạn nên triển khai phương pháp này, trừ phi phương pháp đó xung đột với cách làm của bạn.
  • Recommended (Nên dùng): Phương pháp này có thể giúp cải thiện ứng dụng của bạn.
  • Optional (Không bắt buộc): Phương pháp này có thể cải thiện ứng dụng của bạn trong một số trường hợp nhất định.

Cấu trúc phân lớp

Cấu trúc phân lớp được đề xuất của chúng tôi ưu tiên tách biệt các mối quan ngại. Cấu trúc này điều khiển giao diện người dùng qua mô hình dữ liệu, tuân thủ nguyên tắc một nguồn đáng tin cậy và tuân theo nguyên tắc luồng dữ liệu một chiều. Dưới đây là một số phương pháp hay nhất về cấu trúc phân lớp:

Nội dung đề xuất Nội dung mô tả
Dùng lớp dữ liệu được xác định rõ ràng. Lớp dữ liệu hiển thị dữ liệu ứng dụng với phần còn lại của ứng dụng và chứa phần lớn logic nghiệp vụ của ứng dụng.
  • Tạo kho lưu trữ ngay cả khi chỉ chứa một nguồn dữ liệu.
  • Trong ứng dụng nhỏ, bạn có thể chọn đặt các loại lớp dữ liệu vào gói hoặc mô-đun data.
Dùng lớp giao diện người dùng được xác định rõ ràng. Vai trò của lớp giao diện người dùng UI layer là hiển thị dữ liệu ứng dụng trên màn hình và đóng vai trò là điểm chính trong quá trình tương tác của người dùng. Jetpack Compose là một bộ công cụ hiện đại, được khuyên dùng để xây dựng giao diện người dùng của ứng dụng.
  • Trong ứng dụng nhỏ, bạn có thể chọn đặt các loại lớp dữ liệu vào gói hoặc mô-đun ui.
Để biết thêm thông tin về các phương pháp hay nhất cho lớp giao diện người dùng, hãy xem phần Lớp giao diện người dùng.
Hiển thị dữ liệu ứng dụng từ lớp dữ liệu bằng cách dùng kho lưu trữ.

Đảm bảo các thành phần trong lớp giao diện người dùng, chẳng hạn như thành phần kết hợp hoặc ViewModel không tương tác trực tiếp với nguồn dữ liệu. Ví dụ về nguồn dữ liệu:

  • API Databases, API DataStore, API SharedPreferences, API Firebase.
  • Nhà cung cấp dịch vụ vị trí GPS.
  • Nhà cung cấp dữ liệu Bluetooth.
  • Nhà cung cấp trạng thái kết nối mạng.
Dùng coroutine và luồng. Dùng coroutine và luồng để giao tiếp giữa các lớp.

Để biết thêm thông tin về các phương pháp hay nhất cho coroutine, hãy xem bài viết Các phương pháp hay nhất cho coroutine trong Android.

Dùng lớp miền. Dùng lớp miền, các trường hợp sử dụng nếu bạn cần dùng lại logic nghiệp vụ để tương tác với lớp dữ liệu trên nhiều ViewModel hoặc bạn muốn đơn giản hoá logic nghiệp vụ của một ViewModel cụ thể

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

Vai trò của lớp giao diện người dùng là hiển thị dữ liệu ứng dụng trên màn hình và đóng vai trò là điểm chính trong quá trình tương tác của người dùng. Dưới đây là một số phương pháp hay nhất cho lớp giao diện người dùng:

Nội dung đề xuất Nội dung mô tả
Tuân theo nguyên tắc Luồng dữ liệu một chiều (UDF). Tuân theo nguyên tắc Luồng dữ liệu một chiều (UDF), trong đó ViewModel hiển thị trạng thái giao diện người dùng thông qua mẫu trình quan sát và nhận các thao tác từ giao diện người dùng thông qua lệnh gọi phương thức.
Dùng ViewModel AAC nếu có lợi cho ứng dụng của bạn. Dùng AAC ViewModel để xử lý logic nghiệp vụ, cũng như tìm nạp dữ liệu ứng dụng để hiển thị trạng thái giao diện người dùng cho giao diện người dùng.

Để biết thêm thông tin về các phương pháp hay nhất cho ViewModel, hãy xem bài viết Nội dung đề xuất về cấu trúc.

Để biết thêm thông tin về lợi ích của ViewModel, hãy xem bài viết ViewModel với tư cách là phần tử giữ trạng thái logic nghiệp vụ.

Dùng bộ sưu tập trạng thái giao diện người dùng có nhận biết vòng đời. Thu thập trạng thái giao diện người dùng từ giao diện người dùng bằng trình tạo coroutine có nhận biết vòng đời thích hợp, collectAsStateWithLifecycle.

Đọc thêm về collectAsStateWithLifecycle.

Không gửi các sự kiện từ ViewModel đến giao diện người dùng. Xử lý sự kiện ngay lập tức trong ViewModel và cập nhật trạng thái bằng kết quả xử lý sự kiện. Để biết thêm thông tin về các sự kiện trên giao diện người dùng, hãy xem bài viết Xử lý sự kiện ViewModel.
Dùng ứng dụng hoạt động đơn. Dùng Navigation 3 để di chuyển giữa các màn hình và liên kết sâu đến ứng dụng của bạn nếu ứng dụng có nhiều màn hình.
Dùng Jetpack Compose. Dùng Jetpack Compose để tạo ứng dụng mới cho điện thoại, máy tính bảng, thiết bị có thể gập lại và Wear OS.

Đoạn mã sau đây chỉ ra cách thu thập trạng thái giao diện người dùng theo cách có nhận biết vòng đời:

  @Composable
  fun MyScreen(
      viewModel: MyViewModel = viewModel()
  ) {
      val uiState by viewModel.uiState.collectAsStateWithLifecycle()
  }

ViewModel

ViewModel chịu trách nhiệm cung cấp trạng thái giao diện người dùng và quyền truy cập vào lớp dữ liệu. Dưới đây là một số phương pháp hay nhất về ViewModel:

Nội dung đề xuất Nội dung mô tả
Giữ cho ViewModel độc lập với vòng đời của Android. Trong ViewModel, không giữ tệp tham chiếu đến bất kỳ kiểu nào liên quan đến vòng đời. Không truyền Activity, Context hoặc Resources làm phần phụ thuộc. Nếu cần Context trong ViewModel, hãy đánh giá cẩn thận xem liệu ViewModel có thuộc đúng lớp (layer) hay không.
Dùng coroutine và luồng.

ViewModel tương tác với các lớp dữ liệu hoặc miền thông qua:

  • Các luồng Kotlin để nhận dữ liệu ứng dụng
  • Các hàm suspend để thực hiện các thao tác bằng viewModelScope
Dùng ViewModel ở cấp màn hình.

Không sử dụng ViewModel trong các phần giao diện người dùng có thể tái sử dụng. Bạn nên sử dụng ViewModel trong:

  • Thành phần kết hợp cấp màn hình,
  • Activities/Fragments (Hoạt động/Mảnh) trong Khung hiển thị,
  • Đích đến hoặc biểu đồ khi sử dụng Jetpack Navigation.
Dùng các lớp của phần tử giữ trạng thái thuần tuý trong các thành phần giao diện người dùng có thể tái sử dụng. Dùng các lớp của phần tử giữ trạng thái thuần tuý để xử lý độ phức tạp của các thành phần trên giao diện người dùng có thể tái sử dụng. Khi bạn thực hiện việc này, trạng thái có thể được chuyển lên trên và kiểm soát bên ngoài.
Không sử dụng AndroidViewModel. Dùng lớp ViewModel, chứ không phải AndroidViewModel. Không dùng lớp Application trong ViewModel. Thay vào đó, hãy di chuyển phần phụ thuộc sang giao diện người dùng hoặc lớp dữ liệu.
Hiển thị trạng thái giao diện người dùng. Hãy làm cho ViewModel hiển thị dữ liệu cho giao diện người dùng thông qua một thuộc tính có tên là uiState. Nếu giao diện người dùng hiển thị nhiều phần dữ liệu không liên quan, máy ảo có thể hiển thị nhiều thuộc tính trạng thái giao diện người dùng.
  • Đặt uiState thành StateFlow.
  • Tạo uiState bằng toán tử stateIn dựa vào chính sách WhileSubscribed(5000) nếu có dữ liệu dưới dạng luồng dữ liệu từ các lớp khác trong hệ phân cấp. (Xem ví dụ về mã này.)
  • Đối với các trường hợp đơn giản hơn mà không có luồng dữ liệu nào đến từ lớp dữ liệu, bạn có thể sử dụng MutableStateFlow hiển thị dưới dạng StateFlow không thể thay đổi.
  • Bạn có thể chọn dùng ${Screen}UiState làm lớp dữ liệu có thể chứa dữ liệu, lỗi và tín hiệu tải. Lớp này cũng có thể là một lớp kín nếu các trạng thái khác nhau là loại trừ lẫn nhau.

Đoạn mã sau đây trình bày cách hiển thị trạng thái giao diện người dùng từ ViewModel:

@HiltViewModel
class BookmarksViewModel @Inject constructor(
    newsRepository: NewsRepository
) : ViewModel() {

    val feedState: StateFlow<NewsFeedUiState> =
        newsRepository
            .getNewsResourcesStream()
            .mapToFeedState(savedNewsResourcesState)
            .stateIn(
                scope = viewModelScope,
                started = SharingStarted.WhileSubscribed(5_000),
                initialValue = NewsFeedUiState.Loading
            )

    // ...
}

Vòng đời

Làm theo các phương pháp hay nhất để xử lý vòng đời của Activity lifecycle:

Nội dung đề xuất Nội dung mô tả
Sử dụng các hiệu ứng nhận biết vòng đời trong thành phần kết hợp thay vì ghi đè các phương thức gọi lại trong vòng đời của Activity.

Không ghi đè các phương thức vòng đời của Activity, chẳng hạn như onResume, để chạy các tác vụ liên quan đến giao diện người dùng. Thay vào đó, hãy sử dụng LifecycleEffects của Compose hoặc phạm vi coroutine nhận biết vòng đời:

  • Sử dụng LifecycleStartEffect để thực hiện công việc đồng bộ khi hoạt động của bạn bắt đầu và dừng.
  • Sử dụng LifecycleResumeEffect để thực hiện công việc đồng bộ khi hoạt động của bạn tiếp tục và tạm dừng.
  • Sử dụng repeatOnLifecycle để thực hiện công việc không đồng bộ nhằm phản hồi các sự kiện trong vòng đời.
  • Thu thập dữ liệu không đồng bộ từ Luồng bằng collectAsStateWithLifecycle.

Đoạn mã sau đây trình bày cách thực hiện các thao tác dựa trên một trạng thái Vòng đời nhất định:

  @Composable
  fun LocationChangedEffect(
    locationManager: LocationManager,
    onLocationChanged: (Location) -> Unit
  ) {
    val currentOnLocationChanged by rememberUpdatedState(onLocationChanged)

    LifecycleStartEffect(locationManager) {
        val listener = LocationListener { newLocation ->
            currentOnLocationChanged(newLocation)
        }

        try {
            locationManager.requestLocationUpdates(
                LocationManager.GPS_PROVIDER,
                1000L,
                1f,
                listener,
            )
        } catch (e: SecurityException) {
            // TODO: Handle missing permissions
        }

        onStopOrDispose {
            locationManager.removeUpdates(listener)
        }
    }
  }

Xử lý các phần phụ thuộc

Làm theo các phương pháp hay nhất khi quản lý các phần phụ thuộc giữa các thành phần:

Nội dung đề xuất Nội dung mô tả
Dùng tính năng chèn phần phụ thuộc. Áp dụng các phương pháp hay nhất về kỹ thuật chèn phần phụ thuộc, chủ yếu là chèn hàm khởi tạo khi có thể.
Xác định phạm vi ở một thành phần khi cần. Xác định phạm vi ở một vùng chứa phần phụ thuộc khi loại đó chứa dữ liệu có thể thay đổi cần được chia sẻ hoặc loại cần khởi chạy gây tốn kém và được sử dụng rộng rãi trong ứng dụng.
Dùng Hilt. Dùng Hilt hoặc kỹ thuật chèn phần phụ thuộc theo cách thủ công trong các ứng dụng đơn giản. Dùng Hilt nếu dự án của bạn khá phức tạp, chẳng hạn như nếu dự án đó có bất kỳ nội dung nào sau đây:
  • Nhiều màn hình với ViewModel
  • Sử dụng WorkManager
  • Có ViewModel thuộc phạm vi của ngăn xếp lui điều hướng

Thử nghiệm

Sau đây là một số phương pháp hay nhất để kiểm thử:

Nội dung đề xuất Nội dung mô tả
Nắm rõ nội dung cần kiểm thử.

Trừ phi dự án này đơn giản như ứng dụng "hello world", hãy kiểm thử ứng dụng. Cung cấp ít nhất:

  • Kiểm thử đơn vị đối với ViewModel, bao gồm cả Luồng
  • Kiểm thử đơn vị đối với thực thể lớp dữ liệu, tức là kho lưu trữ và nguồn dữ liệu
  • Các phép kiểm thử điều hướng trên giao diện người dùng rất hữu ích khi kiểm thử hồi quy trong CI
Ưu tiên loại kiểm thử fake hoặc mock (giả hoặc mô phỏng). Để biết thêm thông tin về cách sử dụng loại kiểm thử fake, hãy xem bài viết Dùng đối tượng kiểm thử trong Android.
Kiểm thử StateFlow. Khi kiểm thử StateFlow, hãy làm như sau:

Để biết thêm thông tin, hãy xem bài viết Nội dung cần kiểm thử trong AndroidKiểm thử bố cục Compose.

Mô hình

Áp dụng các phương pháp hay nhất sau đây khi phát triển mô hình trong ứng dụng của bạn:

Nội dung đề xuất Nội dung mô tả
Tạo một mô hình trên mỗi lớp trong các ứng dụng phức tạp.

Trong các ứng dụng phức tạp, hãy tạo mô hình mới ở các lớp hoặc thành phần khác nhau khi thích hợp. Hãy xem các ví dụ sau đây:

  • Một nguồn dữ liệu từ xa có thể ánh xạ mô hình nhận được thông qua mạng đến một lớp đơn giản hơn chỉ bằng dữ liệu mà ứng dụng cần.
  • Các kho lưu trữ có thể ánh xạ mô hình DAO đến các lớp dữ liệu đơn giản hơn chỉ bằng thông tin mà lớp giao diện người dùng cần.
  • ViewModel có thể bao gồm các mô hình lớp dữ liệu trong lớp UiState.

Quy ước đặt tên

Khi đặt tên cho bộ mã, bạn nên nắm được các phương pháp hay nhất sau đây:

Nội dung đề xuất Nội dung mô tả
Đặt tên cho phương thức.
Không bắt buộc
Sử dụng cụm động từ để đặt tên cho phương thức, ví dụ: makePayment().
Đặt tên cho thuộc tính.
Không bắt buộc
Sử dụng cụm danh từ để đặt tên cho thuộc tính, ví dụ: inProgressTopicSelection.
Đặt tên cho luồng dữ liệu.
Không bắt buộc
Khi một lớp hiển thị luồng Quy trình hoặc bất kỳ luồng nào khác, quy ước đặt tên là get{model}Stream. Ví dụ: getAuthorStream(): Flow<Author>. Nếu hàm trả về danh sách mô hình, hãy sử dụng tên mô hình số nhiều: getAuthorsStream(): Flow<List<Author>>.
Đặt tên cho việc triển khai giao diện.
Không bắt buộc
Sử dụng tên có ý nghĩa cho việc triển khai giao diện. Đặt Default làm tiền tố nếu không tìm thấy tên phù hợp hơn. Ví dụ: đối với giao diện NewsRepository, bạn có thể dùng tên OfflineFirstNewsRepository hoặc InMemoryNewsRepository. Nếu bạn không tìm thấy tên phù hợp, hãy dùng tên DefaultNewsRepository. Tiền tố của việc triển khai giả phải là Fake, như trong FakeAuthorsRepository.

Tài nguyên khác

Để biết thêm thông tin về cấu trúc Android, hãy xem các tài nguyên khác sau đây:

Tài liệu

Nội dung khung hiển thị