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

Vai trò của giao diện người dùng (UI) là hiển thị dữ liệu ứng dụng lên màn hình và là điểm chính trong quá trình tương tác của người dùng. Bất cứ khi nào dữ liệu thay đổi – do sự tương tác của người dùng (như nhấn vào một nút) hoặc nhận dữ liệu bên ngoài (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 đó. Vậy, tóm lại là, giao diện người dùng là sự diễn tả trực quan của trạng thái ứng dụng khi truy xuất từ lớp dữ liệu.

Tuy nhiên, dữ liệu ứng dụng bạn nhận được từ lớp dữ liệu thường ở một định dạng khác với thông tin bạn cần hiển thị. Ví dụ: bạn có thể chỉ cần một phần dữ liệu cho giao diện người dùng hoặc bạn có thể cần hợp nhất 2 nguồn dữ liệu khác nhau để trình bày thông tin liên quan cho người dùng. Bất kể logic bạn áp dụng là gì, bạn đều cần chuyển cho giao diện người dùng tất cả các thông tin cần thiết để giao diện kết xuất đầy đủ. Lớp giao diện người dùng là quy trình chuyển đổi các thay đổi dữ liệu ứng dụng thành dạng thức mà giao diện người dùng có thể trình bày và sau đó hiển thị.

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 1. Vai trò của lớp giao diện người dùng trong cấu trúc ứng dụng.

Nghiên cứu điển hình cơ bản

Giả sử có 1 ứng dụng tìm nạp các bài viết tin tức để người dùng đọc. Ứng dụng có màn hình bài viết trình bày các bài viết có sẵn để đọc, đồng thời cũng cho phép người dùng đã đăng nhập đánh dấu các bài viết thực sự nổi bật. Do có rất nhiều bài viết tại mọi thời điểm trong ngày, nên người đọc cần tính năng duyệt qua bài viết theo danh mục. Tóm lại, ứng dụng cho phép người dùng thực hiện các hành động sau:

  • Xem các bài viết có sẵn để đọc.
  • Duyệt qua các bài viết theo danh mục.
  • Đăng nhập và đánh dấu các bài viết nhất định.
  • Truy cập một số tính năng nâng cao nếu đủ điều kiện.
Hình 2. Một ứng dụng tin tức mẫu cho nghiên cứu điển hình về giao diện người dùng.

Các phần sau đây sử dụng ví dụ này làm nghiên cứu điển hình để giới thiệu các nguyên tắc về luồng dữ liệu một chiều, cũng như minh hoạ các vấn đề mà các nguyên tắc này giúp giải quyết trong bối cảnh cấu trúc ứng dụng cho lớp giao diện người dùng.

Cấu trúc lớp giao diện người dùng

Thuật ngữGiao diện người dùng đề cập đến các phần tử trên giao diện người dùng như các hoạt động và các mảnh hiển thị dữ liệu, độc lập với các API mà các phần tử/mảnh đó sử dụng để thực hiện việc này (Views hoặc Jetpack Compose ). Vì vai trò củalớp dữ liệu là giữ, quản lý và cung cấp quyền truy cập vào dữ liệu ứng dụng, nên lớp giao diện người dùng phải thực hiện các bước sau:

  1. Sử dụng và biến đổi dữ liệu ứng dụng thành dữ liệu mà giao diện người dùng (UI) có thể dễ dàng kết xuất.
  2. Sử dụng và biến đổi dữ liệu kết xuất được trên giao diện người dùng thành các phần tử UI để người dùng thấy.
  3. Sử dụng các sự kiện đầu vào từ hoạt động của người dùng từ các phần tử UI được tập hợp đó và phản ánh hiệu ứng của chúng trong dữ liệu UI nếu cần.
  4. Lặp lại các bước từ 1 đến 3 khi cần.

Phần còn lại của hướng dẫn này minh hoạ cách triển khai lớp giao diện người dùng thực hiện các bước này. Cụ thể, hướng dẫn này bao gồm các tác vụ và khái niệm sau:

  • Cách xác định trạng thái giao diện người dùng.
  • Luồng dữ liệu một chiều (UDF) làm phương tiện để tạo và quản lý trạng thái giao diện người dùng.
  • Cách hiển thị trạng thái giao diện người dùng có các kiểu dữ liệu có thể quan sát được theo nguyên tắc UDF.
  • Cách triển khai giao diện người dùng sử dụng trạng thái giao diện có thể quan sát được.

Tác vụ cơ bản nhất trong số này là xác định trạng thái giao diện người dùng.

Xác định trạng thái giao diện người dùng

Tham khảo nghiên cứu điển hình đã nêu ở trên. Tóm lại, giao diện người dùng hiển thị danh sách các bài viết cùng với một số siêu dữ liệu của mỗi bài viết. Thông tin này chính là trạng thái giao diện người dùng mà ứng dụng hiển thị cho người dùng.

Nói cách khác: nếu giao diện người dùng là nội dung mà người dùng nhìn thấy, thì trạng thái giao diện người dùng chính là nội dung mà ứng dụng biểu thị người dùng sẽ thấy. Giống như hai mặt của một đồng xu, giao diện người dùng là đại diện hình ảnh của trạng thái giao diện người dùng. Bất kỳ thay đổi nào đối với trạng thái giao diện người dùng đều được phản ánh ngay lập tức trong giao diện người dùng.

Giao diện người dùng là kết quả của việc liên kết các phần tử trên giao diện người dùng trên màn hình với trạng thái giao diện người dùng.
Hình 3. Giao diện người dùng là kết quả của việc liên kết các phần tử trên giao diện người dùng trên màn hình với trạng thái giao diện người dùng.

Hãy cân nhắc nghiên cứu điển hình; để đáp ứng các yêu cầu của ứng dụng Tin tức, thông tin cần phải có để hiển thị đầy đủ giao diện người dùng có thể được gói gọn trong một lớp dữ liệu NewsUiState được xác định như sau:

data class NewsUiState(
    val isSignedIn: Boolean = false,
    val isPremium: Boolean = false,
    val newsItems: List<NewsItemUiState> = listOf(),
    val userMessages: List<Message> = listOf()
)

data class NewsItemUiState(
    val title: String,
    val body: String,
    val bookmarked: Boolean = false,
    ...
)

Bất biến

Định nghĩa về trạng thái giao diện người dùng trong ví dụ ở trên là bất biến. Lợi ích chính của thuộc tính này là các đối tượng bất biến cung cấp các đảm bảo đối với trạng thái của ứng dụng ngay lập tức. Việc này giải phóng giao diện người dùng để tập trung vào một vai trò duy nhất: đọc trạng thái và cập nhật các phần tử trên giao diện người dùng cho phù hợp. Do đó, bạn không bao giờ nên trực tiếp sửa đổi trạng thái giao diện người dùng trong giao diện người dùng, trừ khi giao diện người dùng đó là nguồn duy nhất của dữ liệu. Việc vi phạm nguyên tắc này sẽ dẫn đến nhiều nguồn thông tin chính xác cho cùng một phần thông tin, dẫn đến những điểm thiếu đồng nhất của dữ liệu và các lỗi nhỏ.

Ví dụ: nếu cập nhật thao tác gắn cờ bookmarked vào trong đối tượng NewsItemUiState từ trạng thái giao diện người dùng trong nghiên cứu điển hình tại lớp Activity, thì cờ đó sẽ xung đột với lớp dữ liệu làm nguồn trạng thái được đánh dấu trang của một bài viết. Các lớp dữ liệu bất biến rất hữu ích để ngăn chặn kiểu phản mẫu này.

Quy ước đặt tên trong hướng dẫn này

Trong hướng dẫn này, các lớp trạng thái giao diện người dùng được đặt tên dựa trên chức năng của màn hình hoặc một phần của màn hình mà lớp đó mô tả. Quy ước như sau:

functionality + UiState.

Ví dụ: trạng thái của màn hình hiển thị tin tức có thể gọi lệnh NewsUiState, còn trạng thái của một mục tin tức trong danh sách mục tin tức có thể là NewsItemUiState.

Quản lý trạng thái bằng Luồng dữ liệu một chiều

Phần trước đã thiết lập trạng thái giao diện người dùng là hiện trạng bất biến của các chi tiết cần thiết để giao diện người dùng hiển thị. Tuy nhiên, tính chất động của dữ liệu trong ứng dụng có nghĩa là trạng thái có thể thay đổi theo thời gian. Điều này có thể là do sự tương tác của người dùng hoặc các sự kiện khác làm thay đổi dữ liệu cơ bản được dùng để điền sẵn ứng dụng.

Những hoạt động tương tác này có thể hưởng lợi từ một trình trung gian để xử lý chúng, việc xác định logic áp dụng cho từng sự kiện và việc chuyển đổi cần thiết đối với các nguồn dữ liệu hỗ trợ để tạo trạng thái giao diện người dùng. Những tương tác này và logic của chúng có thể được đặt trong chính giao diện người dùng. Nhưng điều này có thể nhanh chóng trở nên khó sử dụng vì giao diện người dùng bắt đầu chuyển đổi trở nên phức tạp hơn so với tên gọi của nó: giao diện người dùng trở thành chủ sở hữu dữ liệu, nơi sản xuất, nơi chuyển đổi, v.v Hơn nữa, hoạt động xử lý này có thể ảnh hưởng đến khả năng thử nghiệm vì mã kết quả là một sự kết hợp chặt chẽ mà không có ranh giới rõ ràng. Cuối cùng, hoạt động giảm gánh nặng đó sẽ có lợi cho giao diện người dùng. Trừ khi trạng thái giao diện người dùng rất đơn giản, trách nhiệm duy nhất của giao diện người dùng là sử dụng và hiển thị trạng thái giao diện người dùng.

Phần này thảo luận về Luồng dữ liệu một chiều (UDF), một mẫu cấu trúc giúp thực thi quá trình phân tách trách nhiệm lành mạnh này.

Phần tử giữ trạng thái

Các lớp chịu trách nhiệm tạo trạng thái giao diện người dùng và chứa logic cần thiết mà từ đó tác vụ được gọi là phần tử giữ trạng thái. Các phần tử giữ trạng thái có nhiều kích thước, tuỳ thuộc vào phạm vi của các phần tử trên giao diện người dùng tương ứng mà người dùng quản lý, từ một tiện ích đơn lẻ như thanh ứng dụng ở dưới cùng cho đến toàn bộ màn hình hoặc một đích đến điều hướng.

Trong trường hợp sau, hoạt động triển khai điển hình là phiên bản của ViewModel, mặc dù tuỳ thuộc vào các yêu cầu của ứng dụng, nhưng bạn chỉ nên sử dụng một lớp đơn giản. Ví dụ: Ứng dụng News (Tin tức) trong nghiên cứu điển hình sử dụng một lớp NewsViewModel làm phần tử giữ trạng thái để tạo trạng thái giao diện người dùng cho màn hình hiển thị trong phần đó.

Có nhiều cách để lập mô hình hiện tượng đồng phụ thuộc giữa giao diện người dùng và tính năng nhà sản xuất trạng thái của giao diện đó. Tuy nhiên, do sự tương tác giữa giao diện người dùng và lớp ViewModel của giao diện phần lớn có thể được hiểu là sự kiện đầu vào và đảm bảo trạng thái đầu ra sau đó, mối quan hệ có thể được thể hiện như trong sơ đồ sau:

Các dữ liệu ứng dụng di chuyển từ lớp dữ liệu sang ViewModel. Trạng thái giao diện người dùng
    di chuyển từ ViewModel sang các phần tử trên giao diện người dùng và các sự kiện di chuyển từ các phần tử trên giao diện người dùng
quay trở lại ViewModel.
Hình 4. Sơ đồ về cách hoạt động của UDF trong ứng dụng kiến trúc .

Mô hình mà trạng thái giảm xuống và sự kiện tăng lên được gọi là luồng dữ liệu một chiều (UDF). Các hoạt động di chuyển kéo theo của mô hình này cho cấu trúc ứng dụng là như sau:

  • Tính năng ViewModel giữ và hiển thị trạng thái mà giao diện người dùng sử dụng. Trạng thái giao diện người dùng là dữ liệu ứng dụng được ViewModel chuyển đổi.
  • Giao diện người dùng thông báo cho ViewModel về các sự kiện của người dùng.
  • Tính năng ViewModel xử lý các thao tác của người dùng và cập nhật trạng thái.
  • Trạng thái đã cập nhật được đưa trở lại giao diện người dùng để hiển thị.
  • Các thao tác trên được lặp lại cho bất kỳ sự kiện nào gây ra hiện tượng đột biến của trạng thái.

Đối với các đích đến hoặc màn hình điều hướng, ViewModel hoạt động với các kho lưu trữ hoặc sử dụng các lớp điển hình để lấy và chuyển đổi dữ liệu thành trạng thái giao diện người dùng trong khi kết hợp tác động của các sự kiện có thể gây ra đột biến trạng thái. Nghiên cứu điển hình đã đề cập trước đó có chứa một danh sách các bài viết, mỗi bài viết có tiêu đề, phần mô tả, nguồn, tên tác giả, ngày xuất bản và có được đánh dấu trang hay không. Giao diện người dùng cho mỗi mục trong bài viết như sau:

Hình 5. Giao diện người dùng của một mục bài viết trong ứng dụng điển hình.

Việc một người dùng yêu cầu đánh dấu trang một bài viết là ví dụ về một sự kiện có thể gây ra những đột biến trạng thái. Là nhà sản xuất trạng thái, trách nhiệm của ViewModel là xác định tất cả các logic cần phải đó để điền sẵn tất cả các trường trong trạng thái giao diện người dùng và xử lý các sự kiện cần thiết để giao diện người dùng hiển thị đầy đủ.

Sự kiện giao diện người dùng xảy ra khi người dùng đánh dấu trang một bài viết. ViewModel
    thông báo cho lớp dữ liệu về sự thay đổi trạng thái. Lớp dữ liệu duy trì
    thay đổi dữ liệu và cập nhật dữ liệu ứng dụng. Dữ liệu ứng dụng mới với
    bài viết được đánh dấu trang sẽ được chuyển lên đến ViewModel, sau đó tạo ra
    trạng thái giao diện người dùng mới và chuyển sang các phần tử trên giao diện người dùng để hiển thị.
Hình 6. Sơ đồ minh hoạ chu kỳ của các sự kiện và dữ liệu trong UDF.

Các phần sau sẽ tìm hiểu kỹ hơn về các sự kiện gây ra thay đổi trạng thái và cách xử lý chúng bằng UDF.

Các loại logic

Đánh dấu một bài viết là ví dụ về logic kinh doanh vì bài viết này cung cấp giá trị cho ứng dụng của bạn. Để tìm hiểu thêm về điều này, hãy xem tranglớp dữ liệu. Tuy nhiên, có nhiều loại logic quan trọng cần xác định:

  • Logic nghiệp vụ là việc triển khai các yêu cầu của sản phẩm về dữ liệu ứng dụng. Như đã đề cập, ví dụ là đánh dấu trang một bài viết trong ứng dụng nghiên cứu điển hình. Logic kinh doanh thường được đặt trong lớp miền hoặc lớp dữ liệu, nhưng không bao giờ được đặt trong lớp giao diện người dùng.
  • Logic hành vi giao diện người dùng hoặc logic giao diện người dùngcách hiển thị các thay đổi về trạng thái trên màn hình. Các ví dụ bao gồm việc lấy văn bản phù hợp để hiển thị trên màn hình bằng Android Resources, chuyển đến màn hình cụ thể khi người dùng nhấp vào nút hoặc hiển thị thông báo của người dùng trên màn hình bằng một thông báo ngắn hoặc thanh thông báo nhanh.

Logic giao diện người dùng, đặc biệt là khi liên quan đến các loại giao diện người dùng như Context, phải nằm trong giao diện người dùng chứ không phải trong ViewModel. Nếu giao diện người dùng phát triển phức tạp và bạn muốn uỷ quyền logic giao diện người dùng cho một lớp khác để ưu tiên khả năng kiểm thử và phân tách các mối quan ngại, bạn có thể tạo một lớp đơn giản làm phần tử giữ trạng thái. Các lớp đơn giản được tạo trong giao diện người dùng có thể sử dụng các phần phụ thuộc SDK Android vì các lớp này tuân theo vòng đời của giao diện người dùng; các đối tượng ViewModel có thời gian tồn tại lâu hơn.

Để biết thêm thông tin về phần tử giữ trạng thái và mức độ phù hợp với bối cảnh của chúng trong việc giúp xây dựng giao diện người dùng, hãy xem hướng dẫn trạng thái trong Jetpack Compose.

Tại sao nên sử dụng UDF?

UDF lập mô hình chu kỳ tạo trạng thái như hiển thị trong Hình 4. Cơ chế này cũng tách biệt nơi bắt đầu thay đổi trạng thái, nơi có thay đổi về trạng thái và cuối cùng nơi sử dụng trạng thái. Việc phân tách này cho phép giao diện người dùng thực hiện chính xác nội dung của tên giao diện: hiển thị thông tin bằng cách quan sát các thay đổi trạng thái, và chuyển tiếp ý định của người dùng bằng cách truyền các thay đổi đó tới ViewModel.

Nói cách khác, UDF cho phép nội dung sau:

  • Tính nhất quán của dữ liệu. Có một nguồn thông tin xác thực duy nhất cho giao diện người dùng.
  • Khả năng thử nghiệm. Nguồn trạng thái bị tách riêng và do đó có thể thử nghiệm độc lập với giao diện người dùng.
  • Khả năng bảo trì. Đột biến của trạng thái tuân theo một mẫu được xác định rõ ràng trong đó đột biến là kết quả của cả các sự kiện của người dùng và nguồn dữ liệu mà chúng lấy ra.

Hiển thị trạng thái giao diện người dùng

Sau khi bạn xác định trạng thái giao diện người dùng và xác định cách bạn sẽ quản lý việc tạo trạng thái, bước tiếp theo là hiển thị trạng thái đã được tạo cho giao diện người dùng. Vì bạn đang sử dụng UDF để quản lý việc tạo ra trạng thái, bạn có thể coi trạng thái đã được tạo là một sự kiện phát trực tiếp—nói cách khác, nhiều phiên bản của trạng thái sẽ được tạo theo thời gian. Do đó, bạn nên hiển thị trạng thái giao diện người dùng trong một đối tượng giữ dữ liệu có thể ghi nhận được như LiveData hoặc StateFlow. Lý do là vì giao diện người dùng có thể phản ứng với bất kỳ thay đổi nào được thực hiện trong trạng thái không phải lấy dữ liệu trực tiếp từ ViewModel. Các loại này có lợi ích là luôn lưu vào bộ nhớ đệm phiên bản mới nhất của trạng thái giao diện người dùng. Hoạt động này rất hữu ích trong việc khôi phục trạng thái nhanh chóng sau khi thay đổi cấu hình.

Số lượt xem

class NewsViewModel(...) : ViewModel() {

    val uiState: StateFlow<NewsUiState> = …
}

Compose

class NewsViewModel(...) : ViewModel() {

    val uiState: NewsUiState = …
}

Để biết thông tin giới thiệu về LiveData với tư cách là chủ thể dữ liệu có thể quan sát, hãy xem codelab này. Để có phần giới thiệu tương tự về luồng Kotlin, hãy xem các luồng Kotlin trên Android.

Trong các trường hợp dữ liệu được hiển thị với giao diện người dùng tương đối đơn giản, thì thường nên gói dữ liệu trong một loại trạng thái giao diện người dùng vì loại trạng thái này truyền tải mối quan hệ giữa sự phát xạ của phần tử giữ trạng thái và màn hình hoặc phần tử trên giao diện người dùng được liên kết. Hơn nữa, khi phần tử trên giao diện người dùng trở nên dùng phức tạp hơn, sẽ dễ dàng hơn khi thêm định nghĩa về trạng thái giao diện người dùng nhằm cung cấp thêm thông tin cần thiết để hiển thị phần tử trên giao diện người dùng.

Một cách phổ biến để tạo một trình phát trực tiếp UiState là bằng cách hiển thị một trình phát trực tiếp có thể thay đổi hỗ trợ dưới dạng trình phát bất biến từ ViewModel—ví dụ: hiển thị MutableStateFlow<UiState> dưới dạng StateFlow<UiState>.

Số lượt xem

class NewsViewModel(...) : ViewModel() {

    private val _uiState = MutableStateFlow(NewsUiState())
    val uiState: StateFlow<NewsUiState> = _uiState.asStateFlow()

    ...

}

Compose

class NewsViewModel(...) : ViewModel() {

    var uiState by mutableStateOf(NewsUiState())
        private set

    ...
}

Khi đó, ViewModel có thể hiển thị các phương thức gây đột biến bên trong trạng thái, mà các đột biến này sẽ phát hành thông tin cập nhật để giao diện người dùng sử dụng. Ví dụ: trong trường hợp một hành động không đồng bộ cần được thực hiện; một coroutine có thể khởi chạy bằng cách sử dụng viewModelScope và trạng thái bất biến có khả năng được cập nhật sau khi hoàn thành.

Số lượt xem

class NewsViewModel(
    private val repository: NewsRepository,
    ...
) : ViewModel() {

    private val _uiState = MutableStateFlow(NewsUiState())
    val uiState: StateFlow<NewsUiState> = _uiState.asStateFlow()

    private var fetchJob: Job? = null

    fun fetchArticles(category: String) {
        fetchJob?.cancel()
        fetchJob = viewModelScope.launch {
            try {
                val newsItems = repository.newsItemsForCategory(category)
                _uiState.update {
                    it.copy(newsItems = newsItems)
                }
            } catch (ioe: IOException) {
                // Handle the error and notify the UI when appropriate.
                _uiState.update {
                    val messages = getMessagesFromThrowable(ioe)
                    it.copy(userMessages = messages)
                 }
            }
        }
    }
}

Compose

class NewsViewModel(
    private val repository: NewsRepository,
    ...
) : ViewModel() {

   var uiState by mutableStateOf(NewsUiState())
        private set

    private var fetchJob: Job? = null

    fun fetchArticles(category: String) {
        fetchJob?.cancel()
        fetchJob = viewModelScope.launch {
            try {
                val newsItems = repository.newsItemsForCategory(category)
                uiState = uiState.copy(newsItems = newsItems)
            } catch (ioe: IOException) {
                // Handle the error and notify the UI when appropriate.
                val messages = getMessagesFromThrowable(ioe)
                uiState = uiState.copy(userMessages = messages)
            }
        }
    }
}

Trong ví dụ trên, lớp NewsViewModel thử tìm nạp các bài viết cho một danh mục nhất định, rồi phản ánh kết quả của lần thử—cho dù thành công hay không thành công—ở trạng thái giao diện người dùng trong đó giao diện người dùng có thể phản ứng với lớp một cách phù hợp. Hãy xem mục Hiển thị lỗi trên màn hình để tìm hiểu thêm về cách xử lý lỗi.

Các yếu tố cần cân nhắc khác

Ngoài hướng dẫn trước, hãy cân nhắc các điều sau đây khi hiển thị trạng thái giao diện người dùng:

  • Đối tượng trạng thái giao diện người dùng phải xử lý các trạng thái có liên quan với nhau. Điều này giúp giảm bớt các điểm không thống nhất và giúp mã dễ hiểu hơn. Nếu bạn hiển thị danh sách các mục tin tức và số lượng dấu trang trong hai trình phát trực tiếp khác nhau, bạn có thể gặp phải trường hợp một nội dung đã được cập nhật và mục còn lại thì không. Khi bạn sử dụng một trình phát trực tiếp duy nhất, cả hai thành phần đều được cập nhật. Ngoài ra, một số logic kinh doanh có thể yêu cầu sự kết hợp của các nguồn. Ví dụ: bạn có thể chỉ cần hiển thị nút dấu trang nếu người dùng đăng nhập người dùng đó là người đăng ký gói thuê bao dịch vụ tin tức cao cấp. Bạn có thể xác định lớp trạng thái giao diện người dùng như sau:

    data class NewsUiState(
        val isSignedIn: Boolean = false,
        val isPremium: Boolean = false,
        val newsItems: List<NewsItemUiState> = listOf()
    )
    
    val NewsUiState.canBookmarkNews: Boolean get() = isSignedIn && isPremium
    

    Trong khai báo này, chế độ hiển thị của nút dấu trang là một thuộc tính phái sinh của hai thuộc tính khác. Khi logic kinh doanh ngày càng phức tạp, việc có một lớp UiState duy nhất, trong đó tất cả thuộc tính có sẵn ngay lập tức ngày càng trở nên quan trọng.

  • Trạng thái giao diện người dùng: một trình phát trực tiếp hay nhiều trình phát trực tiếp? Nguyên tắc hướng dẫn chính để lựa chọn giữa việc hiển thị trạng thái giao diện người dùng trong một trình phát trực tiếp hoặc trong nhiều trình phát trực tiếp là dấu đầu dòng trước đó: mối quan hệ giữa các mục đã phát ra. Ưu điểm lớn nhất của khả năng hiển thị quảng cáo phát trực tiếp là sự tiện lợi và tính nhất quán của dữ liệu: người tiêu dùng trạng thái luôn có thông tin mới nhất ngay lập tức tại bất kỳ thời điểm nào. Tuy nhiên, có những trường hợp mà các trình phát trực tiếp riêng biệt từ ViewModel có thể phù hợp:

    • Các loại dữ liệu không liên quan: Một số trạng thái cần thiết để hiển thị giao diện người dùng có thể hoàn toàn độc lập với nhau. Trong những trường hợp như vậy, chi phí của việc gộp các trạng thái khác nhau này lại với nhau có thể lớn hơn lợi ích, đặc biệt là nếu một trong các trạng thái này được cập nhật thường xuyên hơn các trạng thái còn lại.

    • Di chuyển UiState: Càng có nhiều trường trong một đối tượng UiState, càng có nhiều khả năng trình phát trực tiếp sẽ phát do một trong các trường của nó đang được cập nhật. Bởi vì các chế độ xem không có cơ chế di chuyển để hiểu liệu lượt phát liên tiếp là khác nhau hay giống nhau, mọi lượt phát đều tạo ra một bản cập nhật cho chế độ xem. Điều này có nghĩa là có thể cần giảm thiểu sử dụng các API hoặc phương thức Flow như distinctUntilChanged() trên LiveData.

Sử dụng trạng thái giao diện người dùng

Để sử dụng trình phát trực tiếp của đối tượng UiState trong giao diện người dùng, bạn sử dụng toán tử đầu cuối cho loại dữ liệu có khả năng ghi nhận mà bạn đang sử dụng. Ví dụ: đối với LiveData, bạn sử dụng phương thức observe() và đối với các luồng Kotlin, bạn sử dụng phương thức collect() hoặc các biến thể của phương thức này.

Khi sử dụng chủ thể sở hữu dữ liệu có thể quan sát trong giao diện người dùng, hãy đảm bảo rằng bạn xem xét vòng đời của giao diện người dùng. Việc này rất quan trọng vì giao diện người dùng sẽ không quan sát trạng thái giao diện người dùng khi chế độ xem không hiển thị cho người dùng. Để tìm hiểu thêm về chủ đề này, hãy xem bài đăng trên blog này. Khi sử dụng LiveData, LifecycleOwner ngầm quan tâm tới các vấn đề liên quan vòng đời. Khi sử dụng luồng, cách tốt nhất là xử lý luồng với phạm vi coroutine thích hợp và API repeatOnLifecycle:

Số lượt xem

class NewsActivity : AppCompatActivity() {

    private val viewModel: NewsViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        ...

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState.collect {
                    // Update UI elements
                }
            }
        }
    }
}

Compose

@Composable
fun LatestNewsScreen(
    viewModel: NewsViewModel = viewModel()
) {
    // Show UI elements based on the viewModel.uiState
}

Hiển thị thao tác đang diễn ra

Một cách đơn giản để biểu thị các trạng thái đang tải trong một lớp UiState là có trường boolean:

data class NewsUiState(
    val isFetchingArticles: Boolean = false,
    ...
)

Giá trị của cờ này đại diện cho việc có hoặc không có thanh tiến trình trong giao diện người dùng.

Số lượt xem

class NewsActivity : AppCompatActivity() {

    private val viewModel: NewsViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        ...

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                // Bind the visibility of the progressBar to the state
                // of isFetchingArticles.
                viewModel.uiState
                    .map { it.isFetchingArticles }
                    .distinctUntilChanged()
                    .collect { progressBar.isVisible = it }
            }
        }
    }
}

Compose

@Composable
fun LatestNewsScreen(
    modifier: Modifier = Modifier,
    viewModel: NewsViewModel = viewModel()
) {
    Box(modifier.fillMaxSize()) {

        if (viewModel.uiState.isFetchingArticles) {
            CircularProgressIndicator(Modifier.align(Alignment.Center))
        }

        // Add other UI elements. For example, the list.
    }
}

Hiển thị lỗi trên màn hình

Việc hiển thị lỗi trong giao diện người dùng sẽ tương tự như việc hiển thị thao tác đang diễn ra vì chúng đều dễ dàng được biểu thị bằng các giá trị boolean cho biết chúng có hiện diện hay không. Tuy nhiên, lỗi cũng có thể bao gồm thông báo liên quan để chuyển tiếp trở lại tới người dùng hoặc một hành động được liên kết với chúng để thử lại thao tác không thành công. Do đó, mặc dù thao tác đang diễn ra là thực hiện tải hoặc không tải dữ liệu, nhưng các trạng thái lỗi có thể cần được mô hình hoá với các lớp dữ liệu lưu trữ siêu dữ liệu phù hợp với ngữ cảnh của lỗi.

Ví dụ: hãy xem xét ví dụ từ phần trước, hiển thị một thanh tiến trình trong khi tìm nạp bài viết. Nếu thao tác này dẫn đến lỗi, bạn có thể muốn hiển thị một hoặc nhiều thông báo cho người dùng biết chi tiết về sự cố.

data class Message(val id: Long, val message: String)

data class NewsUiState(
    val userMessages: List<Message> = listOf(),
    ...
)

Sau đó, thông báo lỗi có thể được hiển thị cho người dùng dưới dạng các thành phần giao diện người dùng như thanh thông báo nhanh. Điều này liên quan đến cách các sự kiện giao diện người dùng được tạo và sử dụng, hãy xem trang các sự kiện giao diện người dùng để biết thêm thông tin.

Chuỗi lệnh và tính đồng thời

Mọi tác vụ được thực hiện trong ViewModel nên là luồng chính–an toàn để gọi lệnh từ chuỗi lệnh chính. Lý do là do dữ liệu và các lớp miền chịu trách nhiệm về việc chuyển tác vụ sang một chuỗi lệnh khác.

Nếu một ViewModel thực hiện các tác vụ lâu dài, sao đó nó cũng chịu trách nhiệm di chuyển logic đó sang một chuỗi lệnh ở chế độ nền. Các coroutine Kotlin là một cách tuyệt vời để quản lý các thao tác đồng thời và chúng được thành phần cấu trúc Jetpack cung cấp hỗ trợ tích hợp. Để tìm hiểu thêm về cách sử dụng coroutine trong các ứng dụng Android, hãy xem Coroutine Kotlin trên Android.

Các thay đổi trong điều hướng ứng dụng thường được các sự kiện tương tự trình phát tạo ra. Ví dụ: sau khi một lớp SignInViewModel thực hiện đăng nhập, UiState có thể có một trường isSignedIn đặt với true. Những trình kích hoạt như vậy nên được sử dụng giống như các trình kích hoạt được đề cập trong mục Sử dụng trạng thái giao diện người dùng bên trên, ngoại trừ hoạt động triển khai cần phải tuân theo Thành phần điều hướng

Đánh số trang

Thư viện phân trang được sử dụng trong giao diện người dùng với một loại lệnh gọi PagingData. Vì PagingData đại diện và chứa các mục có thể thay đổi theo thời gian. Nói cách khác, thuộc tính này không phải là một loại bất biến nên không nên được biểu thị ở trạng thái giao diện người dùng bất biến. Thay vào đó, bạn nên hiển thị giao diện từ ViewModel một cách độc lập trong trình phát trực tiếp riêng của nó. Hãy xem lớp học lập trình Đánh số trang Android để biết ví dụ cụ thể về nội dung này.

Ảnh động

Để cung cấp chuyển đổi điều hướng ở cấp cao nhất một cách linh hoạt và nhịp nhàng, có thể bạn cần đợi màn hình thứ hai tải dữ liệu trước khi bắt đầu ảnh động. Khung chế độ xem Android cung cấp các giao diện kết nối để trì hoãn việc chuyển đổi giữa các đích đến của mảnh với các API postponeEnterTransition()startPostponedEnterTransition(). Các API này giúp đảm bảo rằng các thành phần giao diện người dùng trên màn hình thứ hai (thường là một hình ảnh tìm nạp từ mạng) sẵn sàng hiển thị trước khi giao diện người dùng tạo hiệu ứng động cho quá trình chuyển đổi sang màn hình đó. Để biết thêm thông tin chi tiết và các thông số kỹ thuật triển khai cụ thể, hãy xem Mẫu Chuyển động dành cho Android.

Mẫu

Các mẫu sau đây của Google minh hoạ cách sử dụng lớp giao diện người dùng. Hãy khám phá những mẫu đó để xem hướng dẫn này trong thực tế: