package com.airbnb.epoxy; import android.support.annotation.Nullable; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; /** * Allows you to easily combine different view types in the same adapter, and handles view holder * creation, binding, and ids for you. Subclasses just need to add their desired {@link EpoxyModel} * objects and the rest is done automatically. * <p/> * {@link android.support.v7.widget.RecyclerView.Adapter#setHasStableIds(boolean)} is set to true by * default, since {@link EpoxyModel} makes it easy to support unique ids. If you don't want to * support this then disable it in your base class (not recommended). */ @SuppressWarnings("WeakerAccess") public abstract class EpoxyAdapter extends BaseEpoxyAdapter { private final HiddenEpoxyModel hiddenModel = new HiddenEpoxyModel(); /** * Subclasses should modify this list as necessary with the models they want to show. Subclasses * are responsible for notifying data changes whenever this list is changed. */ protected final List<EpoxyModel<?>> models = new ModelList(); private DiffHelper diffHelper; @Override List<EpoxyModel<?>> getCurrentModels() { return models; } /** * Enables support for automatically notifying model changes via {@link #notifyModelsChanged()}. * If used, this should be called in the constructor, before any models are changed. * * @see #notifyModelsChanged() */ protected void enableDiffing() { if (diffHelper != null) { throw new IllegalStateException("Diffing was already enabled"); } if (!models.isEmpty()) { throw new IllegalStateException("You must enable diffing before modifying models"); } if (!hasStableIds()) { throw new IllegalStateException("You must have stable ids to use diffing"); } diffHelper = new DiffHelper(this, false); } @Override EpoxyModel<?> getModelForPosition(int position) { EpoxyModel<?> model = models.get(position); return model.isShown() ? model : hiddenModel; } /** * Intelligently notify item changes by comparing the current {@link #models} list against the * previous so you don't have to micromanage notification calls yourself. This may be * prohibitively slow for large model lists (in the hundreds), in which case consider doing * notification calls yourself. If you use this, all your view models must implement {@link * EpoxyModel#hashCode()} and {@link EpoxyModel#equals(Object)} to completely identify their * state, so that changes to a model's content can be detected. Before using this you must enable * it with {@link #enableDiffing()}, since keeping track of the model state adds extra computation * time to all other data change notifications. * * @see #enableDiffing() */ protected void notifyModelsChanged() { if (diffHelper == null) { throw new IllegalStateException("You must enable diffing before notifying models changed"); } diffHelper.notifyModelChanges(); } /** * Notify that the given model has had its data changed. It should only be called if the model * retained the same position. */ protected void notifyModelChanged(EpoxyModel<?> model) { notifyModelChanged(model, null); } /** * Notify that the given model has had its data changed. It should only be called if the model * retained the same position. */ protected void notifyModelChanged(EpoxyModel<?> model, @Nullable Object payload) { int index = getModelPosition(model); if (index != -1) { notifyItemChanged(index, payload); } } /** * Adds the model to the end of the {@link #models} list and notifies that the item was inserted. */ protected void addModel(EpoxyModel<?> modelToAdd) { int initialSize = models.size(); pauseModelListNotifications(); models.add(modelToAdd); resumeModelListNotifications(); notifyItemRangeInserted(initialSize, 1); } /** * Adds the models to the end of the {@link #models} list and notifies that the items were * inserted. */ protected void addModels(EpoxyModel<?>... modelsToAdd) { int initialSize = models.size(); int numModelsToAdd = modelsToAdd.length; ((ModelList) models).ensureCapacity(initialSize + numModelsToAdd); pauseModelListNotifications(); Collections.addAll(models, modelsToAdd); resumeModelListNotifications(); notifyItemRangeInserted(initialSize, numModelsToAdd); } /** * Adds the models to the end of the {@link #models} list and notifies that the items were * inserted. */ protected void addModels(Collection<? extends EpoxyModel<?>> modelsToAdd) { int initialSize = models.size(); pauseModelListNotifications(); models.addAll(modelsToAdd); resumeModelListNotifications(); notifyItemRangeInserted(initialSize, modelsToAdd.size()); } /** * Inserts the given model before the other in the {@link #models} list, and notifies that the * item was inserted. */ protected void insertModelBefore(EpoxyModel<?> modelToInsert, EpoxyModel<?> modelToInsertBefore) { int targetIndex = getModelPosition(modelToInsertBefore); if (targetIndex == -1) { throw new IllegalStateException("Model is not added: " + modelToInsertBefore); } pauseModelListNotifications(); models.add(targetIndex, modelToInsert); resumeModelListNotifications(); notifyItemInserted(targetIndex); } /** * Inserts the given model after the other in the {@link #models} list, and notifies that the item * was inserted. */ protected void insertModelAfter(EpoxyModel<?> modelToInsert, EpoxyModel<?> modelToInsertAfter) { int modelIndex = getModelPosition(modelToInsertAfter); if (modelIndex == -1) { throw new IllegalStateException("Model is not added: " + modelToInsertAfter); } int targetIndex = modelIndex + 1; pauseModelListNotifications(); models.add(targetIndex, modelToInsert); resumeModelListNotifications(); notifyItemInserted(targetIndex); } /** * If the given model exists it is removed and an item removal is notified. Otherwise this does * nothing. */ protected void removeModel(EpoxyModel<?> model) { int index = getModelPosition(model); if (index != -1) { pauseModelListNotifications(); models.remove(index); resumeModelListNotifications(); notifyItemRemoved(index); } } /** * Removes all models */ protected void removeAllModels() { int numModelsRemoved = models.size(); pauseModelListNotifications(); models.clear(); resumeModelListNotifications(); notifyItemRangeRemoved(0, numModelsRemoved); } /** * Removes all models after the given model, which must have already been added. An example use * case is you want to keep a header but clear everything else, like in the case of refreshing * data. */ protected void removeAllAfterModel(EpoxyModel<?> model) { List<EpoxyModel<?>> modelsToRemove = getAllModelsAfter(model); int numModelsRemoved = modelsToRemove.size(); int initialModelCount = models.size(); // This is a sublist, so clearing it will clear the models in the original list pauseModelListNotifications(); modelsToRemove.clear(); resumeModelListNotifications(); notifyItemRangeRemoved(initialModelCount - numModelsRemoved, numModelsRemoved); } /** * Sets the visibility of the given model, and notifies that the item changed if the new * visibility is different from the previous. * * @param model The model to show. It should already be added to the {@link #models} list. * @param show True to show the model, false to hide it. */ protected void showModel(EpoxyModel<?> model, boolean show) { if (model.isShown() == show) { return; } model.show(show); notifyModelChanged(model); } /** * Shows the given model, and notifies that the item changed if the item wasn't already shown. * * @param model The model to show. It should already be added to the {@link #models} list. */ protected void showModel(EpoxyModel<?> model) { showModel(model, true); } /** * Shows the given models, and notifies that each item changed if the item wasn't already shown. * * @param models The models to show. They should already be added to the {@link #models} list. */ protected void showModels(EpoxyModel<?>... models) { showModels(Arrays.asList(models)); } /** * Sets the visibility of the given models, and notifies that the items changed if the new * visibility is different from the previous. * * @param models The models to show. They should already be added to the {@link #models} list. * @param show True to show the models, false to hide them. */ protected void showModels(boolean show, EpoxyModel<?>... models) { showModels(Arrays.asList(models), show); } /** * Shows the given models, and notifies that each item changed if the item wasn't already shown. * * @param models The models to show. They should already be added to the {@link #models} list. */ protected void showModels(Iterable<EpoxyModel<?>> models) { showModels(models, true); } /** * Sets the visibility of the given models, and notifies that the items changed if the new * visibility is different from the previous. * * @param models The models to show. They should already be added to the {@link #models} list. * @param show True to show the models, false to hide them. */ protected void showModels(Iterable<EpoxyModel<?>> models, boolean show) { for (EpoxyModel<?> model : models) { showModel(model, show); } } /** * Hides the given model, and notifies that the item changed if the item wasn't already hidden. * * @param model The model to hide. This should already be added to the {@link #models} list. */ protected void hideModel(EpoxyModel<?> model) { showModel(model, false); } /** * Hides the given models, and notifies that each item changed if the item wasn't already hidden. * * @param models The models to hide. They should already be added to the {@link #models} list. */ protected void hideModels(Iterable<EpoxyModel<?>> models) { showModels(models, false); } /** * Hides the given models, and notifies that each item changed if the item wasn't already hidden. * * @param models The models to hide. They should already be added to the {@link #models} list. */ protected void hideModels(EpoxyModel<?>... models) { hideModels(Arrays.asList(models)); } /** * Hides all models currently located after the given model in the {@link #models} list. * * @param model The model after which to hide. It must exist in the {@link #models} list. */ protected void hideAllAfterModel(EpoxyModel<?> model) { hideModels(getAllModelsAfter(model)); } /** * Returns a sub list of all items in {@link #models} that occur after the given model. This list * is backed by the original models list, any changes to the returned list will be reflected in * the original {@link #models} list. * * @param model Must exist in {@link #models}. */ protected List<EpoxyModel<?>> getAllModelsAfter(EpoxyModel<?> model) { int index = getModelPosition(model); if (index == -1) { throw new IllegalStateException("Model is not added: " + model); } return models.subList(index + 1, models.size()); } /** * We pause the list's notifications when we modify models internally, since we already do the * proper adapter notifications for those modifications. By pausing these list notifications we * prevent the differ having to do work to track them. */ private void pauseModelListNotifications() { ((ModelList) models).pauseNotifications(); } private void resumeModelListNotifications() { ((ModelList) models).resumeNotifications(); } }