diff --git a/src/main/java/ru/touchin/roboswag/components/adapters/AdapterDelegate.java b/src/main/java/ru/touchin/roboswag/components/adapters/AdapterDelegate.java index f3e29fd..bdffcb0 100644 --- a/src/main/java/ru/touchin/roboswag/components/adapters/AdapterDelegate.java +++ b/src/main/java/ru/touchin/roboswag/components/adapters/AdapterDelegate.java @@ -3,8 +3,6 @@ package ru.touchin.roboswag.components.adapters; import android.support.annotation.NonNull; import android.view.ViewGroup; -import java.util.List; - import ru.touchin.roboswag.components.utils.LifecycleBindable; import ru.touchin.roboswag.components.utils.UiUtils; import rx.Completable; @@ -18,12 +16,11 @@ import rx.functions.Action1; * Objects of such class controls creation and binding of specific type of RecyclerView's ViewHolders. * Default {@link #getItemViewType} is generating on construction of object. * - * @param Type of {@link BindableViewHolder} of delegate; - * @param Type of items to bind to {@link BindableViewHolder}s. + * @param Type of {@link BindableViewHolder} of delegate. */ @SuppressWarnings("PMD.TooManyMethods") -//TooManyMethods: it's ok as it is LifecycleBindable -public abstract class AdapterDelegate implements LifecycleBindable { +//TooManyMethods: it's ok +public abstract class AdapterDelegate implements LifecycleBindable { @NonNull private final LifecycleBindable parentLifecycleBindable; @@ -53,29 +50,6 @@ public abstract class AdapterDelegate payloads, - final int adapterPosition, final int itemCollectionPosition) { - //do nothing by default - } - @SuppressWarnings("CPD-START") //CPD: it is same as in other implementation based on BaseLifecycleBindable @NonNull diff --git a/src/main/java/ru/touchin/roboswag/components/adapters/ItemAdapterDelegate.java b/src/main/java/ru/touchin/roboswag/components/adapters/ItemAdapterDelegate.java new file mode 100644 index 0000000..20bad88 --- /dev/null +++ b/src/main/java/ru/touchin/roboswag/components/adapters/ItemAdapterDelegate.java @@ -0,0 +1,81 @@ +package ru.touchin.roboswag.components.adapters; + +import android.support.annotation.NonNull; +import android.view.ViewGroup; + +import java.util.List; + +import ru.touchin.roboswag.components.utils.LifecycleBindable; + +/** + * Objects of such class controls creation and binding of specific type of RecyclerView's ViewHolders. + * Such delegates are creating and binding ViewHolders for specific items. + * Default {@link #getItemViewType} is generating on construction of object. + * + * @param Type of {@link BindableViewHolder} of delegate; + * @param Type of items to bind to {@link BindableViewHolder}s. + */ +public abstract class ItemAdapterDelegate extends AdapterDelegate { + + public ItemAdapterDelegate(@NonNull final LifecycleBindable parentLifecycleBindable) { + super(parentLifecycleBindable); + } + + /** + * Returns if object is processable by this delegate. + * This item will be casted to {@link TItem} and passes to {@link #onBindViewHolder(TViewHolder, TItem, int, int)}. + * + * @param item Item to check; + * @param positionInAdapter Position of item in adapter; + * @param itemCollectionPosition Position of item in collection that contains item; + * @return True if item is processable by this delegate. + */ + public abstract boolean isForViewType(@NonNull final Object item, final int positionInAdapter, final int itemCollectionPosition); + + /** + * Returns unique ID of item to support stable ID's logic of RecyclerView's adapter. + * + * @param item Item to check; + * @param positionInAdapter Position of item in adapter; + * @param positionInCollection Position of item in collection that contains item; + * @return Unique item ID. + */ + public long getItemId(@NonNull final TItem item, final int positionInAdapter, final int positionInCollection) { + return 0; + } + + /** + * Creates ViewHolder to bind item to it later. + * + * @param parent Container of ViewHolder's view. + * @return New ViewHolder. + */ + @NonNull + public abstract TViewHolder onCreateViewHolder(@NonNull final ViewGroup parent); + + /** + * Binds item to created by this object ViewHolder. + * + * @param holder ViewHolder to bind item to; + * @param item Item to check; + * @param positionInAdapter Position of item in adapter; + * @param positionInCollection Position of item in collection that contains item; + */ + public abstract void onBindViewHolder(@NonNull final TViewHolder holder, @NonNull final TItem item, + final int positionInAdapter, final int positionInCollection); + + /** + * Binds item with payloads to created by this object ViewHolder. + * + * @param holder ViewHolder to bind item to; + * @param item Item to check; + * @param payloads Payloads; + * @param positionInAdapter Position of item in adapter; + * @param positionInCollection Position of item in collection that contains item; + */ + public void onBindViewHolder(@NonNull final TViewHolder holder, @NonNull final TItem item, @NonNull final List payloads, + final int positionInAdapter, final int positionInCollection) { + //do nothing by default + } + +} diff --git a/src/main/java/ru/touchin/roboswag/components/adapters/ObservableCollectionAdapter.java b/src/main/java/ru/touchin/roboswag/components/adapters/ObservableCollectionAdapter.java index d9713d3..bb277a5 100644 --- a/src/main/java/ru/touchin/roboswag/components/adapters/ObservableCollectionAdapter.java +++ b/src/main/java/ru/touchin/roboswag/components/adapters/ObservableCollectionAdapter.java @@ -42,6 +42,9 @@ import ru.touchin.roboswag.core.utils.Optional; import ru.touchin.roboswag.core.utils.ShouldNotHappenException; import rx.Observable; import rx.android.schedulers.AndroidSchedulers; +import rx.functions.Action1; +import rx.functions.Action2; +import rx.functions.Action3; import rx.subjects.BehaviorSubject; /** @@ -56,12 +59,22 @@ import rx.subjects.BehaviorSubject; * @param Type of items to bind to ViewHolders; * @param Type of ViewHolders to show items. */ -@SuppressWarnings("unchecked") +@SuppressWarnings({"unchecked", "PMD.TooManyMethods"}) +//TooManyMethods: it's ok public abstract class ObservableCollectionAdapter extends RecyclerView.Adapter { private static final int PRE_LOADING_COUNT = 20; + private static boolean inDebugMode; + + /** + * Enables debugging features like checking concurrent delegates. + */ + public static void setInDebugMode() { + inDebugMode = true; + } + @NonNull private final BehaviorSubject>> observableCollectionSubject = BehaviorSubject.create(new Optional<>(null)); @@ -80,7 +93,7 @@ public abstract class ObservableCollectionAdapter attachedRecyclerViews = new LinkedList<>(); @NonNull - private final List> delegates = new ArrayList<>(); + private final List> delegates = new ArrayList<>(); public ObservableCollectionAdapter(@NonNull final LifecycleBindable lifecycleBindable) { super(); @@ -301,23 +314,38 @@ public abstract class ObservableCollectionAdapter> getDelegates() { + public List> getDelegates() { return Collections.unmodifiableList(delegates); } /** - * Adds {@link AdapterDelegate} to adapter. + * Adds {@link ItemAdapterDelegate} to adapter. * * @param delegate Delegate to add. */ - public void addDelegate(@NonNull final AdapterDelegate delegate) { - for (final AdapterDelegate addedDelegate : delegates) { - if (addedDelegate.getItemViewType() == delegate.getItemViewType()) { - Lc.assertion("AdapterDelegate with viewType=" + delegate.getItemViewType() + " already added"); - return; + public void addDelegate(@NonNull final ItemAdapterDelegate delegate) { + addDelegateInternal(delegate); + } + + /** + * Adds {@link PositionAdapterDelegate} to adapter. + * + * @param delegate Delegate to add. + */ + public void addDelegate(@NonNull final PositionAdapterDelegate delegate) { + addDelegateInternal(delegate); + } + + private void addDelegateInternal(@NonNull final AdapterDelegate delegate) { + if (inDebugMode) { + for (final AdapterDelegate addedDelegate : delegates) { + if (addedDelegate.getItemViewType() == delegate.getItemViewType()) { + Lc.assertion("AdapterDelegate with viewType=" + delegate.getItemViewType() + " already added"); + return; + } } } - delegates.add((AdapterDelegate) delegate); + delegates.add(delegate); notifyDataSetChanged(); } @@ -326,41 +354,88 @@ public abstract class ObservableCollectionAdapter delegate) { - delegates.remove((AdapterDelegate) delegate); + public void removeDelegate(@NonNull final AdapterDelegate delegate) { + delegates.remove(delegate); notifyDataSetChanged(); } + private void checkDelegates(@Nullable final AdapterDelegate alreadyPickedDelegate, @NonNull final AdapterDelegate currentDelegate) { + if (alreadyPickedDelegate != null) { + throw new ShouldNotHappenException("Concurrent delegates: " + currentDelegate + " and " + alreadyPickedDelegate); + } + } + + private int getItemPositionInCollection(final int positionInAdapter) { + final int shiftedPosition = positionInAdapter - getHeadersCount(); + return shiftedPosition >= 0 && shiftedPosition < innerCollection.size() ? shiftedPosition : -1; + } + + @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.ModifiedCyclomaticComplexity", "PMD.StdCyclomaticComplexity", "PMD.NPathComplexity"}) + //Complexity: because of debug code @Override public int getItemViewType(final int positionInAdapter) { - final int positionInCollection = positionInAdapter - getHeadersCount(); - if (positionInCollection < 0 || positionInCollection >= innerCollection.size()) { - return super.getItemViewType(positionInAdapter); - } - final TItem item = innerCollection.get(positionInCollection); - for (final AdapterDelegate delegate : delegates) { - if (delegate.isForViewType(item, positionInAdapter, positionInCollection)) { - return delegate.getItemViewType(); + AdapterDelegate delegateOfViewType = null; + final int positionInCollection = getItemPositionInCollection(positionInAdapter); + final TItem item = positionInCollection >= 0 ? innerCollection.get(positionInCollection) : null; + for (final AdapterDelegate delegate : delegates) { + if (delegate instanceof ItemAdapterDelegate) { + if (item != null && ((ItemAdapterDelegate) delegate).isForViewType(item, positionInAdapter, positionInCollection)) { + checkDelegates(delegateOfViewType, delegate); + delegateOfViewType = delegate; + if (!inDebugMode) { + break; + } + } + } else if (delegate instanceof PositionAdapterDelegate) { + if (((PositionAdapterDelegate) delegate).isForViewType(positionInAdapter)) { + checkDelegates(delegateOfViewType, delegate); + delegateOfViewType = delegate; + if (!inDebugMode) { + break; + } + } + } else { + Lc.assertion("Delegate of type " + delegate.getClass()); } } - return super.getItemViewType(positionInAdapter); + + return delegateOfViewType != null ? delegateOfViewType.getItemViewType() : super.getItemViewType(positionInAdapter); } @Override public long getItemId(final int positionInAdapter) { - final int positionInCollection = positionInAdapter - getHeadersCount(); - if (positionInCollection < 0 || positionInCollection >= innerCollection.size()) { - return super.getItemId(positionInAdapter); - } + final LongContainer result = new LongContainer(); + tryDelegateAction(positionInAdapter, + (itemAdapterDelegate, item, positionInCollection) -> + result.value = itemAdapterDelegate.getItemId(item, positionInAdapter, positionInCollection), + positionAdapterDelegate -> result.value = positionAdapterDelegate.getItemId(positionInAdapter), + (item, positionInCollection) -> result.value = super.getItemId(positionInAdapter)); + return result.value; + } - final int itemViewType = getItemViewType(positionInAdapter); - final TItem item = innerCollection.get(positionInCollection); - for (final AdapterDelegate delegate : delegates) { - if (delegate.getItemViewType() == itemViewType) { - return delegate.getItemId(item, positionInAdapter, positionInCollection); + private void tryDelegateAction(final int positionInAdapter, + @NonNull final Action3 itemAdapterDelegateAction, + @NonNull final Action1 positionAdapterDelegateAction, + @NonNull final Action2 defaultAction) { + final int viewType = getItemViewType(positionInAdapter); + final int positionInCollection = getItemPositionInCollection(positionInAdapter); + final TItem item = positionInCollection >= 0 ? innerCollection.get(positionInCollection) : null; + for (final AdapterDelegate delegate : delegates) { + if (delegate instanceof ItemAdapterDelegate) { + if (item != null && viewType == delegate.getItemViewType()) { + itemAdapterDelegateAction.call((ItemAdapterDelegate) delegate, item, positionInCollection); + return; + } + } else if (delegate instanceof PositionAdapterDelegate) { + if (viewType == delegate.getItemViewType()) { + positionAdapterDelegateAction.call((PositionAdapterDelegate) delegate); + return; + } + } else { + Lc.assertion("Delegate of type " + delegate.getClass()); } } - return super.getItemId(positionInAdapter); + defaultAction.call(item, positionInCollection); } @Override @@ -371,7 +446,7 @@ public abstract class ObservableCollectionAdapter delegate : delegates) { + for (final AdapterDelegate delegate : delegates) { if (delegate.getItemViewType() == viewType) { return delegate.onCreateViewHolder(parent); } @@ -383,26 +458,37 @@ public abstract class ObservableCollectionAdapter= innerCollection.size()) { - return; - } - - updateMoreAutoLoadingRequest(positionInCollection); - bindItemViewHolder(holder, null, positionInAdapter, positionInCollection); + tryDelegateAction(positionInAdapter, + (itemAdapterDelegate, item, positionInCollection) -> { + bindItemViewHolder(itemAdapterDelegate, holder, item, null, positionInAdapter, positionInCollection); + updateMoreAutoLoadingRequest(positionInCollection); + }, + positionAdapterDelegate -> positionAdapterDelegate.onBindViewHolder(holder, positionInAdapter), + (item, positionInCollection) -> { + if (item != null) { + bindItemViewHolder(null, holder, item, null, positionInAdapter, positionInCollection); + } + }); } @Override public void onBindViewHolder(@NonNull final BindableViewHolder holder, final int positionInAdapter, @NonNull final List payloads) { super.onBindViewHolder(holder, positionInAdapter, payloads); - final int positionInCollection = positionInAdapter - getHeadersCount(); - if (positionInCollection < 0 || positionInCollection >= innerCollection.size()) { - return; - } - bindItemViewHolder(holder, payloads, positionInAdapter, positionInCollection); + tryDelegateAction(positionInAdapter, + (itemAdapterDelegate, item, positionInCollection) -> { + bindItemViewHolder(itemAdapterDelegate, holder, item, payloads, positionInAdapter, positionInCollection); + updateMoreAutoLoadingRequest(positionInCollection); + }, + positionAdapterDelegate -> positionAdapterDelegate.onBindViewHolder(holder, positionInAdapter), + (item, positionInCollection) -> { + if (item != null) { + bindItemViewHolder(null, holder, item, payloads, positionInAdapter, positionInCollection); + } + }); } - private void bindItemViewHolder(@NonNull final BindableViewHolder holder, @Nullable final List payloads, + private void bindItemViewHolder(@Nullable final ItemAdapterDelegate itemAdapterDelegate, + @NonNull final BindableViewHolder holder, @NonNull final TItem item, @Nullable final List payloads, final int positionInAdapter, final int positionInCollection) { final TItemViewHolder itemViewHolder; try { @@ -411,24 +497,19 @@ public abstract class ObservableCollectionAdapter delegate : delegates) { - if (itemViewType == delegate.getItemViewType()) { - if (payloads == null) { - delegate.onBindViewHolder(itemViewHolder, item, positionInAdapter, positionInCollection); - } else { - delegate.onBindViewHolder(itemViewHolder, item, payloads, positionInAdapter, positionInCollection); - } - return; + if (itemAdapterDelegate != null) { + if (payloads == null) { + itemAdapterDelegate.onBindViewHolder(itemViewHolder, item, positionInAdapter, positionInCollection); + } else { + itemAdapterDelegate.onBindViewHolder(itemViewHolder, item, payloads, positionInAdapter, positionInCollection); } - } - if (payloads == null) { - onBindItemToViewHolder(itemViewHolder, positionInAdapter, item); } else { - onBindItemToViewHolder(itemViewHolder, positionInAdapter, item, payloads); + if (payloads == null) { + onBindItemToViewHolder(itemViewHolder, positionInAdapter, item); + } else { + onBindItemToViewHolder(itemViewHolder, positionInAdapter, item, payloads); + } } } @@ -485,8 +566,8 @@ public abstract class ObservableCollectionAdapter= innerCollection.size() ? null : innerCollection.get(positionInCollection); + final int positionInCollection = getItemPositionInCollection(positionInAdapter); + return positionInCollection >= 0 ? innerCollection.get(positionInCollection) : null; } /** @@ -577,4 +658,10 @@ public abstract class ObservableCollectionAdapter Type of {@link BindableViewHolder} of delegate. + */ +public abstract class PositionAdapterDelegate extends AdapterDelegate { + + public PositionAdapterDelegate(@NonNull final LifecycleBindable parentLifecycleBindable) { + super(parentLifecycleBindable); + } + + /** + * Returns if object is processable by this delegate. + * + * @param positionInAdapter Position of item in adapter; + * @return True if item is processable by this delegate. + */ + public abstract boolean isForViewType(final int positionInAdapter); + + /** + * Returns unique ID of item to support stable ID's logic of RecyclerView's adapter. + * + * @param positionInAdapter Position of item in adapter; + * @return Unique item ID. + */ + public long getItemId(final int positionInAdapter) { + return 0; + } + + /** + * Creates ViewHolder to bind position to it later. + * + * @param parent Container of ViewHolder's view. + * @return New ViewHolder. + */ + @NonNull + public abstract TViewHolder onCreateViewHolder(@NonNull final ViewGroup parent); + + /** + * Binds position to ViewHolder. + * + * @param holder ViewHolder to bind position to; + * @param positionInAdapter Position of item in adapter. + */ + public abstract void onBindViewHolder(@NonNull final TViewHolder holder, final int positionInAdapter); + + /** + * Binds position with payloads to ViewHolder. + * + * @param holder ViewHolder to bind position to; + * @param payloads Payloads; + * @param positionInAdapter Position of item in adapter. + */ + public void onBindViewHolder(@NonNull final TViewHolder holder, @NonNull final List payloads, final int positionInAdapter) { + //do nothing by default + } + +}