Skip to content

Instantly share code, notes, and snippets.

@balysv
Last active August 29, 2015 14:01
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save balysv/e1aa80a9f6471ebf6cb7 to your computer and use it in GitHub Desktop.
Save balysv/e1aa80a9f6471ebf6cb7 to your computer and use it in GitHub Desktop.
Endless Collection adapter using RxJava and Bindable pattern for Android
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);
}
}
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