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