Przekształcanie strumieni danych

Gdy pracujesz z danymi z podziałem na strony, często musisz przekształcać strumień danych podczas jego wczytywania. Konieczne może być np. przefiltrowanie listy elementów lub przekonwertowanie elementów na inny typ przed ich wyświetleniem w interfejsie. Kolejnym typowym przypadkiem użycia przekształcenia strumienia danych jest dodanie separatorów list.

Ogólnie rzecz biorąc, stosowanie przekształceń bezpośrednio do strumienia danych pozwala oddzielić konstrukcje repozytorium i elementy interfejsu użytkownika.

Zakładamy na tej stronie, że znasz podstawowe sposoby korzystania z biblioteki stronicowania.

Zastosuj podstawowe przekształcenia

Zasób PagingData jest zamknięty w strumieniu reaktywnym, więc możesz stosować operacje przekształcania danych stopniowo między wczytaniem danych a ich prezentacją.

Aby zastosować przekształcenia do każdego obiektu PagingData w strumieniu, umieść je wewnątrz operacji map() w strumieniu:

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

Konwertuj dane

Podstawowa operacja na strumieniu danych to przekształcenie go na inny typ. Po uzyskaniu dostępu do obiektu PagingData możesz wykonać operację map() na każdym elemencie listy z podziałem na strony w obiekcie PagingData.

Jednym z typowych przypadków użycia jest mapowanie obiektu warstwy sieci lub bazy danych na obiekt używany specjalnie w warstwie interfejsu użytkownika. Poniższy przykład pokazuje, jak zastosować ten rodzaj operacji na mapie:

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

Kolejna typowa konwersja danych polega na konwertowaniu danych wejściowych użytkownika (np. ciągu zapytania) na dane wyjściowe żądania w celu ich wyświetlenia. Skonfigurowanie tej funkcji wymaga nasłuchiwania i przechwytywania zapytań użytkownika, wykonania żądania oraz przesłania wyniku zapytania z powrotem do interfejsu.

Możesz nasłuchiwać danych wejściowych zapytania, korzystając z interfejsu stream API. Zachowaj odniesienie do transmisji w ViewModel. Warstwa interfejsu nie powinna mieć do niej bezpośredniego dostępu. Zamiast tego zdefiniuj funkcję powiadamiającą obiekt ViewModel o zapytaniu użytkownika.

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

Gdy wartość zapytania zmieni się w strumieniu danych, możesz wykonać operacje, aby przekonwertować wartość zapytania na odpowiedni typ danych i zwrócić wynik do warstwy interfejsu użytkownika. Konkretna funkcja konwersji zależy od użytego języka i platformy, ale wszystkie mają podobne funkcje.

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

Użycie operacji takich jak flatMapLatest lub switchMap zapewnia, że do interfejsu będą zwracane tylko najnowsze wyniki. Jeśli użytkownik zmieni dane wejściowe zapytania przed zakończeniem operacji na bazie danych, operacje te odrzucają wyniki starego zapytania i natychmiast uruchamiają nowe wyszukiwanie.

Filtruj dane

Inną często wykonywaną funkcją jest filtrowanie. Możesz filtrować dane według kryteriów określonych przez użytkownika lub usuwać je z interfejsu, jeśli powinny być ukryte na podstawie innych kryteriów.

Te operacje filtra musisz umieścić w wywołaniu map(), ponieważ filtr dotyczy obiektu PagingData. Po odfiltrowaniu danych z PagingData nowe wystąpienie PagingData jest przekazywane do warstwy interfejsu w celu wyświetlenia.

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

Dodaj separatory list

Biblioteka stronicowania obsługuje dynamiczne separatory list. Możesz poprawić czytelność listy, wstawiając separatory bezpośrednio do strumienia danych jako elementy listy RecyclerView. W efekcie separatory są w pełni funkcjonalnymi obiektami ViewHolder, co umożliwia interakcję, koncentrację na ułatwieniach dostępu i wszystkie inne funkcje zapewniane przez View.

Aby wstawić separatory do listy stron, należy wykonać 3 czynności:

  1. Przekonwertuj model interfejsu użytkownika, aby uwzględnić elementy separatora.
  2. Przekształć strumień danych, aby dynamicznie dodawać separatory między wczytaniem danych a ich prezentacją.
  3. Zaktualizuj interfejs użytkownika, aby obsługiwał elementy separatorów.

Konwertowanie modelu UI

Biblioteka stronicowania wstawia separatory list do elementu RecyclerView jako rzeczywiste elementy listy. Elementy te muszą jednak być odróżnialne od elementów danych na liście, aby mogły zostać powiązane z innym typem ViewHolder w odrębnym interfejsie. Rozwiązaniem jest utworzenie zabezpieczonej klasy Kotlin z podklasami do reprezentowania Twoich danych i separatorów. Możesz też utworzyć klasę bazową rozszerzoną o klasę elementu listy i klasę separatora.

Załóżmy, że chcesz dodać separatory do podzielonej na strony listy elementów (User). Poniższy fragment kodu pokazuje, jak utworzyć klasę bazową, w której instancje mogą być typu UserModel lub 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;
    }
  }
}

Przekształcanie strumienia danych

Przekształcenia musisz zastosować do strumienia danych po jego wczytaniu, a przed jego zaprezentowaniem. Przekształcenia powinny działać w ten sposób:

  • Przekonwertuj załadowane elementy listy na nowy podstawowy typ elementu.
  • Aby dodać separatory, użyj metody PagingData.insertSeparators().

Więcej informacji o operacjach przekształcania znajdziesz w artykule Stosowanie podstawowych przekształceń.

Ten przykład przedstawia operacje przekształcania mające na celu zaktualizowanie strumienia PagingData<User> do strumienia PagingData<UiModel> z dodanymi separatorami:

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

Obsługa separatorów w interfejsie

Ostatnim krokiem jest dostosowanie interfejsu użytkownika do typu elementu separatora. Utwórz układ i stojak widoków dla elementów separatora i zmień dostosowanie listy, tak aby używał RecyclerView.ViewHolder jako typu uchwytu, co umożliwi obsługę więcej niż jednego typu uchwytu. Możesz też zdefiniować wspólną klasę bazową, która obejmuje zarówno klasy elementu widoku elementu, jak i separatora.

Musisz też wprowadzić te zmiany w adapterze listy:

  • Dodaj wielkość liter do metod onCreateViewHolder() i onBindViewHolder(), aby uwzględnić elementy listy separatorów.
  • Zaimplementować nowy komparator.

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

Unikanie powielania pracy

Dlatego jednym z najważniejszych problemów, których należy unikać, jest to, że aplikacja wykonuje niepotrzebną pracę. Pobieranie danych jest kosztowne, a przekształcanie danych może zajmować dużo czasu. Gdy dane zostaną wczytane i przygotowane do wyświetlania w interfejsie, należy je zapisać na wypadek zmiany konfiguracji i konieczności odtworzenia interfejsu.

Operacja cachedIn() zapisuje w pamięci podręcznej wyniki wszystkich przekształceń, które miały miejsce przed nią. Dlatego wywołanie cachedIn() powinno być ostatnim wywołaniem w modelu 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);

Więcej informacji o użyciu interfejsu cachedIn() ze strumieniem PagingData znajdziesz w sekcji dotyczącej konfigurowania strumienia danych PagingData.

Dodatkowe materiały

Więcej informacji o bibliotece stronicowania znajdziesz w tych dodatkowych materiałach:

Ćwiczenia z programowania

Próbki