Przekształcanie strumieni danych

Podczas pracy z danymi po stronie często trzeba przekształcić strumień danych podczas ich wczytywania. Możesz np. filtrować listę elementów lub konwertować elementy na inny typ, zanim wyświetlisz je w interfejsie. Innym typowym przypadkiem użycia transformacji strumienia danych jest dodawanie rozdzielaczy list.

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

Na tej stronie zakładamy, że znasz podstawy korzystania z biblioteki Paging.

Stosowanie podstawowych przekształceń

Funkcja PagingData jest umieszczona w strumieniu reaktywnym, więc możesz stosować operacje przekształcania na danych stopniowo, między wczytywaniem a prezentowaniem.

Aby zastosować przekształcenia do każdego obiektu PagingData w strumieniu, umieść je w 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.
  });

Konwertowanie danych

Najprostszą operacją na strumieniu danych jest jego konwertowanie na inny typ. Po uzyskaniu dostępu do obiektu PagingData możesz wykonać operację map() na każdym elemencie listy stron w obiekcie PagingData.

Jednym z popularnych zastosowań jest mapowanie obiektu warstwy sieci lub bazy danych na obiekt używany w warstwie interfejsu użytkownika. Przykład poniżej pokazuje, jak zastosować tego typu operację mapowania:

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

Innym częstym przykładem konwersji danych jest przekształcenie danych wejściowych użytkownika, np. ciągu zapytania, w dane wyjściowe do wyświetlenia. Aby to skonfigurować, musisz nasłuchiwać i przechwytywać dane wejściowe zapytania użytkownika, wykonywać żądanie i przesyłać wynik zapytania z powrotem do interfejsu.

Możesz słuchać danych wejściowych zapytania za pomocą interfejsu Stream API. Zachowaj odwołanie do strumienia w ViewModel. Warstwy interfejsu użytkownika nie powinny mieć bezpośredniego dostępu do tej funkcji. Zamiast tego zdefiniuj funkcję, która powiadomi 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 potoku 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 języka i ramy, 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)
);

Wykonywanie operacji takich jak flatMapLatest lub switchMap zapewnia, że do interfejsu zwracane są tylko najnowsze wyniki. Jeśli użytkownik zmieni dane wejściowe zapytania przed zakończeniem operacji na bazie danych, operacje te odrzucą wyniki starego zapytania i natychmiast rozpoczną nowe wyszukiwanie.

Filtrowanie danych

Inną typową operacją jest filtrowanie. Możesz filtrować dane na podstawie kryteriów, a także usuwać dane z interfejsu użytkownika, jeśli mają być ukryte na podstawie innych kryteriów.

Te operacje filtrowania musisz umieścić w wywołaniu funkcji map(), ponieważ filtr jest stosowany do obiektu PagingData. Gdy dane zostaną odfiltrowane z poziomu PagingData, nowa instancja PagingData zostanie przekazana do warstwy interfejsu użytkownika 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())
)

Dodawanie separatorów list

Biblioteka stronowania obsługuje dynamiczne rozdzielacze list. Możesz ulepszyć czytelność listy, wstawiając separatory bezpośrednio w strumień danych jako elementy listy RecyclerView. W związku z tym separatory to pełnowartościowe obiekty ViewHolder, które umożliwiają interaktywność, ułatwienia dostępu i wszystkie inne funkcje dostępne w komponencie View.

Aby wstawić separatory do listy podzielonej na strony:

  1. Przekształć model interfejsu, aby uwzględnić elementy separatora.
  2. Przekształcaj strumień danych, aby dynamicznie dodawać separatory między wczytywaniem danych a ich prezentowaniem.
  3. Zaktualizuj interfejs użytkownika, aby obsługiwał elementy separatora.

Konwertowanie modelu interfejsu

Biblioteka stronowania wstawia rozdzielacze listy do RecyclerView jako rzeczywiste elementy listy, ale elementy rozdzielcze muszą być odróżnialne od elementów danych na liście, aby można je było powiązać z innym typem ViewHolder z odrębnym interfejsem. Rozwiązaniem jest utworzenie zapieczętowanej klasy Kotlin z podklasami, które będą reprezentować dane i rozdzielacze. Możesz też utworzyć klasę podstawową, która jest rozszerzona o klasę elementu listy i klasę separatora.

Załóżmy, że chcesz dodać separatory do listy User elementów na stronach. Ten fragment kodu pokazuje, jak utworzyć klasę podstawową, której instancje mogą być albo 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

Przekształcenia musisz zastosować do strumienia danych po jego załadowaniu, ale przed jego przedstawieniem. Transformacje powinny:

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

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

Ten przykład pokazuje operacje przekształcania, które służą do aktualizowania strumienia PagingData<User> na strumień 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 zmiana interfejsu użytkownika, aby uwzględnić typ elementu separatora. Utwórz układ i element widoku dla elementów separatora, a następnie zmień adapter listy, aby używać typu elementu widoku RecyclerView.ViewHolder, dzięki czemu będzie on obsługiwać więcej niż 1 typ elementu widoku. Możesz też zdefiniować wspólną klasę bazową, z której będą korzystać klasy elementu i separatora widoku.

Musisz też wprowadzić te zmiany w adapterze listy:

  • Dodaj do metod onCreateViewHolder()onBindViewHolder() przypadki uwzględniające elementy listy rozdzielników.
  • Wdrożyć 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);
  }
}

Unikaj powielania pracy

Jednym z kluczowych problemów, których należy unikać, jest niepotrzebna praca aplikacji. Pobieranie danych to kosztowna operacja, a przekształcanie danych może też pochłaniać cenny czas. Po załadowaniu danych i przygotowaniu ich do wyświetlania w interfejsie należy je zapisać na wypadek, gdyby nastąpiła zmiana konfiguracji i trzeba było ponownie utworzyć interfejs.

Operacja cachedIn() przechowuje w pamięci podręcznej wyniki wszystkich przekształceń, które występują przed nią. Dlatego cachedIn() powinien być ostatnim wywołaniem w Twoim 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żywaniu funkcji cachedIn() w przypadku strumienia PagingData znajdziesz w artykule Konfigurowanie strumienia danych typu PagingData.

Dodatkowe materiały

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

Ćwiczenia z programowania