package com.airbnb.epoxy; import android.os.Bundle; import android.support.annotation.CallSuper; import android.support.annotation.Nullable; import android.support.v7.widget.GridLayoutManager.SpanSizeLookup; import android.support.v7.widget.RecyclerView; import android.view.View; import android.view.ViewGroup; import java.util.Collections; import java.util.List; abstract class BaseEpoxyAdapter extends RecyclerView.Adapter<EpoxyViewHolder> { private static final String SAVED_STATE_ARG_VIEW_HOLDERS = "saved_state_view_holders"; private int spanCount = 1; private final ViewTypeManager viewTypeManager = new ViewTypeManager(); /** * Keeps track of view holders that are currently bound so we can save their state in {@link * #onSaveInstanceState(Bundle)}. */ private final BoundViewHolders boundViewHolders = new BoundViewHolders(); private ViewHolderState viewHolderState = new ViewHolderState(); private final SpanSizeLookup spanSizeLookup = new SpanSizeLookup() { @Override public int getSpanSize(int position) { try { return getModelForPosition(position).getSpanSize(spanCount, position, getItemCount()); } catch (IndexOutOfBoundsException e) { // There seems to be a GridLayoutManager bug where when the user is in accessibility mode // it incorrectly uses an outdated view position // when calling this method. This crashes when a view is animating out, when it is // removed from the adapter but technically still added // to the layout. We've posted a bug report and hopefully can update when the support // library fixes this // TODO: (eli_hart 8/23/16) Figure out if this has been fixed in new support library onExceptionSwallowed(e); return 1; } } }; public BaseEpoxyAdapter() { // Defaults to stable ids since view models generate unique ids. Set this to false in the // subclass if you don't want to support it setHasStableIds(true); spanSizeLookup.setSpanIndexCacheEnabled(true); } /** * This is called when recoverable exceptions happen at runtime. They can be ignored and Epoxy * will recover, but you can override this to be aware of when they happen. */ protected void onExceptionSwallowed(RuntimeException exception) { } @Override public int getItemCount() { return getCurrentModels().size(); } /** Return the models currently being used by the adapter to populate the recyclerview. */ abstract List<EpoxyModel<?>> getCurrentModels(); public boolean isEmpty() { return getCurrentModels().isEmpty(); } @Override public EpoxyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { EpoxyModel<?> model = viewTypeManager.getModelForViewType(this, viewType); View view = model.buildView(parent); return new EpoxyViewHolder(view); } @Override public void onBindViewHolder(EpoxyViewHolder holder, int position) { onBindViewHolder(holder, position, Collections.emptyList()); } @Override public void onBindViewHolder(EpoxyViewHolder holder, int position, List<Object> payloads) { // A ViewHolder can be bound again even it is already bound and showing, like when it is on // screen and is changed. In this case we need // to carry the state of the previous view over to the new view. This may not be necessary if // the viewholder is reused (see RecyclerView.ItemAnimator#canReuseUpdatedViewHolder) // but we don't rely on that to be safe and to simplify EpoxyViewHolder boundViewHolder = boundViewHolders.get(holder); if (boundViewHolder != null) { viewHolderState.save(boundViewHolder); } EpoxyModel<?> modelToShow = getModelForPosition(position); EpoxyModel<?> previouslyBoundModel = null; if (diffPayloadsEnabled()) { previouslyBoundModel = DiffPayload.getModelFromPayload(payloads, getItemId(position)); } holder.bind(modelToShow, previouslyBoundModel, payloads, position); viewHolderState.restore(holder); boundViewHolders.put(holder); if (diffPayloadsEnabled()) { onModelBound(holder, modelToShow, position, previouslyBoundModel); } else { onModelBound(holder, modelToShow, position, payloads); } } boolean diffPayloadsEnabled() { return false; } /** * Called immediately after a model is bound to a view holder. Subclasses can override this if * they want alerts on when a model is bound. */ protected void onModelBound(EpoxyViewHolder holder, EpoxyModel<?> model, int position, @Nullable List<Object> payloads) { onModelBound(holder, model, position); } void onModelBound(EpoxyViewHolder holder, EpoxyModel<?> model, int position, @Nullable EpoxyModel<?> previouslyBoundModel) { onModelBound(holder, model, position); } /** * Called immediately after a model is bound to a view holder. Subclasses can override this if * they want alerts on when a model is bound. */ protected void onModelBound(EpoxyViewHolder holder, EpoxyModel<?> model, int position) { } /** * Returns an object that manages the view holders currently bound to the RecyclerView. This * object is mainly used by the base Epoxy adapter to save view states, but you may find it useful * to help access views or models currently shown in the RecyclerView. */ protected BoundViewHolders getBoundViewHolders() { return boundViewHolders; } @Override public int getItemViewType(int position) { return viewTypeManager.getViewType(getModelForPosition(position)); } @Override public long getItemId(int position) { // This does not call getModelForPosition so that we don't use the id of the empty model when // hidden, // so that the id stays constant when gone vs shown return getCurrentModels().get(position).id(); } EpoxyModel<?> getModelForPosition(int position) { return getCurrentModels().get(position); } @Override public void onViewRecycled(EpoxyViewHolder holder) { viewHolderState.save(holder); boundViewHolders.remove(holder); EpoxyModel<?> model = holder.getModel(); holder.unbind(); onModelUnbound(holder, model); } /** * Called immediately after a model is unbound from a view holder. Subclasses can override this if * they want alerts on when a model is unbound. */ protected void onModelUnbound(EpoxyViewHolder holder, EpoxyModel<?> model) { } @CallSuper @Override public boolean onFailedToRecycleView(EpoxyViewHolder holder) { //noinspection unchecked,rawtypes return ((EpoxyModel) holder.getModel()).onFailedToRecycleView(holder.objectToBind()); } @CallSuper @Override public void onViewAttachedToWindow(EpoxyViewHolder holder) { //noinspection unchecked,rawtypes ((EpoxyModel) holder.getModel()).onViewAttachedToWindow(holder.objectToBind()); } @CallSuper @Override public void onViewDetachedFromWindow(EpoxyViewHolder holder) { //noinspection unchecked,rawtypes ((EpoxyModel) holder.getModel()).onViewDetachedFromWindow(holder.objectToBind()); } public void onSaveInstanceState(Bundle outState) { // Save the state of currently bound views first so they are included. Views that were // scrolled off and unbound will already have had // their state saved. for (EpoxyViewHolder holder : boundViewHolders) { viewHolderState.save(holder); } if (viewHolderState.size() > 0 && !hasStableIds()) { throw new IllegalStateException("Must have stable ids when saving view holder state"); } outState.putParcelable(SAVED_STATE_ARG_VIEW_HOLDERS, viewHolderState); } public void onRestoreInstanceState(@Nullable Bundle inState) { // To simplify things we enforce that state is restored before views are bound, otherwise it // is more difficult to update view state once they are bound if (boundViewHolders.size() > 0) { throw new IllegalStateException( "State cannot be restored once views have been bound. It should be done before adding " + "the adapter to the recycler view."); } if (inState != null) { viewHolderState = inState.getParcelable(SAVED_STATE_ARG_VIEW_HOLDERS); if (viewHolderState == null) { throw new IllegalStateException( "Tried to restore instance state, but onSaveInstanceState was never called."); } } } /** * Finds the position of the given model in the list. Doesn't use indexOf to avoid unnecessary * equals() calls since we're looking for the same object instance. * * @return The position of the given model in the current models list, or -1 if the model can't be * found. */ protected int getModelPosition(EpoxyModel<?> model) { int size = getCurrentModels().size(); for (int i = 0; i < size; i++) { if (model == getCurrentModels().get(i)) { return i; } } return -1; } /** * For use with a grid layout manager - use this to get the {@link SpanSizeLookup} for models in * this adapter. This will delegate span look up calls to each model's {@link * EpoxyModel#getSpanSize(int, int, int)}. Make sure to also call {@link #setSpanCount(int)} so * the span count is correct. */ public SpanSizeLookup getSpanSizeLookup() { return spanSizeLookup; } /** * If you are using a grid layout manager you must call this to set the span count of the grid. * This span count will be passed on to the models so models can choose what span count to be. * * @see #getSpanSizeLookup() * @see EpoxyModel#getSpanSize(int, int, int) */ public void setSpanCount(int spanCount) { this.spanCount = spanCount; } public int getSpanCount() { return spanCount; } public boolean isMultiSpan() { return spanCount > 1; } }