Chuyển đổi luồng dữ liệu

Khi làm việc với dữ liệu được phân trang, thông thường, bạn cần biến đổi luồng dữ liệu khi tải dữ liệu. Ví dụ: Bạn có thể cần lọc danh sách các mục hoặc chuyển đổi các mục thành một loại khác trước khi hiển thị các mục đó trong giao diện người dùng. Một trường hợp sử dụng phổ biến khác của tính năng biến đổi luồng dữ liệu là thêm các dòng phân cách danh sách.

Nhìn chung, việc áp dụng các phép biến đổi trực tiếp vào luồng dữ liệu cho phép bạn tách riêng các cấu trúc kho lưu trữ và cấu trúc giao diện người dùng.

Trang này giả định rằng bạn đã quen thuộc với cách sử dụng cơ bản thư viện Phân trang.

Thao tác áp dụng các phép biến đổi cơ bản

PagingData được đóng gói trong một luồng phản ứng, nên bạn có thể áp dụng các thao tác biến đổi cho dữ liệu ở mức độ tăng dần giữa quá trình tải và trình bày dữ liệu.

Để áp dụng các phép biến đổi cho từng đối tượng PagingData trong luồng, hãy đặt các phép biến đổi bên trong map() thao tác trên luồng:

pager.flow // Type is Flow<PagingData<User>>.
  // Map the outer stream so that the transformations are applied to
  // each new generation of PagingData.
  .map { pagingData ->
    // Transformations in this block are applied to the items
    // in the paged data.
}

Chuyển đổi dữ liệu

Thao tác cơ bản nhất trên luồng dữ liệu là chuyển đổi dữ liệu đó sang một loại khác. Sau khi có quyền truy cập vào đối tượng PagingData, bạn có thể thực hiện một thao tác map() với từng mục riêng lẻ trong danh sách được phân trang trong đối tượng PagingData.

Một trường hợp sử dụng phổ biến của thao tác này là ánh xạ đối tượng lớp cơ sở dữ liệu hoặc mạng vào một đối tượng cụ thể dùng trong lớp giao diện người dùng. Ví dụ bên dưới minh hoạ cách áp dụng loại thao tác ánh xạ này:

pager.flow // Type is Flow<PagingData<User>>.
  .map { pagingData ->
    pagingData.map { user -> UiModel(user) }
  }

Một cách chuyển đổi dữ liệu phổ biến khác là lấy dữ liệu đầu vào của người dùng, chẳng hạn như chuỗi truy vấn và chuyển đổi dữ liệu đó thành kết quả đầu ra của yêu cầu để hiển thị. Phương thức này đòi hỏi hệ thống phải theo dõi và ghi lại dữ liệu đầu vào truy vấn của người dùng, thực hiện yêu cầu và đẩy kết quả truy vấn trở lại giao diện người dùng.

Bạn có thể theo dõi đầu vào của truy vấn bằng cách sử dụng API luồng. Hãy giữ tham chiếu luồng trong ViewModel của bạn. Lớp giao diện người dùng không được có quyền truy cập trực tiếp vào lớp đó; thay vào đó, hãy xác định một hàm để thông báo cho ViewModel về truy vấn của người dùng.

private val queryFlow = MutableStateFlow("")

fun onQueryChanged(query: String) {
  queryFlow.value = query
}

Khi giá trị truy vấn thay đổi trong luồng dữ liệu, bạn có thể thực hiện các thao tác để chuyển đổi giá trị truy vấn thành loại dữ liệu mong muốn và trả về kết quả cho lớp giao diện người dùng. Hàm chuyển đổi cụ thể tuỳ thuộc vào ngôn ngữ và khung được sử dụng, nhưng tất cả đều cung cấp chức năng tương tự.

val querySearchResults: Flow<User> = queryFlow.flatMapLatest { query ->
  // The database query returns a Flow which is output through
  // querySearchResults
  userDatabase.searchBy(query)
}

Việc sử dụng các thao tác như flatMapLatest hoặc switchMap đảm bảo rằng chỉ các kết quả mới nhất mới được trả về giao diện người dùng. Nếu người dùng thay đổi đầu vào truy vấn trước khi thao tác cơ sở dữ liệu hoàn tất, thì các thao tác này sẽ loại bỏ kết quả từ truy vấn cũ và bắt đầu tìm kiếm mới ngay lập tức.

Thao tác lọc dữ liệu

Một thao tác phổ biến khác là lọc. Bạn có thể lọc dữ liệu dựa trên tiêu chí của người dùng hoặc bạn có thể loại bỏ dữ liệu khỏi giao diện người dùng nếu dữ liệu đó cần được ẩn dựa trên các tiêu chí khác.

Bạn cần đặt các phép lọc này bên trong lệnh gọi map() vì bộ lọc này áp dụng cho đối tượng PagingData. Sau khi dữ liệu bị lọc ra khỏi PagingData, thực thể PagingData mới sẽ được truyền vào lớp giao diện người dùng để hiển thị.

pager.flow // Type is Flow<PagingData<User>>.
  .map { pagingData ->
    pagingData.filter { user -> !user.hiddenFromUi }
  }

Thêm các dòng phân cách danh sách

Thư viện phân trang hỗ trợ các dòng phân cách danh sách động. Bạn có thể cải thiện mức độ dễ đọc của danh sách bằng cách chèn dòng phân cách trực tiếp vào luồng dữ liệu dưới dạng các thành phần kết hợp trong bố cục. Do vậy, dòng phân cách là những thành phần kết hợp có đầy đủ tính năng, cho phép tương tác đầy đủ, tạo kiểu và ngữ nghĩa về khả năng tiếp cận.

Quá trình chèn dòng phân cách vào danh sách được phân trang bao gồm 3 bước:

  1. Chuyển đổi mô hình giao diện người dùng để phù hợp với các mục có dòng phân cách. Một cách để thực hiện việc này là gói mục dữ liệu và dòng phân cách vào một lớp kín. Điều này cho phép giao diện người dùng xử lý nhiều loại mục trong cùng một danh sách.
  2. Biến đổi luồng dữ liệu để tự động thêm dòng phân cách giữa quá trình tải dữ liệu và trình bày dữ liệu.
  3. Cập nhật giao diện người dùng để xử lý các mục có dòng phân cách.

Chuyển đổi mô hình giao diện người dùng

Thư viện phân trang sẽ chèn dòng phân cách danh sách vào giao diện người dùng dưới dạng các mục thực tế trong danh sách. Tuy nhiên, các mục có dòng phân cách phải dễ phân biệt với các mục dữ liệu trong danh sách để đảm bảo cả hai loại thành phần kết hợp đều được hiển thị riêng biệt. Giải pháp cho yêu cầu này là tạo một lớp Kotlin kín có các lớp con để đại diện cho dữ liệu và dòng phân cách của bạn. Ngoài ra, bạn có thể tạo một lớp cơ sở bằng cách dùng lớp mục danh sách và lớp dòng phân cách để mở rộng.

Giả sử bạn muốn thêm dòng phân cách vào danh sách đã phân trang của mục User. Đoạn mã sau đây cho biết cách tạo một lớp cơ sở mà trong đó các thực thể có thể là UserModel hoặc SeparatorModel:

sealed class UiModel {
  class UserModel(val id: String, val label: String) : UiModel() {
    constructor(user: User) : this(user.id, user.label)
  }

  class SeparatorModel(val description: String) : UiModel()
}

Biến đổi luồng dữ liệu

Bạn phải áp dụng các phép biến đổi cho luồng dữ liệu sau khi tải và trước khi trình bày nó. Bạn nên thực hiện các phép biến đổi theo các bước sau:

  • Chuyển đổi các mục danh sách đã tải để phản ánh loại mục cơ sở mới.
  • Dùng phương thức PagingData.insertSeparators() để thêm dòng phân cách.

Để tìm hiểu thêm về các thao tác biến đổi, hãy xem phần Áp dụng các phép biến đổi cơ bản.

Ví dụ sau đây cho thấy các thao tác biến đổi để cập nhật luồng PagingData<User> thành luồng PagingData<UiModel> với các dòng phân cách được bổ sung:

pager.flow.map { pagingData: PagingData<User> ->
  // Map outer stream, so you can perform transformations on
  // each paging generation.
  pagingData
  .map { user ->
    // Convert items in stream to UiModel.UserModel.
    UiModel.UserModel(user)
  }
  .insertSeparators<UiModel.UserModel, UiModel> { before, after ->
    when {
      before == null -> UiModel.SeparatorModel("HEADER")
      after == null -> UiModel.SeparatorModel("FOOTER")
      shouldSeparate(before, after) -> UiModel.SeparatorModel(
        "BETWEEN ITEMS $before AND $after"
      )
      // Return null to avoid adding a separator between two items.
      else -> null
    }
  }
}

Xử lý các dòng phân cách trong giao diện người dùng

Bước cuối cùng là thay đổi giao diện người dùng cho phù hợp với loại mục của dòng phân cách. Trong bố cục lười biếng, bạn có thể xử lý nhiều loại mục bằng cách kiểm tra loại của từng UiModel được phát. Khi lặp lại dữ liệu được phân trang, hãy sử dụng câu lệnh when để gọi thành phần kết hợp thích hợp. Điều này cho phép bạn cung cấp một giao diện người dùng riêng biệt cho các mục dữ liệu và dòng phân cách.

@Composable fun UserList(pagingItems: LazyPagingItems) {
  LazyColumn {
    items(
      count = pagingItems.itemCount,
      key = { index ->
        val item = pagingItems.peek(index)
        when (item) {
          is UiModel.UserModel -> item.user.id
          is UiModel.SeparatorModel -> item.description
          else -> index
        }
      }
    ) { index ->
      when (val item = pagingItems[index]) {
        is UiModel.UserModel -> UserItemComposable(item.user)
        is UiModel.SeparatorModel -> SeparatorComposable(item.description)
        null -> PlaceholderComposable()
      }
    }
  }
}

Tránh lặp lại công việc

Một vấn đề chính cần tránh là ứng dụng phải thực hiện các công việc không cần thiết. Việc tìm nạp dữ liệu là một thao tác tốn kém và các phép biến đổi dữ liệu cũng có thể mất nhiều thời gian. Sau khi hệ thống tải và chuẩn bị dữ liệu để hiển thị trong giao diện người dùng, dữ liệu sẽ được lưu trong trường hợp có thay đổi về cấu hình và giao diện người dùng cần được tạo lại.

Thao tác cachedIn() sẽ lưu kết quả của các phép biến đổi xảy ra trước đó vào bộ nhớ đệm. Thông thường, bạn áp dụng toán tử này trong ViewModel trước khi hiển thị Flow cho các thành phần kết hợp.

Để quản lý bộ nhớ đệm đúng cách, hãy truyền CoroutineScope đến cachedIn(), như minh hoạ trong ví dụ sau bằng cách sử dụng viewModelScope.

pager.flow // Type is Flow<PagingData<User>>.
  .map { pagingData ->
    pagingData.filter { user -> !user.hiddenFromUi }
      .map { user -> UiModel.UserModel(user) }
  }
  .cachedIn(viewModelScope)

Để biết thêm thông tin về cách sử dụng cachedIn() với luồng PagingData, hãy xem Thiết lập luồng của PagingData.

Tài nguyên khác

Để tìm hiểu thêm về thư viện Paging, hãy xem các tài nguyên khác sau đây:

Tài liệu

Nội dung về lượt xem