Last active
August 29, 2015 14:01
-
-
Save balysv/e1aa80a9f6471ebf6cb7 to your computer and use it in GitHub Desktop.
Endless Collection adapter using RxJava and Bindable pattern for Android
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import android.content.Context; | |
import android.view.LayoutInflater; | |
import android.view.View; | |
import android.view.ViewGroup; | |
import android.widget.BaseAdapter; | |
import java.util.Collection; | |
import java.util.concurrent.atomic.AtomicInteger; | |
import rx.Observable; | |
import rx.Observer; | |
import rx.Subscription; | |
import timber.log.Timber; | |
/** | |
* An implementation of a {@link BaseAdapter} that uses the new "Bindable" pattern. It also | |
* observes data changes using RxJava when the end of this adapter is reached and appends them to | |
* an associated {@link Collection}. | |
* TODO: a version using some adapter wrapper | |
* | |
* @param <S> {@link android.widget.BaseAdapter} item data type | |
* @param <V> data used in an {@link rx.Observer}. Usually a {@link java.util.List} of type {@link S} | |
*/ | |
public abstract class EndlessBindableAdapter<S, V> extends BaseAdapter implements Observer<V> { | |
/** | |
* If true {@link #registerNewSubscriber()} will be called when end of | |
* adapter is reached. | |
* | |
* @return true if more data needs to be loaded | |
*/ | |
public abstract boolean shouldLoadMore(); | |
/** | |
* Create an {@link rx.Observable} that will be observed by | |
* this adapter | |
*/ | |
public abstract Observable<V> registerNewSubscriber(); | |
/** | |
* @return {@link Collection} associated with this adapter | |
*/ | |
public abstract Collection<S> getCollection(); | |
/** | |
* Appends new items to the {@link Collection} of this adapter. Ra on UI thread | |
* | |
* @param items items published in {@link #onNext(V)} of this adapter | |
*/ | |
public abstract void appendItems(V items); | |
/** | |
* Create a new instance of a view for the specified position. | |
*/ | |
public abstract View newView(LayoutInflater inflater, int position, ViewGroup container); | |
/** | |
* Bind the data for the specified {@code position} to the view. | |
*/ | |
public abstract void bindView(S item, int position, View view); | |
/** | |
* Inflates a view to be shown at the end of the adapter when more items are loading | |
* | |
* @param inflater LayoutInflater | |
* @param container parent ViewGroup | |
* @return List item View of pending requests | |
*/ | |
public abstract View newPendingView(LayoutInflater inflater, ViewGroup container); | |
/** | |
* Keep a reference to an inflated View of pending requests to be reused if scrolled up or | |
* in case of errors | |
*/ | |
private View pendingView; | |
/** | |
* Reference to the current subscription. Only a single subscription can be observed | |
* at a time | |
*/ | |
private Subscription subscription; | |
/** | |
* Our safeguard against two identical subscriptions being spawned when {@link #getView} is | |
* being called more than once on {@link #pendingView} | |
*/ | |
private final AtomicInteger loadingPosition = new AtomicInteger(-1); | |
private final Context context; | |
private final LayoutInflater inflater; | |
public EndlessBindableAdapter(Context context) { | |
this.context = context; | |
this.inflater = LayoutInflater.from(context); | |
} | |
public Context getContext() { | |
return context; | |
} | |
/** | |
* Append data and inform adapter on subscription callback | |
* | |
* @param items new adapter data | |
*/ | |
@Override | |
public void onNext(V items) { | |
appendItems(items); | |
notifyDataSetChanged(); | |
} | |
/** | |
* could move {@link #appendItems(V)} and {@link #notifyDataSetChanged()} here if | |
* the subscription is multiple item based | |
*/ | |
@Override | |
public void onCompleted() { | |
pendingView = null; | |
subscription.unsubscribe(); | |
} | |
@Override | |
public void onError(Throwable throwable) { | |
Timber.e(throwable, "Error"); | |
pendingView = null; | |
subscription.unsubscribe(); | |
} | |
@Override | |
public final View getView(int position, View view, ViewGroup container) { | |
if (position == getCollectionSize() && shouldLoadMore()) { | |
if (pendingView == null) { | |
pendingView = newPendingView(inflater, container); | |
} | |
if (loadingPosition.get() != position) { | |
subscription = registerNewSubscriber().subscribe(this); | |
loadingPosition.set(position); | |
} | |
return pendingView; | |
} | |
if (view == null) { | |
view = newView(inflater, position, container); | |
if (view == null) { | |
throw new IllegalStateException("newView result must not be null."); | |
} | |
} | |
bindView(getItem(position), position, view); | |
return view; | |
} | |
@Override | |
public abstract S getItem(int position); | |
/** | |
* Increase adapter size if pending View is present | |
*/ | |
@Override | |
public final int getCount() { | |
if (shouldLoadMore()) { | |
return getCollectionSize() + 1; | |
} | |
return getCollectionSize(); | |
} | |
/** | |
* Ignore pending View | |
*/ | |
@Override | |
public final int getItemViewType(int position) { | |
if (position == getCollectionSize()) { | |
return IGNORE_ITEM_VIEW_TYPE; | |
} | |
return super.getItemViewType(position); | |
} | |
/** | |
* @see #getItemViewType(int) | |
*/ | |
@Override | |
public final int getViewTypeCount() { | |
return super.getViewTypeCount() + 1; | |
} | |
/** | |
* Disable pending View | |
*/ | |
@Override | |
public boolean isEnabled(int position) { | |
return position < getCollectionSize() && super.isEnabled(position); | |
} | |
/** | |
* @return size of the {@link Collection} of this adapter | |
*/ | |
protected int getCollectionSize() { | |
return getCollection().size(); | |
} | |
/** | |
* @return current subscription | |
*/ | |
public Subscription getSubscription() { | |
return subscription; | |
} | |
/** | |
* Replace items in underlying Collection | |
* | |
* @param items to replace | |
*/ | |
public void replace(Collection<S> items) { | |
loadingPosition.set(-1); | |
getCollection().clear(); | |
getCollection().addAll(items); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class ExampleAdapter extends EndlessBindableAdapter<Object, ObservableObject> { | |
private final Api api; | |
private final List<Object> objects = new ArrayList<>(); | |
private final AtomicInteger errorCount = new AtomicInteger(0); | |
private Subscription subscription; | |
private boolean canLoadMore = true; | |
public RestaurantsAdapter(Context context, Api api) { | |
super(context); | |
this.api = api; | |
} | |
@Override public boolean shouldLoadMore() { | |
return errorCount.get() <= 3 && canLoadMore | |
} | |
@Override public void registerNewSubscriber() { | |
subscription = api.getObjectCollection(null, null, null) | |
.subscribeOn(Schedulers.io()) | |
.observeOn(AndroidSchedulers.mainThread()) | |
.doOnNext(new Action1<RestaurantList>() { | |
@Override public void call(ObservableObject objectList) { | |
if (objectList.totals > 100) canLoadMore = false; | |
} | |
}) | |
.subscribe(this); | |
} | |
@Override public Collection<Object> getCollection() { | |
return objects; | |
} | |
@Override public void appendItems(ObjectList items) { | |
objects.addAll(items.objects); | |
} | |
@Override public View newPendingView(LayoutInflater inflater, ViewGroup container) { | |
return inflater.inflate(R.layout.pending_list_item, container, false); | |
} | |
@Override public View newView(LayoutInflater inflater, int position, ViewGroup container) { | |
return inflater.inflate(android.R.layout.simple_list_item_1, container, false); | |
} | |
@Override public void bindView(Object item, int position, View view) { | |
TextView tv = ButterKnife.findById(view, android.R.id.text1); | |
tv.setText(item.title); | |
} | |
@Override public Object getItem(int position) { | |
return objects.get(position); | |
} | |
@Override public long getItemId(int position) { | |
return position; | |
} | |
@Override public void onError(Throwable throwable) { | |
Timber.e(throwable, "Error"); | |
subscription.unsubscribe(); | |
errorCount.incrementAndGet(); | |
if (shouldLoadMore()) { | |
registerNewSubscriber(); | |
} | |
} | |
@Override | |
public void onCompleted() { | |
subscription.unsubscribe(); | |
errorCount.set(0); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment