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 thao tác map() trên luồng:

Kotlin

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

Java

PagingRx.getFlowable(pager) // Type is Flowable<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.
  });

Java

// Map the outer stream so that the transformations are applied to
// each new generation of PagingData.
Transformations.map(
  // Type is LiveData<PagingData<User>>.
  PagingLiveData.getLiveData(pager),
  pagingData -> {
    // Transformations in this block are applied to the items
    // in the paged data.
  });

Thao tác 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:

Kotlin

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

Java

// Type is Flowable<PagingData<User>>.
PagingRx.getFlowable(pager)
  .map(pagingData ->
    pagingData.map(UiModel.UserModel::new)
  )

Java

Transformations.map(
  // Type is LiveData<PagingData<User>>.
  PagingLiveData.getLiveData(pager),
  pagingData ->
    pagingData.map(UiModel.UserModel::new)
)

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.

Kotlin

private val queryFlow = MutableStateFlow("")

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

Java

private BehaviorSubject<String> querySubject = BehaviorSubject.create("");

public void onQueryChanged(String query) {
  queryFlow.onNext(query)
}

Java

private MutableLiveData<String> queryLiveData = new MutableLiveData("");

public void onQueryChanged(String query) {
  queryFlow.setValue(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ự.

Kotlin

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

Java

Observable<User> querySearchResults =
  querySubject.switchMap(query -> userDatabase.searchBy(query));

Java

LiveData<User> querySearchResults = Transformations.switchMap(
  queryLiveData,
  query -> 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ị.

Kotlin

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

Java

// Type is Flowable<PagingData<User>>.
PagingRx.getFlowable(pager)
  .map(pagingData ->
    pagingData.filter(user -> !user.isHiddenFromUi())
  )
}

Java

Transformations.map(
  // Type is LiveData<PagingData<User>>.
  PagingLiveData.getLiveData(pager),
  pagingData ->
    pagingData.filter(user -> !user.isHiddenFromUi())
)

Tính năng 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 mục RecyclerView trong danh sách. Do vậy, dòng phân cách là những đối tượng ViewHolder có đầy đủ tính năng, cho phép tương tác, tập trung vào khả năng tiếp cận và tất cả các tính năng khác mà một View cung cấp.

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

Thao tác 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 RecyclerView 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 để cho phép các mục này liên kết với loại ViewHolder khác thông qua giao diện người dùng đặc 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:

Kotlin

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()
}

Java

class UiModel {
  private UiModel() {}

  static class UserModel extends UiModel {
    @NonNull
    private String mId;
    @NonNull
    private String mLabel;

    UserModel(@NonNull String id, @NonNull String label) {
      mId = id;
      mLabel = label;
    }

    UserModel(@NonNull User user) {
      mId = user.id;
      mLabel = user.label;
    }

    @NonNull
    public String getId() {
      return mId;
    }

    @NonNull
    public String getLabel() {
      return mLabel;
      }
    }

    static class SeparatorModel extends UiModel {
    @NonNull
    private String mDescription;

    SeparatorModel(@NonNull String description) {
      mDescription = description;
    }

    @NonNull
    public String getDescription() {
      return mDescription;
    }
  }
}

Java

class UiModel {
  private UiModel() {}

  static class UserModel extends UiModel {
    @NonNull
    private String mId;
    @NonNull
    private String mLabel;

    UserModel(@NonNull String id, @NonNull String label) {
      mId = id;
      mLabel = label;
    }

    UserModel(@NonNull User user) {
      mId = user.id;
      mLabel = user.label;
    }

    @NonNull
    public String getId() {
      return mId;
    }

    @NonNull
    public String getLabel() {
      return mLabel;
      }
    }

    static class SeparatorModel extends UiModel {
    @NonNull
    private String mDescription;

    SeparatorModel(@NonNull String description) {
      mDescription = description;
    }

    @NonNull
    public String getDescription() {
      return mDescription;
    }
  }
}

Tính năng 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:

Kotlin

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
    }
  }
}

Java

// Map outer stream, so you can perform transformations on each
// paging generation.
PagingRx.getFlowable(pager).map(pagingData -> {
  // First convert items in stream to UiModel.UserModel.
  PagingData<UiModel> uiModelPagingData = pagingData.map(
    UiModel.UserModel::new);

  // Insert UiModel.SeparatorModel, which produces PagingData of
  // generic type UiModel.
  return PagingData.insertSeparators(uiModelPagingData,
    (@Nullable UiModel before, @Nullable UiModel after) -> {
      if (before == null) {
        return new UiModel.SeparatorModel("HEADER");
      } else if (after == null) {
        return new UiModel.SeparatorModel("FOOTER");
      } else if (shouldSeparate(before, after)) {
        return new UiModel.SeparatorModel("BETWEEN ITEMS "
          + before.toString() + " AND " + after.toString());
      } else {
        // Return null to avoid adding a separator between two
        // items.
        return null;
      }
    });
});

Java

// Map outer stream, so you can perform transformations on each
// paging generation.
Transformations.map(PagingLiveData.getLiveData(pager),
  pagingData -> {
    // First convert items in stream to UiModel.UserModel.
    PagingData<UiModel> uiModelPagingData = pagingData.map(
      UiModel.UserModel::new);

    // Insert UiModel.SeparatorModel, which produces PagingData of
    // generic type UiModel.
    return PagingData.insertSeparators(uiModelPagingData,
      (@Nullable UiModel before, @Nullable UiModel after) -> {
        if (before == null) {
          return new UiModel.SeparatorModel("HEADER");
        } else if (after == null) {
          return new UiModel.SeparatorModel("FOOTER");
        } else if (shouldSeparate(before, after)) {
          return new UiModel.SeparatorModel("BETWEEN ITEMS "
            + before.toString() + " AND " + after.toString());
        } else {
          // Return null to avoid adding a separator between two
          // items.
          return 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. Hãy tạo bố cục và trình lưu giữ thành phần hiển thị (view holder) cho các mục có dòng phân cách và thay đổi bộ chuyển đổi danh sách để sử dụng RecyclerView.ViewHolder làm trình lưu giữ thành phần hiển thị để có thể xử lý nhiều hơn một loại trình lưu giữ thành phần hiển thị. Ngoài ra, bạn có thể xác định một lớp cơ sở chung mà cả mục và các lớp của trình lưu giữ thành phần hiển thị dòng phân cách mở rộng.

Bạn cũng phải thực hiện các thay đổi sau đối với bộ chuyển đổi danh sách của mình:

  • Thêm các trường hợp vào phương thức onCreateViewHolder()onBindViewHolder() để điều chỉnh cho phù hợp với các mục danh sách có dòng phân cách.
  • Triển khai một trình so sánh mới.

Kotlin

class UiModelAdapter :
  PagingDataAdapter<UiModel, RecyclerView.ViewHolder>(UiModelComparator) {

  override fun onCreateViewHolder(
    parent: ViewGroup,
    viewType: Int
  ) = when (viewType) {
    R.layout.item -> UserModelViewHolder(parent)
    else -> SeparatorModelViewHolder(parent)
  }

  override fun getItemViewType(position: Int) {
    // Use peek over getItem to avoid triggering page fetch / drops, since
    // recycling views is not indicative of the user's current scroll position.
    return when (peek(position)) {
      is UiModel.UserModel -> R.layout.item
      is UiModel.SeparatorModel -> R.layout.separator_item
      null -> throw IllegalStateException("Unknown view")
    }
  }

  override fun onBindViewHolder(
    holder: RecyclerView.ViewHolder,
    position: Int
  ) {
    val item = getItem(position)
    if (holder is UserModelViewHolder) {
      holder.bind(item as UserModel)
    } else if (holder is SeparatorModelViewHolder) {
      holder.bind(item as SeparatorModel)
    }
  }
}

object UiModelComparator : DiffUtil.ItemCallback<UiModel>() {
  override fun areItemsTheSame(
    oldItem: UiModel,
    newItem: UiModel
  ): Boolean {
    val isSameRepoItem = oldItem is UiModel.UserModel
      && newItem is UiModel.UserModel
      && oldItem.id == newItem.id

    val isSameSeparatorItem = oldItem is UiModel.SeparatorModel
      && newItem is UiModel.SeparatorModel
      && oldItem.description == newItem.description

    return isSameRepoItem || isSameSeparatorItem
  }

  override fun areContentsTheSame(
    oldItem: UiModel,
    newItem: UiModel
  ) = oldItem == newItem
}

Java

class UiModelAdapter extends PagingDataAdapter<UiModel, RecyclerView.ViewHolder> {
  UiModelAdapter() {
    super(new UiModelComparator(), Dispatchers.getMain(),
      Dispatchers.getDefault());
  }

  @NonNull
  @Override
  public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
    int viewType) {
    if (viewType == R.layout.item) {
      return new UserModelViewHolder(parent);
    } else {
      return new SeparatorModelViewHolder(parent);
    }
  }

  @Override
  public int getItemViewType(int position) {
    // Use peek over getItem to avoid triggering page fetch / drops, since
    // recycling views is not indicative of the user's current scroll position.
    UiModel item = peek(position);
    if (item instanceof UiModel.UserModel) {
      return R.layout.item;
    } else if (item instanceof UiModel.SeparatorModel) {
      return R.layout.separator_item;
    } else {
      throw new IllegalStateException("Unknown view");
    }
  }

  @Override
  public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder,
    int position) {
    if (holder instanceOf UserModelViewHolder) {
      UserModel userModel = (UserModel) getItem(position);
      ((UserModelViewHolder) holder).bind(userModel);
    } else {
      SeparatorModel separatorModel = (SeparatorModel) getItem(position);
      ((SeparatorModelViewHolder) holder).bind(separatorModel);
    }
  }
}

class UiModelComparator extends DiffUtil.ItemCallback<UiModel> {
  @Override
  public boolean areItemsTheSame(@NonNull UiModel oldItem,
    @NonNull UiModel newItem) {
    boolean isSameRepoItem = oldItem instanceof UserModel
      && newItem instanceof UserModel
      && ((UserModel) oldItem).getId().equals(((UserModel) newItem).getId());

    boolean isSameSeparatorItem = oldItem instanceof SeparatorModel
      && newItem instanceof SeparatorModel
      && ((SeparatorModel) oldItem).getDescription().equals(
      ((SeparatorModel) newItem).getDescription());

    return isSameRepoItem || isSameSeparatorItem;
  }

  @Override
  public boolean areContentsTheSame(@NonNull UiModel oldItem,
    @NonNull UiModel newItem) {
    return oldItem.equals(newItem);
  }
}

Java

class UiModelAdapter extends PagingDataAdapter<UiModel, RecyclerView.ViewHolder> {
  UiModelAdapter() {
    super(new UiModelComparator(), Dispatchers.getMain(),
      Dispatchers.getDefault());
  }

  @NonNull
  @Override
  public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
    int viewType) {
    if (viewType == R.layout.item) {
      return new UserModelViewHolder(parent);
    } else {
      return new SeparatorModelViewHolder(parent);
    }
  }

  @Override
  public int getItemViewType(int position) {
    // Use peek over getItem to avoid triggering page fetch / drops, since
    // recycling views is not indicative of the user's current scroll position.
    UiModel item = peek(position);
    if (item instanceof UiModel.UserModel) {
      return R.layout.item;
    } else if (item instanceof UiModel.SeparatorModel) {
      return R.layout.separator_item;
    } else {
      throw new IllegalStateException("Unknown view");
    }
  }

  @Override
  public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder,
    int position) {
    if (holder instanceOf UserModelViewHolder) {
      UserModel userModel = (UserModel) getItem(position);
      ((UserModelViewHolder) holder).bind(userModel);
    } else {
      SeparatorModel separatorModel = (SeparatorModel) getItem(position);
      ((SeparatorModelViewHolder) holder).bind(separatorModel);
    }
  }
}

class UiModelComparator extends DiffUtil.ItemCallback<UiModel> {
  @Override
  public boolean areItemsTheSame(@NonNull UiModel oldItem,
    @NonNull UiModel newItem) {
    boolean isSameRepoItem = oldItem instanceof UserModel
      && newItem instanceof UserModel
      && ((UserModel) oldItem).getId().equals(((UserModel) newItem).getId());

    boolean isSameSeparatorItem = oldItem instanceof SeparatorModel
      && newItem instanceof SeparatorModel
      && ((SeparatorModel) oldItem).getDescription().equals(
      ((SeparatorModel) newItem).getDescription());

    return isSameRepoItem || isSameSeparatorItem;
  }

  @Override
  public boolean areContentsTheSame(@NonNull UiModel oldItem,
    @NonNull UiModel newItem) {
    return oldItem.equals(newItem);
  }
}

Tính năng 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 đó trong bộ nhớ đệm. Do đó, cachedIn() phải là lệnh gọi cuối cùng trong trình ViewModel.

Kotlin

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

Java

// CoroutineScope helper provided by the lifecycle-viewmodel-ktx artifact.
CoroutineScope viewModelScope = ViewModelKt.getViewModelScope(viewModel);
PagingRx.cachedIn(
  // Type is Flowable<PagingData<User>>.
  PagingRx.getFlowable(pager)
    .map(pagingData -> pagingData
      .filter(user -> !user.isHiddenFromUi())
      .map(UiModel.UserModel::new)),
  viewModelScope);
}

Java

// CoroutineScope helper provided by the lifecycle-viewmodel-ktx artifact.
CoroutineScope viewModelScope = ViewModelKt.getViewModelScope(viewModel);
PagingLiveData.cachedIn(
  Transformations.map(
    // Type is LiveData<PagingData<User>>.
    PagingLiveData.getLiveData(pager),
    pagingData -> pagingData
      .filter(user -> !user.isHiddenFromUi())
      .map(UiModel.UserModel::new)),
  viewModelScope);

Để biết thêm thông tin về cách sử dụng cachedIn() với luồng PagingData, hãy xem phần 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:

Lớp học lập trình