package com.mikepenz.fastadapter_extensions.scroll;
import android.support.annotation.NonNull;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.LayoutManager;
import android.view.View;
import com.mikepenz.fastadapter.IItem;
import com.mikepenz.fastadapter.IItemAdapter;
import com.mikepenz.fastadapter.adapters.FooterAdapter;
import com.mikepenz.fastadapter.adapters.GenericItemAdapter;
import com.mikepenz.fastadapter.utils.Function;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
import static android.support.v7.widget.com_mikepenz_fastadapter_extensions_scroll.postOnRecyclerView;
/**
* This is an extension of {@link EndlessRecyclerOnScrollListener}, providing a more powerful API
* for endless scrolling.
* This class exposes 2 callbacks to separate the loading logic from delivering the results:
* <ul>
* <li>{@link OnLoadMoreHandler OnLoadMoreHandler}</li>
* <li>{@link OnNewItemsListener OnNewItemsListener}</li>
* </ul>
* This class also takes care of other various stuffs like:
* <ul>
* <li>Ensuring the results are delivered on the RecyclerView's handler – which also ensures
* that the results are delivered only when the RecyclerView is attached to the window, see {@link
* View#post(Runnable)}.</li>
* <li>Prevention of memory leaks when implemented properly (i.e. {@link OnLoadMoreHandler
* OnLoadMoreHandler} should be implemented via static classes or lambda expressions).</li>
* <li>An easier way to deliver results to an {@link #withNewItemsDeliveredTo(IItemAdapter,
* Function) IItemAdapter} or {@link #withNewItemsDeliveredTo(GenericItemAdapter)
* GenericItemAdapter}.</li>
* </ul>
* Created by jayson on 3/26/2016.
*/
public class EndlessScrollHelper<Model> extends EndlessRecyclerOnScrollListener {
private OnLoadMoreHandler<Model> mOnLoadMoreHandler;
private OnNewItemsListener<Model> mOnNewItemsListener;
public EndlessScrollHelper() {
}
public EndlessScrollHelper(LayoutManager layoutManager) {
super(layoutManager);
}
public EndlessScrollHelper(LayoutManager layoutManager, int visibleThreshold) {
super(layoutManager, visibleThreshold);
}
public EndlessScrollHelper(LayoutManager layoutManager, int visibleThreshold, FooterAdapter footerAdapter) {
super(layoutManager, visibleThreshold, footerAdapter);
}
public EndlessScrollHelper<Model> addTo(RecyclerView recyclerView) {
recyclerView.addOnScrollListener(this);
return this;
}
/**
* A callback interface provided by the {@link EndlessScrollHelper} where
* {@link #onLoadMore(ResultReceiver, int) onLoadMore()} results are to be delivered.
* The underlying implementation is safe to use by any background-thread, as long as only 1
* thread is using it. Results delivered via {@link #deliverNewItems(List)} are automatically
* dispatched to the RecyclerView's message queue (i.e. to be delivered in the ui thread).
*
* @param <Model>
*/
public interface ResultReceiver<Model> {
/**
* @return the current page where the results will be delivered.
*/
int getReceiverPage();
/**
* Delivers the result of an {@link #onLoadMore(ResultReceiver, int) onLoadMore()} for the
* current {@linkplain #getReceiverPage() page}. This method must be called only once.
*
* @param result the result of an {@link #onLoadMore(ResultReceiver, int) onLoadMore()}
* @return whether results where delivered successfully or not, possibly because the
* RecyclerView is no longer attached or the {@link EndlessScrollHelper} is no longer
* in use (and it has been garbage collected).
* @throws IllegalStateException when more than one results are delivered.
*/
boolean deliverNewItems(@NonNull List<Model> result);
}
public interface OnLoadMoreHandler<Model> {
/**
* Handles loading of the specified page and delivers the results to the specified
* {@link ResultReceiver}.
*
* @param out
* @param currentPage
*/
void onLoadMore(@NonNull ResultReceiver<Model> out, int currentPage);
}
public interface OnNewItemsListener<Model> {
/**
* Called on the RecyclerView's message queue to receive the results of a previous
* {@link #onLoadMore(ResultReceiver, int) onLoadMore()}.
*
* @param newItems
* @param page
*/
void onNewItems(@NonNull List<Model> newItems, int page);
}
/**
* Define the {@link OnLoadMoreHandler OnLoadMoreHandler} which will be used for loading new
* items.
*
* @param onLoadMoreHandler
* @return
*/
public EndlessScrollHelper<Model> withOnLoadMoreHandler(@NonNull OnLoadMoreHandler<Model> onLoadMoreHandler) {
mOnLoadMoreHandler = onLoadMoreHandler;
return this;
}
/**
* Define the {@link OnNewItemsListener OnNewItemsListener} which will receive the new items
* loaded by {@link #onLoadMore(ResultReceiver, int) onLoadMore()}.
*
* @param onNewItemsListener
* @return
* @see #withNewItemsDeliveredTo(IItemAdapter, Function) withNewItemsDeliveredTo(IItemAdapter, Function)
* @see #withNewItemsDeliveredTo(GenericItemAdapter) withNewItemsDeliveredTo(GenericItemAdapter)
*/
public EndlessScrollHelper<Model> withOnNewItemsListener(@NonNull OnNewItemsListener<Model> onNewItemsListener) {
mOnNewItemsListener = onNewItemsListener;
return this;
}
/**
* Registers an {@link OnNewItemsListener OnNewItemsListener} that delivers results to the
* specified {@link IItemAdapter}. Converting each result to an {@link IItem} using the given
* {@code itemFactory}.
*
* @param itemAdapter
* @param itemFactory
* @param <Item>
* @return
* @see #withNewItemsDeliveredTo(IItemAdapter, Function, OnNewItemsListener) withNewItemsDeliveredTo(IItemAdapter, Function, OnNewItemsListener)
*/
public <Item extends IItem> EndlessScrollHelper<Model> withNewItemsDeliveredTo(@NonNull IItemAdapter<Item> itemAdapter, @NonNull Function<Model, Item> itemFactory) {
mOnNewItemsListener = new DeliverToIItemAdapter<>(itemAdapter, itemFactory);
return this;
}
/**
* Registers an {@link OnNewItemsListener OnNewItemsListener} that delivers results to the
* specified {@link GenericItemAdapter} through its {@link GenericItemAdapter#addModel} method.
*
* @param genericItemAdapter
* @return
* @see #withNewItemsDeliveredTo(GenericItemAdapter, OnNewItemsListener) withNewItemsDeliveredTo(GenericItemAdapter, OnNewItemsListener)
*/
public EndlessScrollHelper<Model> withNewItemsDeliveredTo(@NonNull GenericItemAdapter<Model, ?> genericItemAdapter) {
mOnNewItemsListener = new DeliverToGenericItemAdapter<>(genericItemAdapter);
return this;
}
/**
* An overload of {@link #withNewItemsDeliveredTo(IItemAdapter, Function) withNewItemsDeliveredTo()}
* that allows additional callbacks.
*
* @param itemAdapter
* @param itemFactory
* @param extraOnNewItemsListener
* @param <Item>
* @return
*/
public <Item extends IItem> EndlessScrollHelper<Model> withNewItemsDeliveredTo(@NonNull IItemAdapter<Item> itemAdapter, @NonNull Function<Model, Item> itemFactory, @NonNull OnNewItemsListener<Model> extraOnNewItemsListener) {
mOnNewItemsListener = new DeliverToIItemAdapter2<>(itemAdapter, itemFactory, extraOnNewItemsListener);
return this;
}
/**
* An overload of {@link #withNewItemsDeliveredTo(GenericItemAdapter) withNewItemsDeliveredTo()}
* that allows additional callbacks.
*
* @param genericItemAdapter
* @param extraOnNewItemsListener
* @return
*/
public EndlessScrollHelper<Model> withNewItemsDeliveredTo(@NonNull GenericItemAdapter<Model, ?> genericItemAdapter, @NonNull OnNewItemsListener<Model> extraOnNewItemsListener) {
mOnNewItemsListener = new DeliverToGenericItemAdapter2<>(genericItemAdapter, extraOnNewItemsListener);
return this;
}
//-------------------------
//-------------------------
//Override-able methods
//-------------------------
//-------------------------
/**
* The default implementation takes care of calling the previously set
* {@link OnLoadMoreHandler OnLoadMoreHandler}.
*
* @param out
* @param currentPage
* @see #withOnLoadMoreHandler(OnLoadMoreHandler) withOnLoadMoreHandler(OnLoadMoreHandler)
*/
protected void onLoadMore(@NonNull ResultReceiver<Model> out, int currentPage) {
OnLoadMoreHandler<Model> loadMoreHandler = this.mOnLoadMoreHandler;
try {
loadMoreHandler.onLoadMore(out, currentPage);
} catch (NullPointerException npe) {
// Lazy null checking! If this was our npe, then throw with an appropriate message.
throw loadMoreHandler != null ? npe
: new NullPointerException("You must provide an `OnLoadMoreHandler`");
}
}
/**
* The default implementation takes care of calling the previously set
* {@link OnNewItemsListener OnNewItemsListener}.
*
* @param newItems
* @param page
* @see #withOnNewItemsListener(OnNewItemsListener) withOnNewItemsListener(OnNewItemsListener)
*/
protected void onNewItems(@NonNull List<Model> newItems, int page) {
OnNewItemsListener<Model> onNewItemsListener = this.mOnNewItemsListener;
try {
onNewItemsListener.onNewItems(newItems, page);
} catch (NullPointerException npe) {
// Lazy null checking! If this was our npe, then throw with an appropriate message.
throw onNewItemsListener != null ? npe
: new NullPointerException("You must provide an `OnNewItemsListener`");
}
}
//-------------------------
//-------------------------
//Internal stuff
//-------------------------
//-------------------------
@Override
public void onLoadMore(int currentPage) {
onLoadMore(new ResultReceiverImpl<>(this, currentPage), currentPage);
}
private static final class ResultReceiverImpl<Model> extends WeakReference<EndlessScrollHelper<Model>> implements ResultReceiver<Model>, Runnable {
private final int mReceiverPage;
private EndlessScrollHelper<Model> mHelperStrongRef;
private List<Model> mResult;
ResultReceiverImpl(EndlessScrollHelper<Model> helper, int receiverPage) {
super(helper); // We use WeakReferences to outer class to avoid memory leaks.
mReceiverPage = receiverPage;
}
@Override
public int getReceiverPage() {
return mReceiverPage;
}
@Override
public boolean deliverNewItems(@NonNull List<Model> result) {
if (mResult != null) // We might also see `null` here if more than 1 thread is modifying this.
throw new IllegalStateException("`result` already provided!");
mResult = result;
mHelperStrongRef = super.get();
return mHelperStrongRef != null
&& postOnRecyclerView(mHelperStrongRef.getLayoutManager(), this);
}
@Override
public void run() {
// At this point, mHelperStrongRef != null
try {
if (mHelperStrongRef.getCurrentPage() != mReceiverPage) {
// throw new IllegalStateException("Inconsistent state! "
// + "Page might have already been loaded! "
// + "Or `loadMore(result)` might have been used by more than 1 thread!");
return; // Let it fail and possibly load correctly
}
} catch (NullPointerException npe) {
if (mHelperStrongRef == null) {
throw new AssertionError(npe);
}
throw npe;
}
mHelperStrongRef.onNewItems(mResult, mReceiverPage);
}
}
//-----------------------------------------
//-----------------------------------------
//`withNewItemsDeliveredTo()` stuff
//-----------------------------------------
//-----------------------------------------
private static class DeliverToIItemAdapter<Model, Item extends IItem> implements OnNewItemsListener<Model> {
@NonNull
private final IItemAdapter<Item> mItemAdapter;
@NonNull
private final Function<Model, Item> mItemFactory;
DeliverToIItemAdapter(@NonNull IItemAdapter<Item> itemAdapter, @NonNull Function<Model, Item> itemFactory) {
mItemAdapter = itemAdapter;
mItemFactory = itemFactory;
}
@Override
public void onNewItems(@NonNull List<Model> newItems, int page) {
int size = newItems.size();
List<Item> iitems = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
iitems.add(mItemFactory.apply(newItems.get(i)));
}
mItemAdapter.add(iitems);
}
}
private static class DeliverToGenericItemAdapter<Model> implements OnNewItemsListener<Model> {
@NonNull
private final GenericItemAdapter<Model, ?> mGenericItemAdapter;
DeliverToGenericItemAdapter(@NonNull GenericItemAdapter<Model, ?> genericItemAdapter) {
mGenericItemAdapter = genericItemAdapter;
}
@Override
public void onNewItems(@NonNull List<Model> newItems, int page) {
mGenericItemAdapter.addModel(newItems);
}
}
private static class DeliverToIItemAdapter2<Model, Item extends IItem> extends DeliverToIItemAdapter<Model, Item> {
@NonNull
private final OnNewItemsListener<Model> mExtraOnNewItemsListener;
DeliverToIItemAdapter2(@NonNull IItemAdapter<Item> itemAdapter, @NonNull Function<Model, Item> itemFactory, @NonNull OnNewItemsListener<Model> extraOnNewItemsListener) {
super(itemAdapter, itemFactory);
mExtraOnNewItemsListener = extraOnNewItemsListener;
}
@Override
public void onNewItems(@NonNull List<Model> newItems, int page) {
mExtraOnNewItemsListener.onNewItems(newItems, page);
super.onNewItems(newItems, page);
}
}
private static class DeliverToGenericItemAdapter2<Model> extends DeliverToGenericItemAdapter<Model> {
@NonNull
private final OnNewItemsListener<Model> mExtraOnNewItemsListener;
DeliverToGenericItemAdapter2(@NonNull GenericItemAdapter<Model, ?> genericItemAdapter, @NonNull OnNewItemsListener<Model> extraOnNewItemsListener) {
super(genericItemAdapter);
mExtraOnNewItemsListener = extraOnNewItemsListener;
}
@Override
public void onNewItems(@NonNull List<Model> newItems, int page) {
mExtraOnNewItemsListener.onNewItems(newItems, page);
super.onNewItems(newItems, page);
}
}
}