From 3faa0cb9db07de50298d40cef89da74f28d2f0af Mon Sep 17 00:00:00 2001 From: Gavriil Sitnikov Date: Thu, 18 Feb 2016 17:08:22 +0300 Subject: [PATCH] providers changed --- .../components/listing/ItemsProvider.java | 26 +-- .../components/listing/ListProvider.java | 3 +- .../listing/SimplePagingProvider.java | 217 ++++++++++++++++++ .../adapters/AbstractItemsAdapter.java | 61 +++-- 4 files changed, 272 insertions(+), 35 deletions(-) create mode 100644 src/main/java/org/roboswag/components/listing/SimplePagingProvider.java diff --git a/src/main/java/org/roboswag/components/listing/ItemsProvider.java b/src/main/java/org/roboswag/components/listing/ItemsProvider.java index 90d9a37..2dc3354 100644 --- a/src/main/java/org/roboswag/components/listing/ItemsProvider.java +++ b/src/main/java/org/roboswag/components/listing/ItemsProvider.java @@ -35,10 +35,10 @@ import rx.subjects.PublishSubject; */ public abstract class ItemsProvider { - private final PublishSubject> listChangesSubject = PublishSubject.create(); + private final PublishSubject> listChangesSubject = PublishSubject.create(); - protected void notifyChanges(@NonNull final List listChanges) { - listChangesSubject.onNext(listChanges); + protected void notifyChanges(@NonNull final List changes) { + listChangesSubject.onNext(changes); } @Nullable @@ -49,16 +49,16 @@ public abstract class ItemsProvider { public abstract int getSize(); @SuppressWarnings("unchecked") - public Observable> loadRange(int first, int last) { + public Observable> loadRange(final int first, final int last) { final List>> itemsRequests = new ArrayList<>(); - int i = first; - while (i <= last) { + int index = first; + while (index <= last) { final List> 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 resultPart = new ArrayList<>(args.length); @@ -79,18 +79,18 @@ public abstract class ItemsProvider { } @NonNull - public Observable> observeListChanges() { + public Observable> 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; diff --git a/src/main/java/org/roboswag/components/listing/ListProvider.java b/src/main/java/org/roboswag/components/listing/ListProvider.java index 5fbc7a6..bd37446 100644 --- a/src/main/java/org/roboswag/components/listing/ListProvider.java +++ b/src/main/java/org/roboswag/components/listing/ListProvider.java @@ -38,6 +38,7 @@ public class ListProvider extends ItemsProvider { private final List items; public ListProvider(@NonNull final Collection collection) { + super(); items = Collections.unmodifiableList(new ArrayList<>(collection)); } @@ -49,7 +50,7 @@ public class ListProvider extends ItemsProvider { @Override public Observable loadItem(final int position) { - return Observable.just(items.get(position)); + return Observable.just(items.size() > position ? items.get(position) : null); } @Override diff --git a/src/main/java/org/roboswag/components/listing/SimplePagingProvider.java b/src/main/java/org/roboswag/components/listing/SimplePagingProvider.java new file mode 100644 index 0000000..1d775a9 --- /dev/null +++ b/src/main/java/org/roboswag/components/listing/SimplePagingProvider.java @@ -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 extends ItemsProvider { + + 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> loadedPages = new SparseArray<>(); + @Nullable + private Integer maxLoadedPage; + private boolean isLastPageLoaded; + private final SparseArray>> loadingPages = new SparseArray<>(); + @NonNull + private final PagesProvider pagesProvider; + + public SimplePagingProvider(@NonNull final PagesProvider pagesProvider) { + this(pagesProvider, DEFAULT_PAGE_SIZE); + } + + public SimplePagingProvider(@NonNull final PagesProvider 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 page = loadedPages.get(pageIndexOf(position)); + final int indexOnPage = indexOnPage(position); + return page != null && page.size() > indexOnPage ? page.get(indexOnPage) : null; + } + } + + @NonNull + public Observable loadItem(final int position) { + final int indexOfPage = pageIndexOf(position); + final int indexOnPage = indexOnPage(position); + return Observable.create(subscriber -> { + final List 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> 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 pageItems, final int indexOfPage) { + return (indexOfPage == 0 || !pageItems.isEmpty()) && (maxLoadedPage == null || maxLoadedPage < indexOfPage); + } + + @NonNull + private Observable> 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 pageItems, final int oldSize, final boolean oldIsLastPageLoaded) { + final List 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 validatePageItems(@NonNull final Collection 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 { + + @NonNull + Observable> loadPage(int offset, int limit); + + } + +} diff --git a/src/main/java/org/roboswag/components/listing/adapters/AbstractItemsAdapter.java b/src/main/java/org/roboswag/components/listing/adapters/AbstractItemsAdapter.java index 32d304a..5bf0f0d 100644 --- a/src/main/java/org/roboswag/components/listing/adapters/AbstractItemsAdapter.java +++ b/src/main/java/org/roboswag/components/listing/adapters/AbstractItemsAdapter.java @@ -48,8 +48,6 @@ import rx.functions.Actions; public abstract class AbstractItemsAdapter extends RecyclerView.Adapter { - 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 onItemClickListener; @Nullable private ItemsProvider itemsProvider; + @Nullable + private Subscription itemsProviderSubscription; public void setItems(@NonNull final List items) { setItemsProvider(new ListProvider<>(items)); } + protected int itemsOffset() { + return 0; + } + public void setItemsProvider(@NonNull final ItemsProvider 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 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 onItemClickListener) { @@ -116,11 +131,13 @@ public abstract class AbstractItemsAdapter { //TODO: fix multitap @@ -179,7 +196,9 @@ public abstract class AbstractItemsAdapter {