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:
- Przekształć model interfejsu, aby uwzględnić elementy separatora.
- Przekształć strumień danych, aby dynamicznie dodawać separatory między wczytywaniem danych a ich prezentowaniem.
- 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()ionBindViewHolder(), 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
Polecane dla Ciebie
- Uwaga: tekst linku jest wyświetlany, gdy język JavaScript jest wyłączony.
- Wczytywanie i wyświetlanie danych podzielonych na strony
- Testowanie implementacji biblioteki Paging
- Zarządzanie stanami wczytywania i ich prezentowanie