providers changed

This commit is contained in:
Gavriil Sitnikov 2016-02-18 17:08:22 +03:00
parent d9729f1373
commit 3faa0cb9db
4 changed files with 272 additions and 35 deletions

View File

@ -35,10 +35,10 @@ import rx.subjects.PublishSubject;
*/
public abstract class ItemsProvider<T> {
private final PublishSubject<List<ListChange>> listChangesSubject = PublishSubject.create();
private final PublishSubject<List<Change>> listChangesSubject = PublishSubject.create();
protected void notifyChanges(@NonNull final List<ListChange> listChanges) {
listChangesSubject.onNext(listChanges);
protected void notifyChanges(@NonNull final List<Change> changes) {
listChangesSubject.onNext(changes);
}
@Nullable
@ -49,16 +49,16 @@ public abstract class ItemsProvider<T> {
public abstract int getSize();
@SuppressWarnings("unchecked")
public Observable<List<T>> loadRange(int first, int last) {
public Observable<List<T>> loadRange(final int first, final int last) {
final List<Observable<List<T>>> itemsRequests = new ArrayList<>();
int i = first;
while (i <= last) {
int index = first;
while (index <= last) {
final List<Observable<T>> limitedPageRequests = new ArrayList<>();
final int maxIndex = i + RxRingBuffer.SIZE - 1;
while (i <= Math.min(last, maxIndex)) {
limitedPageRequests.add(loadItem(i));
i++;
final int maxIndex = index + RxRingBuffer.SIZE - 1;
while (index <= Math.min(last, maxIndex)) {
limitedPageRequests.add(loadItem(index));
index++;
}
itemsRequests.add(Observable.combineLatest(limitedPageRequests, args -> {
final List<T> resultPart = new ArrayList<>(args.length);
@ -79,18 +79,18 @@ public abstract class ItemsProvider<T> {
}
@NonNull
public Observable<List<ListChange>> observeListChanges() {
public Observable<List<Change>> observeListChanges() {
return listChangesSubject;
}
public static class ListChange {
public static class Change {
@NonNull
private final Type type;
private final int start;
private final int count;
public ListChange(final @NonNull Type type, final int start, final int count) {
public Change(@NonNull final Type type, final int start, final int count) {
this.type = type;
this.start = start;
this.count = count;

View File

@ -38,6 +38,7 @@ public class ListProvider<T> extends ItemsProvider<T> {
private final List<T> items;
public ListProvider(@NonNull final Collection<T> collection) {
super();
items = Collections.unmodifiableList(new ArrayList<>(collection));
}
@ -49,7 +50,7 @@ public class ListProvider<T> extends ItemsProvider<T> {
@Override
public Observable<T> loadItem(final int position) {
return Observable.just(items.get(position));
return Observable.just(items.size() > position ? items.get(position) : null);
}
@Override

View File

@ -0,0 +1,217 @@
/*
* Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov)
*
* This file is part of RoboSwag library.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.roboswag.components.listing;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.SparseArray;
import org.roboswag.core.log.Lc;
import org.roboswag.core.utils.android.RxAndroidUtils;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import rx.Observable;
import rx.Scheduler;
/**
* Created by Gavriil Sitnikov on 07/12/2015.
* TODO: fill description
*/
public class SimplePagingProvider<T> extends ItemsProvider<T> {
public static final int DEFAULT_PAGE_SIZE = 25;
private final Scheduler scheduler = RxAndroidUtils.createLooperScheduler();
private final Object lock = new Object();
private final int pageSize;
private final SparseArray<List<T>> loadedPages = new SparseArray<>();
@Nullable
private Integer maxLoadedPage;
private boolean isLastPageLoaded;
private final SparseArray<Observable<List<T>>> loadingPages = new SparseArray<>();
@NonNull
private final PagesProvider<T> pagesProvider;
public SimplePagingProvider(@NonNull final PagesProvider<T> pagesProvider) {
this(pagesProvider, DEFAULT_PAGE_SIZE);
}
public SimplePagingProvider(@NonNull final PagesProvider<T> pagesProvider, final int pageSize) {
super();
this.pagesProvider = pagesProvider;
this.pageSize = pageSize;
}
public int getPageSize() {
return pageSize;
}
@Override
public int getSize() {
synchronized (lock) {
return (maxLoadedPage != null ? maxLoadedPage * pageSize + loadedPages.get(maxLoadedPage).size() : 0)
+ (isLastPageLoaded ? 0 : 1);
}
}
private int pageIndexOf(final int position) {
return position / pageSize;
}
private int indexOnPage(final int position) {
return position % pageSize;
}
@Nullable
@Override
public T getItem(final int position) {
synchronized (lock) {
final List<T> page = loadedPages.get(pageIndexOf(position));
final int indexOnPage = indexOnPage(position);
return page != null && page.size() > indexOnPage ? page.get(indexOnPage) : null;
}
}
@NonNull
public Observable<T> loadItem(final int position) {
final int indexOfPage = pageIndexOf(position);
final int indexOnPage = indexOnPage(position);
return Observable.<T>create(subscriber -> {
final List<T> page = loadedPages.get(indexOfPage);
subscriber.onNext(page != null && page.size() > indexOnPage ? page.get(indexOnPage) : null);
subscriber.onCompleted();
}).switchMap(item -> {
if (item != null || (isLastPageLoaded && maxLoadedPage != null && maxLoadedPage <= indexOfPage)) {
return Observable.just(item);
}
Observable<List<T>> loadingPage = loadingPages.get(indexOfPage);
if (loadingPage == null) {
loadingPage = createPageLoadingObservable(indexOfPage);
loadingPages.put(indexOfPage, loadingPage);
}
return loadingPage.map(page -> {
int index = 0;
for (final T nextItem : page) {
if (index == indexOnPage) {
return nextItem;
}
index++;
}
return null;
});
}).subscribeOn(scheduler);
}
private boolean shouldReplaceMaxLoaded(@NonNull final List<T> pageItems, final int indexOfPage) {
return (indexOfPage == 0 || !pageItems.isEmpty()) && (maxLoadedPage == null || maxLoadedPage < indexOfPage);
}
@NonNull
private Observable<List<T>> createPageLoadingObservable(final int indexOfPage) {
return pagesProvider
.loadPage(indexOfPage * pageSize, pageSize)
.observeOn(scheduler)
.map(this::validatePageItems)
.doOnNext(pageItems -> {
synchronized (lock) {
final int oldSize = getSize();
final boolean oldIsLastPageLoaded = isLastPageLoaded;
if (pageItems.size() < pageSize) {
if (maxLoadedPage != null && maxLoadedPage > indexOfPage) {
maxLoadedPage = indexOfPage == 0 || !pageItems.isEmpty() ? indexOfPage : null;
downgradeMaxLoadedPages(indexOfPage);
isLastPageLoaded = false;
}
if (shouldReplaceMaxLoaded(pageItems, indexOfPage)) {
maxLoadedPage = indexOfPage;
}
isLastPageLoaded = isLastPageLoaded
|| (maxLoadedPage != null
&& (maxLoadedPage == indexOfPage || maxLoadedPage == indexOfPage - 1));
} else if (shouldReplaceMaxLoaded(pageItems, indexOfPage)) {
maxLoadedPage = indexOfPage;
}
if (indexOfPage == 0 || !pageItems.isEmpty()) {
loadedPages.put(indexOfPage, pageItems);
}
loadingPages.remove(indexOfPage);
updateItemsChanges(indexOfPage, pageItems, oldSize, oldIsLastPageLoaded);
}
}).replay(1)
.refCount();
}
private void downgradeMaxLoadedPages(final int maximum) {
for (int i = 0; i <= loadedPages.size(); i++) {
final int key = loadedPages.keyAt(i);
if (key > maximum || loadedPages.get(key).size() < pageSize) {
loadedPages.remove(key);
} else if (maxLoadedPage == null || maxLoadedPage < key) {
maxLoadedPage = key;
}
}
}
private void updateItemsChanges(final int indexOfPage, @NonNull final List<T> pageItems, final int oldSize, final boolean oldIsLastPageLoaded) {
final List<Change> changes = new ArrayList<>();
final int size = getSize();
if (size == oldSize) {
if (!pageItems.isEmpty()) {
changes.add(new Change(Change.Type.CHANGED, indexOfPage * pageSize, pageItems.size()));
}
} else if (size > oldSize) {
if (!oldIsLastPageLoaded) {
changes.add(new Change(Change.Type.CHANGED, oldSize - 1, 1));
}
changes.add(new Change(Change.Type.INSERTED, oldSize, size - oldSize));
} else {
changes.add(new Change(Change.Type.REMOVED, size, oldSize - size));
if (!isLastPageLoaded) {
changes.add(new Change(Change.Type.CHANGED, size - 1, 1));
}
}
if (!changes.isEmpty()) {
notifyChanges(changes);
}
}
@NonNull
private List<T> validatePageItems(@NonNull final Collection<T> pageItems) {
if (pageItems.size() > pageSize) {
Lc.assertion("Unexpectedly big pageItems: " + pageItems.size() + '/' + pageSize);
return Collections.unmodifiableList(new ArrayList<>(pageItems).subList(0, pageSize));
} else {
return Collections.unmodifiableList(new ArrayList<>(pageItems));
}
}
public interface PagesProvider<T> {
@NonNull
Observable<Collection<T>> loadPage(int offset, int limit);
}
}

View File

@ -48,8 +48,6 @@ import rx.functions.Actions;
public abstract class AbstractItemsAdapter<TItem, TViewHolder extends RecyclerView.ViewHolder>
extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private static final int PRE_LOADING_COUNT = 2;
private static final int LOADED_ITEM_TYPE = R.id.LOADED_ITEM_TYPE;
private static final int NOT_LOADED_ITEM_TYPE = R.id.NOT_LOADED_ITEM_TYPE;
@ -58,31 +56,48 @@ public abstract class AbstractItemsAdapter<TItem, TViewHolder extends RecyclerVi
private OnItemClickListener<TItem> onItemClickListener;
@Nullable
private ItemsProvider<TItem> itemsProvider;
@Nullable
private Subscription itemsProviderSubscription;
public void setItems(@NonNull final List<TItem> items) {
setItemsProvider(new ListProvider<>(items));
}
protected int itemsOffset() {
return 0;
}
public void setItemsProvider(@NonNull final ItemsProvider<TItem> itemsProvider) {
if (itemsProviderSubscription != null) {
itemsProviderSubscription.unsubscribe();
itemsProviderSubscription = null;
}
this.itemsProvider = itemsProvider;
notifyDataSetChanged();
itemsProvider.observeListChanges()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(listChanges -> {
for (final ItemsProvider.ListChange listChange : listChanges) {
switch (listChange.getType()) {
case INSERTED:
notifyItemRangeInserted(listChange.getStart(), listChange.getCount());
break;
case CHANGED:
notifyItemRangeChanged(listChange.getStart(), listChange.getCount());
break;
case REMOVED:
notifyItemRangeRemoved(listChange.getStart(), listChange.getCount());
break;
}
}
});
if (this.itemsProvider != null) {
itemsProviderSubscription = itemsProvider.observeListChanges()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::onItemsChanged);
}
}
protected void onItemsChanged(@NonNull final List<ItemsProvider.Change> changes) {
for (final ItemsProvider.Change change : changes) {
switch (change.getType()) {
case INSERTED:
notifyItemRangeInserted(change.getStart() + itemsOffset(), change.getCount());
break;
case CHANGED:
notifyItemRangeChanged(change.getStart() + itemsOffset(), change.getCount());
break;
case REMOVED:
notifyItemRangeRemoved(change.getStart() + itemsOffset(), change.getCount());
break;
default:
Lc.assertion("Not supported " + change.getType());
break;
}
}
}
public void setOnItemClickListener(@Nullable final OnItemClickListener<TItem> onItemClickListener) {
@ -116,11 +131,13 @@ public abstract class AbstractItemsAdapter<TItem, TViewHolder extends RecyclerVi
}
((NotLoadedItemViewHolder) holder).bindItem(position, itemsProvider);
} else {
if (item == null) {
if (item == null || itemsProvider == null) {
Lc.assertion(new ShouldNotHappenException("Item at" + position + " should not be null"));
return;
}
onBindItemToViewHolder((TViewHolder) holder, position, item);
itemsProvider.loadRange(Math.max(0, position - itemsProvider.getSize() / 2), position + itemsProvider.getSize() / 2).first()
.subscribe(Actions.empty(), Actions.empty());
if (onItemClickListener != null && !isOnClickListenerDisabled(item)) {
holder.itemView.setOnClickListener(v -> {
//TODO: fix multitap
@ -179,7 +196,9 @@ public abstract class AbstractItemsAdapter<TItem, TViewHolder extends RecyclerVi
}
retryButton.setVisibility(View.INVISIBLE);
progressBar.setVisibility(View.VISIBLE);
subscription = itemsProvider.loadRange(Math.max(0, position - PRE_LOADING_COUNT), position + PRE_LOADING_COUNT)
subscription = itemsProvider
.loadRange(Math.max(0, position - itemsProvider.getSize() / 2), position + itemsProvider.getSize() / 2)
.first()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(Actions.empty(),
throwable -> {