Transformasi aliran data

Saat bekerja dengan data yang di-page, aliran data sering kali perlu ditransformasi saat Anda memuatnya. Misalnya, Anda mungkin perlu memfilter daftar item atau mengonversi item menjadi jenis yang berbeda sebelum menyajikannya dalam UI. Kasus penggunaan umum lainnya terkait transformasi aliran data adalah menambahkan pemisah daftar.

Secara lebih umum, penerapan transformasi ke aliran data secara langsung akan memungkinkan Anda memisahkan konstruksi repositori dan UI.

Halaman ini mengasumsikan bahwa Anda telah memahami penggunaan dasar library Paging.

Menerapkan transformasi dasar

Karena PagingData dienkapsulasi dalam aliran reaktif, Anda dapat menerapkan operasi transformasi pada data secara bertahap antara proses pemuatan dan penyajian data.

Untuk menerapkan transformasi ke setiap objek PagingData di dalam aliran, tempatkan transformasi di dalam operasi map() di aliran tersebut:

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

Mengonversi data

Operasi paling dasar pada aliran data adalah mengonversinya ke jenis lain. Setelah memiliki akses ke objek PagingData, Anda dapat menjalankan operasi map() pada setiap item individual dalam daftar yang di-page dalam objek PagingData.

Salah satu kasus penggunaan umum untuk hal ini adalah memetakan objek lapisan database atau jaringan ke objek yang secara khusus digunakan di lapisan UI. Contoh di bawah menunjukkan cara menerapkan jenis operasi peta ini:

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

Konversi data umum lainnya adalah menggunakan input dari pengguna, seperti string kueri, dan mengonversinya menjadi output permintaan untuk ditampilkan. Untuk menyiapkannya, Anda harus memproses dan merekam input kueri pengguna, melakukan permintaannya, dan mengirimkan hasil kuerinya kembali ke UI.

Anda dapat memproses input kueri menggunakan API aliran data. Simpan referensi aliran data dalam ViewModel. Lapisan UI tidak boleh memiliki akses langsung ke input tersebut; sebagai gantinya, tetapkan fungsi untuk memberi tahu ViewModel tentang kueri pengguna.

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

Saat nilai kueri berubah dalam aliran data, Anda dapat menjalankan operasi untuk mengonversi nilai kueri ke jenis data yang diinginkan dan menampilkan hasilnya ke lapisan UI. Fungsi konversi khusus bergantung pada bahasa dan framework yang digunakan, tetapi semuanya menyediakan fungsi yang serupa.

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)
);

Menggunakan operasi seperti flatMapLatest atau switchMap memastikan bahwa hanya hasil terbaru yang akan ditampilkan ke UI. Jika pengguna mengubah input kueri sebelum operasi database selesai, operasi ini akan membuang hasil dari kueri lama dan segera meluncurkan penelusuran baru.

Memfilter data

Operasi umum lainnya adalah pemfilteran. Anda dapat memfilter data berdasarkan kriteria dari pengguna, atau Anda dapat menghapus data dari UI jika data harus disembunyikan berdasarkan kriteria lain.

Anda harus menempatkan operasi filter ini dalam panggilan map() karena filternya berlaku untuk objek PagingData. Setelah data difilter dari PagingData, instance PagingData baru akan diteruskan ke lapisan UI untuk ditampilkan.

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

Menambahkan pemisah daftar

Library Paging mendukung pemisah daftar dinamis. Anda dapat meningkatkan keterbacaan daftar dengan memasukkan pemisah secara langsung ke aliran data sebagai item daftar RecyclerView. Akibatnya, pemisah menjadi objek ViewHolder berfitur lengkap, yang memungkinkan interaktivitas, fokus aksesibilitas, serta semua fitur lainnya yang disediakan oleh View.

Ada tiga langkah saat Anda memasukan separator ke dalam daftar yang di-page:

  1. Mengonversi model UI untuk mengakomodasi item pemisah.
  2. Mentransformasi aliran data untuk menambahkan pemisah antara proses pemuatan dan penyajian data secara dinamis.
  3. Mengupdate UI untuk menangani item pemisah.

Mengonversi model UI

Library Paging akan memasukkan pemisah daftar ke dalam RecyclerView sebagai item daftar yang sebenarnya, tetapi item pemisah harus dapat dibedakan dari item data dalam daftar agar dapat diikat ke jenis ViewHolder yang berbeda dengan UI berbeda. Solusinya adalah dengan membuat class terbatas dalam Kotlin dengan subclass untuk mewakili data dan pemisah Anda. Selain itu, Anda juga dapat membuat class dasar yang diperpanjang oleh class item daftar dan class pemisah.

Anggaplah Anda ingin menambahkan pemisah ke daftar item User yang di-page. Cuplikan berikut menunjukkan cara pembuatan class dasar saat instancenya berupa UserModel atau 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;
    }
  }
}

Mentransformasi aliran data

Anda harus melakukan transformasi aliran data setelah memuat dan sebelum menampilkannya. Transformasi harus melakukan hal berikut:

  • Mengonversi item daftar yang dimuat untuk merefleksikan jenis item dasar yang baru.
  • Menggunakan metode PagingData.insertSeparators() untuk menambahkan pemisah.

Untuk mempelajari operasi transformasi lebih lanjut, lihat Menerapkan transformasi dasar.

Contoh berikut menunjukkan operasi transformasi untuk memperbarui aliran PagingData<User> menjadi aliran PagingData<UiModel> dengan pemisah yang ditambahkan:

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

Menangani pemisah di UI

Langkah terakhir adalah mengubah UI agar mengakomodasi jenis item pemisah. Buat tata letak dan penampung tampilan untuk item pemisah Anda, lalu ubah adaptor daftar agar menggunakan RecyclerView.ViewHolder sebagai jenis penampung tampilannya sehingga dapat menangani lebih dari satu jenis penampung tampilan. Selain itu, Anda dapat menentukan class dasar umum yang diperluas class penunjang tampilan pemisah dan item.

Anda juga harus membuat perubahan berikut pada adaptor daftar:

  • Menambahkan kasus ke metode onCreateViewHolder() dan onBindViewHolder() untuk menjelaskan item daftar pemisah.
  • Mengimplementasikan pembanding baru.

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

Menghindari tugas duplikat

Satu masalah utama yang harus dihindari adalah membuat aplikasi melakukan tugas yang tidak perlu. Pengambilan data adalah operasi yang mahal, dan transformasi data juga dapat menghabiskan waktu yang berharga. Setelah dimuat dan disiapkan untuk ditampilkan di UI, data harus disimpan jika perubahan konfigurasi terjadi dan UI harus dibuat ulang.

Operasi cachedIn() akan meng-cache hasil transformasi apa pun yang terjadi sebelumnya. Oleh karena itu, cachedIn() harus menjadi panggilan terakhir dalam ViewModel Anda.

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);

Untuk informasi selengkapnya tentang cara menggunakan cachedIn() dengan aliran data PagingData, lihat Menyiapkan aliran data PagingData.

Referensi lainnya

Untuk mempelajari library Paging lebih lanjut, lihat referensi tambahan berikut:

Codelab