Przekształcanie strumieni danych

Gdy korzystasz z funkcji Paged danych, często trzeba przekształcać strumień danych w miarę jego wczytywania. Konieczne może być na przykład filtrowanie listę elementów lub przekonwertuj elementy na inny typ przed ich wyświetleniem oraz interfejs użytkownika. Innym typowym przypadkiem użycia przekształcenia strumienia danych jest dodawanie list separatory.

Ogólnie rzecz biorąc, zastosowanie przekształceń bezpośrednio do strumienia danych pozwala aby oddzielić konstrukcje repozytorium i konstrukcje interfejsu.

Zakładamy, że znasz podstawowe korzystanie z funkcji stronicowania Biblioteka.

Zastosuj podstawowe przekształcenia

Ponieważ PagingData jest umieszczone w strumieniu reaktywnym, możesz zastosować operacje przekształcania na danych stopniowo między wczytaniem danych a ich zaprezentowaniem.

Aby zastosować przekształcenia do każdego obiektu PagingData w strumieniu, umieść przekształcenia w elemencie map(). operacji na 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

Najbardziej podstawową operacją na strumieniu danych jest konwertowanie go na inny typu. Po uzyskaniu dostępu do obiektu PagingData możesz wykonać map() na poszczególnych elementach na liście stronicowanej w elemencie PagingData obiektu.

Jednym z typowych zastosowań tego rozwiązania jest mapowanie obiektu sieci lub warstwy bazy danych na obiekt używany w warstwie interfejsu użytkownika. Poniższy przykład pokazuje, aby zastosować ten typ 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 wykorzystaniu informacji od użytkownika, np. zapytania i przekonwertować go na dane wyjściowe żądania w celu wyświetlenia. Konfiguruję wymaga wsłuchiwania i rejestrowania danych wejściowych użytkownika, i przekazywaniu wyniku zapytania z powrotem do interfejsu użytkownika.

Dane wejściowe zapytania można nasłuchiwać za pomocą interfejsu API strumienia. Zachowaj odwołanie do transmisji w: ViewModel. Warstwa interfejsu nie powinna mieć do niej bezpośredniego dostępu. zamiast zdefiniować funkcję powiadamiającą model 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 na: skonwertować wartość zapytania na odpowiedni typ danych i zwrócić wynik do interfejsu użytkownika; warstwę. Konkretna funkcja konwersji zależy od języka i platformy ale wszystkie oferują podobną funkcjonalność.

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 tylko dane najnowsze wyniki są zwracane do interfejsu. Jeśli użytkownik zmieni dane wejściowe zapytania przed zakończeniem operacji na bazie danych, te operacje odrzucają wyniki, ze starego zapytania i natychmiast rozpocząć nowe wyszukiwanie.

Filtruj dane

Inną typową operacją jest filtrowanie. Dane możesz filtrować na podstawie kryteriów użytkownika lub usunąć dane z interfejsu, jeśli powinny być ukryte według innych kryteriów.

Operacje filtra musisz umieścić w wywołaniu map(), ponieważ filtr jest stosowany do obiektu PagingData. Po odfiltrowaniu danych z PagingData, nowa instancja PagingData jest przekazywana do warstwy interfejsu użytkownika do wyświetlacz.

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 listy

Biblioteka stronnicza obsługuje dynamiczne separatory list. Możesz ulepszyć listę przez wstawienie separatorów bezpośrednio do strumienia danych jako RecyclerView elementy listy. Dzięki temu separatory są w pełni funkcjonalne, ViewHolder obiektów, włączające interaktywność, skupienie na ułatwieniach dostępu i wszystkie funkcje inne funkcje zapewniane przez View.

Wstawianie separatorów na liście stronicowanej wymaga wykonania trzech czynności:

  1. Przekonwertuj model interfejsu, aby uwzględnić elementy separatora.
  2. Przekształć strumień danych, aby dynamicznie dodawać separatory między wczytywaniem i prezentowanie ich.
  3. Zaktualizuj interfejs, aby obsługiwał elementy separatora.
.

Konwertowanie modelu interfejsu

Biblioteka stronicowania wstawia separatory listy do elementu RecyclerView jako rzeczywiste elementów listy, ale separatory muszą być odróżnialne od elementów danych na liście, aby umożliwić powiązanie z innym typem ViewHolder za pomocą parametru przez użytkownika. Rozwiązaniem jest stworzenie zabezpieczeń systemu Kotlin zajęcia z podklasami reprezentującymi dane i separatory. Ewentualnie może 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 mogą być instancje UserModel albo 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

Musisz zastosować przekształcenia do strumienia danych po jego wczytaniu, a przed za jego pomocą. Przekształcenia powinny:

  • Przekonwertuj wczytane elementy listy, aby odzwierciedlały nowy typ elementu podstawowego.
  • Aby dodać separatory, użyj metody PagingData.insertSeparators().

Aby dowiedzieć się więcej o operacjach przekształcania, zapoznaj się z sekcją Stosowanie podstawowych funkcji przekształcenia.

Przykład poniżej pokazuje operacje przekształcania, które aktualizują Strumień PagingData<User> do strumienia PagingData<UiModel> z separatorami dodano:

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 znacznik widoku dla elementów na separatory oraz zmień listę do używania adaptera RecyclerView.ViewHolder jako typu elementu widoku danych, dzięki czemu może obsługują więcej niż jeden typ słupka widoku. Możesz również zdefiniować wspólną wersję klasa bazowa, która jest rozszerzana zarówno do klas widoku elementu, jak i separatora separatora.

Musisz też wprowadzić następujące zmiany w adapterze listy:

  • Dodaj zgłoszenia do metod onCreateViewHolder() i onBindViewHolder(), aby dla elementów listy z separatorami.
  • Zaimplementuj nowy mechanizm porównawczy.

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

Unikaj powielania zadań

Jedną z kluczowych kwestii, których należy unikać, jest niepotrzebne wykonywanie przez aplikację pracy. Pobieram dane kosztowne operacje, a przekształcenia danych również mogą być czasochłonne. Po załadowaniu danych i przygotowaniu ich do wyświetlenia w interfejsie użytkownika należy je zapisać. w przypadku zmiany konfiguracji i konieczności ponownego utworzenia interfejsu użytkownika.

Operacja cachedIn() zapisuje w pamięci podręcznej wyniki wszelkich występujących przekształceń wcześniej. Dlatego 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 korzystaniu z usługi cachedIn() ze strumieniem danych PagingData znajdziesz tutaj: Skonfiguruj strumień Dane stronicowania.

Dodatkowe materiały

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

Ćwiczenia z programowania

Próbki

. .