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:

  • 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.
  • Bạn nên tạo các kho lưu trữ ngay cả khi chỉ cần chứa một nguồn dữ liệu duy nhất.
  • 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. Lớp giao diện người dùng 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.
  • 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.
Xem thêm các phương pháp hay nhất về lớp giao diện người dùng tại đây.
Lớp dữ liệu sẽ hiển thị dữ liệu ứng dụng bằng cách dùng kho lưu trữ.

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ạt động hoặc ViewModel không được 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.

Xem các phương pháp hay nhất về coroutine tại đây.

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 kinh doanh, 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 (Khung hiển thị Compose hoặc Android).

Xem thêm các phương pháp hay nhất về ViewModel tại đây.

Xem các lợi ích của ViewModel tại đây.

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: repeatOnLifecycle trong hệ thống View (Khung hiển thị) và collectAsStateWithLifecycle trong Jetpack Compose.

Đọc thêm về repeatOnLifecycle.

Đọ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. Tìm hiểu thêm về sự kiện giao diện người dùng tại đây.
Dùng ứng dụng hoạt động đơn. Dùng Mảnh điều hướng hoặc thành phần Điều hướng trong Compose để di chuyển giữa các màn hình và đường 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:

Khung hiển thị

class MyFragment : Fragment() {

    private val viewModel: MyViewModel by viewModel()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        viewLifecycleOwner.lifecycleScope.launch {
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState.collect {
                    // Process item
                }
            }
        }
    }
}

Compose

@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ả
ViewModel phải độc lập với vòng đời của Android. ViewModel không được chứa 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, Fragment, Context hoặc Resources làm phần phụ thuộc. Nếu cần Context trong ViewModel, bạn nên đánh giá 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. Bằng cách 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 nên 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. ViewModel phải 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.
  • Bạn nên đặt uiState làm StateFlow.
  • Bạn nên tạo uiState bằng toán tử stateIn dựa vào chính sách WhileSubscribed(5000) (ví dụ) 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.
  • Đố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 (ví dụ).
  • 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

Sau đây là một số phương pháp hay nhất để xử lý vòng đời của Android:

Nội dung đề xuất Nội dung mô tả
Không ghi đè các phương thức vòng đời trong Activity (Hoạt động) hoặc Fragment (Mảnh). Không ghi đè các phương thức vòng đời, chẳng hạn như onResume trong Activity (Hoạt động) hoặc Fragment (Mảnh). Thay vào đó, hãy sử dụng LifecycleObserver. Nếu ứng dụng cần thực hiện công việc khi vòng đời đạt đến một Lifecycle.State nhất định, hãy sử dụng API repeatOnLifecycle.

Đ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:

Khung hiển thị

class MyFragment: Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        viewLifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {
            override fun onResume(owner: LifecycleOwner) {
                // ...
            }
            override fun onPause(owner: LifecycleOwner) {
                // ...
            }
        }
    }
}

Compose

@Composable
fun MyApp() {

    val lifecycleOwner = LocalLifecycleOwner.current
    DisposableEffect(lifecycleOwner, ...) {
        val lifecycleObserver = object : DefaultLifecycleObserver {
            override fun onStop(owner: LifecycleOwner) {
                // ...
            }
        }

        lifecycleOwner.lifecycle.addObserver(lifecycleObserver)
        onDispose {
            lifecycleOwner.lifecycle.removeObserver(lifecycleObserver)
        }
    }
}

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

Bạn nên áp dụng một số 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. Ví dụ: nếu bạn:
  • Dùng nhiều màn hình với ViewModel – tích hợp
  • Dùng WorkManager – tích hợp
  • Dùng sớm tính năng Điều hướng, chẳng hạn như ViewModel thuộc phạm vi biểu đồ điều hướng — tích hợp.

Kiểm thử

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, ít nhất, bạn nên kiểm thử ứng dụng bằng:

  • 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). Vui lòng đọc thêm trong tài liệu Dùng kỹ thuật nhân đôi kiểm thử (test double) trong Android.
Kiểm thử StateFlow. Khi kiểm thử StateFlow:

Để biết thêm thông tin, hãy xem hướng dẫn Nội dung cần kiểm thử trong Android DAC.

Mô hình

Bạn nên á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 mình:

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.
Optional (Không bắt buộc)
Phương thức phải là một cụm động từ. Ví dụ: makePayment().
Đặt tên cho thuộc tính.
Optional (Không bắt buộc)
Thuộc tính phải là một cụm danh từ. Ví dụ: inProgressTopicSelection.
Đặt tên cho luồng dữ liệu.
Optional (Không bắt buộc)
Khi một lớp hiển thị luồng Quy trình, LiveData 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, tên mô hình phải ở dạng số nhiều: getAuthorsStream(): Flow<List<Author>>
Đặt tên cho việc triển khai giao diện.
Optional (Không bắt buộc)
Tên cho việc triển khai giao diện phải có ý nghĩa. Đặ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. Việc triển khai giả phải có tiền tố là Fake, như trong FakeAuthorsRepository.