Przekształcanie strumieni danych (widoków)

Pojęcia i implementacja w Jetpack Compose

Podczas pracy ze stronnicowanymi danymi często trzeba przekształcać strumień danych podczas jego wczytywania. Może być na przykład konieczne odfiltrowanie listy elementów lub przekonwertowanie ich na inny typ przed wyświetleniem w interfejsie. Innym typowym zastosowaniem przekształcania strumienia danych jest dodawanie separatorów list.

Ogólnie rzecz biorąc, stosowanie przekształceń bezpośrednio w strumieniu danych pozwala zachować oddzielenie konstrukcji repozytorium i konstrukcji interfejsu.

Na tej stronie zakładamy, że znasz podstawowe zastosowania biblioteki Paging.

Stosowanie podstawowych przekształceń

Ponieważ PagingData jest zamknięty w strumieniu reaktywnym, możesz stopniowo stosować operacje przekształcania danych między ich wczytaniem a wyświetleniem.

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

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

Najbardziej podstawową operacją na strumieniu danych jest przekształcenie go w inny typ. Gdy uzyskasz dostęp do obiektu PagingData, możesz wykonać operację map() na każdym poszczególnym elemencie na stronie listy w obiekcie PagingData.

Jednym z typowych zastosowań jest mapowanie obiektu warstwy sieci lub bazy danych na obiekt używany w warstwie interfejsu. Poniższy przykład pokazuje, jak zastosować ten typ operacji mapowania:

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 typowym przykładem konwersji danych jest pobieranie danych wejściowych od użytkownika, np. ciągu zapytania, i przekształcanie ich w dane wyjściowe żądania, które mają być wyświetlane. Skonfigurowanie tego wymaga nasłuchiwania i przechwytywania danych wejściowych zapytania użytkownika, wykonania żądania i przesłania wyniku zapytania z powrotem do interfejsu.

Możesz nasłuchiwać danych wejściowych zapytania za pomocą interfejsu API strumienia. Zachowaj odniesienie do strumienia w ViewModel. Warstwa interfejsu nie powinna mieć do niej bezpośredniego dostępu. Zamiast tego zdefiniuj funkcję, która powiadomi ViewModel o zapytaniu użytkownika.

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 w strumieniu danych ulegnie zmianie, możesz wykonać operacje, aby przekonwertować ją na odpowiedni typ danych i zwrócić wynik do warstwy interfejsu. Konkretna funkcja konwersji zależy od używanego języka i platformy, ale wszystkie zapewniają podobną funkcjonalność.

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 gwarantuje, że do interfejsu zostaną zwrócone tylko najnowsze wyniki. Jeśli użytkownik zmieni dane wejściowe zapytania przed zakończeniem operacji na bazie danych, te operacje odrzucą wyniki starego zapytania i natychmiast uruchomią nowe wyszukiwanie.

Filtrowanie danych

Inną powszechną operacją jest filtrowanie. Dane możesz filtrować na podstawie kryteriów podanych przez użytkownika lub usuwać je z interfejsu, jeśli powinny być ukryte na podstawie innych kryteriów.

Operacje filtrowania 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, aby ją wyświetlić.

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 Paging obsługuje dynamiczne separatory list. Możesz poprawić czytelność listy, wstawiając separatory bezpośrednio do strumienia danych jako RecyclerViewelementy listy. W związku z tym separatory są w pełni funkcjonalnymi obiektamiViewHolder, które umożliwiają interakcję, ustawianie fokusu ułatwień dostępu i korzystanie ze wszystkich innych funkcji zapewnianych przez element View.

Aby wstawić separatory na listę podzieloną na strony, wykonaj te 3 czynności:

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

Konwertowanie modelu interfejsu

Biblioteka Paging wstawia separatory listy do elementu RecyclerView jako rzeczywiste elementy listy, ale elementy separatora muszą być odróżnialne od elementów danych na liście, aby można było powiązać je z innym typem ViewHolder z odrębnym interfejsem. Rozwiązaniem jest utworzenie klasy zapieczętowanej w języku Kotlin z podklasami reprezentującymi dane i separatory. Możesz też utworzyć klasę bazową, która będzie rozszerzana przez klasę elementu listy i klasę separatora.

Załóżmy, że chcesz dodać separatory do stronicowanej listy User elementów. Poniższy fragment kodu pokazuje, jak utworzyć klasę bazową, której instancje mogą być typu UserModel lub SeparatorModel:

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 wyświetleniem. Przekształcenia powinny wykonywać te czynności:

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

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

W przykładzie poniżej pokazujemy operacje przekształcania, które aktualizują strumień PagingData<User> do strumienia PagingData<UiModel> z dodanymi separatorami:

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, aby uwzględnić typ produktu separatora. Utwórz układ i uchwyt widoku dla elementów separatora i zmień adapter listy, aby używał RecyclerView.ViewHolder jako typu uchwytu widoku, dzięki czemu będzie mógł obsługiwać więcej niż 1 typ uchwytu widoku. Możesz też zdefiniować wspólną klasę bazową, którą rozszerzają klasy elementów i separatorów.

Musisz też wprowadzić te zmiany w adapterze listy:

  • Dodaj przypadki do metod onCreateViewHolder()onBindViewHolder(), aby uwzględnić elementy listy separatorów.
  • Wdróż nowy komparator.

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

Jednym z głównych problemów, których należy unikać, jest wykonywanie przez aplikację niepotrzebnej pracy. Pobieranie danych jest kosztowną operacją, a przekształcanie danych może również zająć dużo czasu. Po załadowaniu danych i przygotowaniu ich do wyświetlenia w interfejsie należy je zapisać na wypadek zmiany konfiguracji i konieczności ponownego utworzenia interfejsu.

Operacja cachedIn() buforuje wyniki wszystkich przekształceń, które występują przed nią. Dlatego cachedIn() powinno być ostatnim wywołaniem w Twoim ViewModelu.

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 cachedIn() ze strumieniem PagingData znajdziesz w artykule Konfigurowanie strumienia PagingData.

Dodatkowe materiały

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

Codelabs