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

Vai trò của giao diện người dùng là hiển thị dữ liệu ứng dụng trên màn hình. Giao diện người dùng cũng đóng vai trò 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 lượt 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 trang các bài viết thực sự nổi bật. Do có thể có rất nhiều bài viết tại mọi thời điểm, nên người đọc phải có thể 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.
Một ứng dụng tin tức mẫu cho thấy bản xem trước của bài viết, trong đó có một bài viết được đánh dấu.
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 vùng chứa và hàm có khả năng kết hợp hiển thị dữ liệu. Để xây dựng giao diện người dùng Android, Jetpack Compose là bộ công cụ được đề xuất. 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 người dùng 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

Trong nghiên cứu điển hình được trình bày trước đó, 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,
    ...
)

Để biết thêm thông tin về trạng thái giao diện người dùng, hãy xem bài viết Trạng thái và Jetpack Compose.

Tính bất biến

Định nghĩa về trạng thái giao diện người dùng trong ví dụ trước 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 vai trò chính: đọ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. Không bao giờ sửa đổi trạng thái giao diện người dùng trong giao diện người dùng một cách trực tiếp, trừ phi 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ụ: hãy xem xét nghiên cứu điển hình trước đó. Nếu bạn cập nhật cờ bookmarked trong đối tượng NewsItemUiState từ trạng thái giao diện người dùng trong 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 loại điểm không nhất quán 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ác nguồn dữ liệu hỗ trợ để tạo trạng thái giao diện người dùng. Mặc dù 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 đảm nhận quá nhiều trách nhiệm. 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ả được liên kết chặt chẽ. Trừ phi trạng thái giao diện người dùng rất đơn giản, hãy đảm bảo 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

Phần tử giữ trạng thái là các lớp chịu trách nhiệm tạo trạng thái giao diện người dùng và logic cần thiết để tạo 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à chú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:

Một mục bài viết duy nhất trong ứng dụng nghiên cứu điển hình. Giao diện người dùng cho thấy hình thu nhỏ, tiêu đề bài viết, tác giả, thời gian đọc ước tính của bài viết và biểu tượng dấu trang.
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 thiết để đ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.

Giữ logic giao diện người dùng trong giao diện người dùng, chứ không phải trong ViewModel, đặc biệt là khi logic đó liên quan đến các loại giao diện người dùng như Context. 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.

Khi 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. 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ư StateFlow. Điều này cho phép giao diện người dùng phản ứng với mọi thay đổi được thực hiện trong trạng thái mà không cần phải lấy dữ liệu trực tiếp từ ViewModel theo cách thủ công. Việc này cũng 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.

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

    val uiState: NewsUiState = 
}

Để biết thông tin giới thiệu về các luồng Kotlin, hãy xem các luồng Kotlin trên Android. Để tìm hiểu cách sử dụng StateFlow làm phần tử giữ dữ liệu có thể ghi nhận được, hãy xem lớp học lập trình Trạng thái nâng cao và hiệu ứng phụ trong Jetpack Compose.

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. Khi phần tử trên giao diện người dùng trở nên phức tạp hơn, bạn có thể dễ dàng thêm vào định nghĩa về trạng thái giao diện người dùng, nhờ đó, bạn có thể 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 luồng UiState là bằng cách hiển thị một thuộc tính mutableStateOf bằng private set, giữ trạng thái có thể thay đổi bên trong ViewModel nhưng chỉ đọc đối với giao diện người dùng.

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 bạn cần thực hiện một thao tác không đồng bộ. Bạn có thể khởi chạy một coroutine bằng cách sử dụng viewModelScope, sau đó cập nhật trạng thái có thể thay đổi sau khi hoàn tất.

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ước, lớp NewsViewModel cố gắng 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. Để biết thêm thông tin về cách xử lý lỗi, hãy xem phần Hiển thị lỗi trên màn hình.

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:

  • Sử dụng một đối tượng trạng thái giao diện người dùng duy nhất để 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 một 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 luồng hoặc trong nhiều luồng là mối quan hệ giữa các mục đượ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ậ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 đối tượng đó đang được cập nhật. Vì các phần tử trên giao diện người dùng không có một cơ chế so sánh để hiểu liệu các 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 phần tử trên giao diện người dùng. Điều này có nghĩa là có thể cần giảm thiểu bằng cách sử dụng các phương thức API Flow như distinctUntilChanged().

Để biết thêm thông tin về quá trình kết xuất và trạng thái giao diện người dùng, hãy xem phần Vòng đời của thành phần kết hợp.

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

Để sử dụng luồng đối tượng UiState trong giao diện người dùng, hãy dùng toán tử đầu cuối cho loại dữ liệu có thể ghi nhận được mà bạn đang sử dụng. Ví dụ: đối với các luồng Kotlin, hãy 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ể ghi nhận được trong giao diện người dùng, hãy nhớ xem xét vòng đời của giao diện người dùng. Đừng để giao diện người dùng quan sát trạng thái giao diện người dùng khi thành phần kết hợp 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 này trên blog. Khi sử dụng luồng, cách tốt nhất là xử lý các vấn đề liên quan đến vòng đời bằng phạm vi coroutine thích hợp và API collectAsStateWithLifecycle:

@Composable
private fun ConversationScreen(
    conversationViewModel: ConversationViewModel = viewModel()
) {

    val messages by conversationViewModel.messages.collectAsStateWithLifecycle()

    ConversationScreen(
        messages = messages,
        onSendMessage = { message: Message -> conversationViewModel.sendMessage(message) }
    )
}

@Composable
private fun ConversationScreen(
    messages: List<Message>,
    onSendMessage: (Message) -> Unit
) {

    MessagesList(messages, onSendMessage)
    /* ... */
}

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.

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

Hãy xem xét ví dụ trước cho thấy 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 đó, bạn có thể trình bày thông báo lỗi cho người dùng dưới dạng các phần tử giao diện người dùng như thanh thông báo nhanh. Để biết thêm thông tin về cách các sự kiện giao diện người dùng được tạo và sử dụng, hãy xem phần các sự kiện giao diện người dùng.

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

Đảm bảo mọi thao tác được thực hiện trong ViewModel đều an toàn cho luồng chính – an toàn khi gọi từ luồng chính. Các lớp dữ liệu và miền chịu trách nhiệm về việc chuyển tác vụ sang một luồng 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. Sử dụng các trình kích hoạt như vậy 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 trước đó, nhưng hãy trì hoãn việc triển khai sử dụng cho Thành phần điều hướng.

Để biết thêm thông tin về hoạt động điều hướng trên giao diện người dùng, hãy xem bài viết Navigation 3.

Paging

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 đừng biểu thị thuộc tính này ở trạng thái giao diện người dùng bất biến. Thay vào đó, hãy hiển thị giao diện này từ ViewModel một cách độc lập trong luồng riêng.

Ví dụ sau đây minh hoạ API Compose của thư viện Paging:

@Composable
fun MyScreen(flow: Flow<PagingData<String>>) {
    val lazyPagingItems = flow.collectAsLazyPagingItems()
    LazyColumn {
        items(
            lazyPagingItems.itemCount,
            key = lazyPagingItems.itemKey { it }
        ) { index ->
            val item = lazyPagingItems[index]
            Text("Item is $item")
        }
    }
}

Ảnh động

Để cung cấp chuyển đổi điều hướng ở cấp cao nhất một cách 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.

Để biết thêm thông tin về hiệu ứng chuyển đổi điều hướng, hãy xem phần Điều hướng 3Hiệu ứng chuyển đổi phần tử dùng chung trong Compose.

Tài nguyên khác

Xem nội dung

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