diff --git a/src/main/java/ru/touchin/roboswag/components/adapters/AdapterDelegate.java b/src/main/java/ru/touchin/roboswag/components/adapters/AdapterDelegate.java new file mode 100644 index 0000000..f3e29fd --- /dev/null +++ b/src/main/java/ru/touchin/roboswag/components/adapters/AdapterDelegate.java @@ -0,0 +1,253 @@ +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; +import rx.Observable; +import rx.Single; +import rx.Subscription; +import rx.functions.Action0; +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. + */ +@SuppressWarnings("PMD.TooManyMethods") +//TooManyMethods: it's ok as it is LifecycleBindable +public abstract class AdapterDelegate implements LifecycleBindable { + + @NonNull + private final LifecycleBindable parentLifecycleBindable; + private final int defaultItemViewType; + + public AdapterDelegate(@NonNull final LifecycleBindable parentLifecycleBindable) { + this.parentLifecycleBindable = parentLifecycleBindable; + this.defaultItemViewType = UiUtils.OfViews.generateViewId(); + } + + /** + * Returns parent {@link LifecycleBindable} that this delegate created from (e.g. Activity or ViewController). + * + * @return Parent {@link LifecycleBindable}. + */ + @NonNull + public LifecycleBindable getParentLifecycleBindable() { + return parentLifecycleBindable; + } + + /** + * Unique ID of AdapterDelegate. + * + * @return Unique ID. + */ + public int getItemViewType() { + return defaultItemViewType; + } + + /** + * 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 adapterPosition 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 adapterPosition, final int itemCollectionPosition); + + /** + * Returns unique ID of item to support stable ID's logic of RecyclerView's adapter. + * + * @param item Item to check; + * @param adapterPosition Position of item in adapter; + * @param itemCollectionPosition Position of item in collection that contains item; + * @return Unique item ID. + */ + public long getItemId(@NonNull final TItem item, final int adapterPosition, final int itemCollectionPosition) { + 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 adapterPosition Position of item in adapter; + * @param itemCollectionPosition Position of item in collection that contains item; + */ + public abstract void onBindViewHolder(@NonNull final TViewHolder holder, @NonNull final TItem item, + final int adapterPosition, final int itemCollectionPosition); + + /** + * 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 adapterPosition Position of item in adapter; + * @param itemCollectionPosition 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 adapterPosition, final int itemCollectionPosition) { + //do nothing by default + } + + @SuppressWarnings("CPD-START") + //CPD: it is same as in other implementation based on BaseLifecycleBindable + @NonNull + @Override + public Subscription untilStop(@NonNull final Observable observable) { + return parentLifecycleBindable.untilStop(observable); + } + + @NonNull + @Override + public Subscription untilStop(@NonNull final Observable observable, @NonNull final Action1 onNextAction) { + return parentLifecycleBindable.untilStop(observable, onNextAction); + } + + @NonNull + @Override + public Subscription untilStop(@NonNull final Observable observable, + @NonNull final Action1 onNextAction, + @NonNull final Action1 onErrorAction) { + return parentLifecycleBindable.untilStop(observable, onNextAction, onErrorAction); + } + + @NonNull + @Override + public Subscription untilStop(@NonNull final Observable observable, + @NonNull final Action1 onNextAction, + @NonNull final Action1 onErrorAction, + @NonNull final Action0 onCompletedAction) { + return parentLifecycleBindable.untilStop(observable, onNextAction, onErrorAction, onCompletedAction); + } + + @NonNull + @Override + public Subscription untilStop(@NonNull final Single single) { + return parentLifecycleBindable.untilStop(single); + } + + @NonNull + @Override + public Subscription untilStop(@NonNull final Single single, @NonNull final Action1 onSuccessAction) { + return parentLifecycleBindable.untilStop(single, onSuccessAction); + } + + @NonNull + @Override + public Subscription untilStop(@NonNull final Single single, + @NonNull final Action1 onSuccessAction, + @NonNull final Action1 onErrorAction) { + return parentLifecycleBindable.untilStop(single, onSuccessAction, onErrorAction); + } + + @NonNull + @Override + public Subscription untilStop(@NonNull final Completable completable) { + return parentLifecycleBindable.untilStop(completable); + } + + @NonNull + @Override + public Subscription untilStop(@NonNull final Completable completable, @NonNull final Action0 onCompletedAction) { + return parentLifecycleBindable.untilStop(completable, onCompletedAction); + } + + @NonNull + @Override + public Subscription untilStop(@NonNull final Completable completable, + @NonNull final Action0 onCompletedAction, + @NonNull final Action1 onErrorAction) { + return parentLifecycleBindable.untilStop(completable, onCompletedAction, onErrorAction); + } + + @NonNull + @Override + public Subscription untilDestroy(@NonNull final Observable observable) { + return parentLifecycleBindable.untilDestroy(observable); + } + + @NonNull + @Override + public Subscription untilDestroy(@NonNull final Observable observable, @NonNull final Action1 onNextAction) { + return parentLifecycleBindable.untilDestroy(observable, onNextAction); + } + + @NonNull + @Override + public Subscription untilDestroy(@NonNull final Observable observable, + @NonNull final Action1 onNextAction, + @NonNull final Action1 onErrorAction) { + return parentLifecycleBindable.untilDestroy(observable, onNextAction, onErrorAction); + } + + @NonNull + @Override + public Subscription untilDestroy(@NonNull final Observable observable, + @NonNull final Action1 onNextAction, + @NonNull final Action1 onErrorAction, + @NonNull final Action0 onCompletedAction) { + return parentLifecycleBindable.untilDestroy(observable, onNextAction, onErrorAction, onCompletedAction); + } + + @NonNull + @Override + public Subscription untilDestroy(@NonNull final Single single) { + return parentLifecycleBindable.untilDestroy(single); + } + + @NonNull + @Override + public Subscription untilDestroy(@NonNull final Single single, @NonNull final Action1 onSuccessAction) { + return parentLifecycleBindable.untilDestroy(single, onSuccessAction); + } + + @NonNull + @Override + public Subscription untilDestroy(@NonNull final Single single, + @NonNull final Action1 onSuccessAction, + @NonNull final Action1 onErrorAction) { + return parentLifecycleBindable.untilDestroy(single, onSuccessAction, onErrorAction); + } + + @NonNull + @Override + public Subscription untilDestroy(@NonNull final Completable completable) { + return parentLifecycleBindable.untilDestroy(completable); + } + + @NonNull + @Override + public Subscription untilDestroy(@NonNull final Completable completable, @NonNull final Action0 onCompletedAction) { + return parentLifecycleBindable.untilDestroy(completable, onCompletedAction); + } + + @NonNull + @Override + public Subscription untilDestroy(@NonNull final Completable completable, + @NonNull final Action0 onCompletedAction, + @NonNull final Action1 onErrorAction) { + return parentLifecycleBindable.untilDestroy(completable, onCompletedAction, onErrorAction); + } + +} 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 8a877bf..ac1a3d1 100644 --- a/src/main/java/ru/touchin/roboswag/components/adapters/ObservableCollectionAdapter.java +++ b/src/main/java/ru/touchin/roboswag/components/adapters/ObservableCollectionAdapter.java @@ -22,19 +22,17 @@ package ru.touchin.roboswag.components.adapters; import android.os.Looper; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import android.support.annotation.StringRes; import android.support.v7.widget.RecyclerView; -import android.view.View; import android.view.ViewGroup; +import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.LinkedList; import java.util.List; -import java.util.concurrent.TimeUnit; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; import io.reactivex.subjects.BehaviorSubject; import ru.touchin.roboswag.components.utils.LifecycleBindable; import ru.touchin.roboswag.components.utils.UiUtils; @@ -44,11 +42,13 @@ import ru.touchin.roboswag.core.observables.collections.ObservableCollection; import ru.touchin.roboswag.core.observables.collections.ObservableList; import ru.touchin.roboswag.core.observables.collections.loadable.LoadingMoreList; import ru.touchin.roboswag.core.utils.Optional; +import ru.touchin.roboswag.core.utils.ShouldNotHappenException; /** * Created by Gavriil Sitnikov on 20/11/2015. * Adapter based on {@link ObservableCollection} and providing some useful features like: - * - item-based binding by {@link #onBindItemToViewHolder(ViewHolder, int, Object)}} method; + * - item-based binding method; + * - delegates by {@link AdapterDelegate} over itemViewType logic; * - item click listener setup by {@link #setOnItemClickListener(OnItemClickListener)}; * - allows to inform about footers/headers by overriding base create/bind methods and {@link #getHeadersCount()} plus {@link #getFootersCount()}; * - by default it is pre-loading items for collections like {@link ru.touchin.roboswag.core.observables.collections.loadable.LoadingMoreList}. @@ -56,27 +56,31 @@ import ru.touchin.roboswag.core.utils.Optional; * @param Type of items to bind to ViewHolders; * @param Type of ViewHolders to show items. */ -public abstract class ObservableCollectionAdapter +@SuppressWarnings("unchecked") +public abstract class ObservableCollectionAdapter extends RecyclerView.Adapter { - private static final int PRE_LOADING_COUNT = 10; + private static final int PRE_LOADING_COUNT = 20; @NonNull private final BehaviorSubject>> observableCollectionSubject = BehaviorSubject.createDefault(new Optional<>(null)); @NonNull + private final BehaviorSubject moreAutoLoadingRequested = BehaviorSubject.create(); + @NonNull private final LifecycleBindable lifecycleBindable; @Nullable - private OnItemClickListener onItemClickListener; + private Object onItemClickListener; private int lastUpdatedChangeNumber = -1; - @NonNull - private final Observable historyPreLoadingObservable; @NonNull private final ObservableList innerCollection = new ObservableList<>(); private boolean anyChangeApplied; + private long itemClickDelayMillis; @NonNull private final List attachedRecyclerViews = new LinkedList<>(); + @NonNull + private final List> delegates = new ArrayList<>(); public ObservableCollectionAdapter(@NonNull final LifecycleBindable lifecycleBindable) { super(); @@ -89,16 +93,33 @@ public abstract class ObservableCollectionAdapter { - final ObservableCollection collection = optional.get(); + lifecycleBindable.untilDestroy(createMoreAutoLoadingObservable()); + } + + @NonNull + private Observable createMoreAutoLoadingObservable() { + return observableCollectionSubject + .switchMap(collectionOptional -> { + final ObservableCollection collection = collectionOptional.get(); if (!(collection instanceof LoadingMoreList)) { return Observable.empty(); } - final int size = collection.size(); - return ((LoadingMoreList) collection).loadRange(size, size + PRE_LOADING_COUNT).toObservable(); + return moreAutoLoadingRequested + .distinctUntilChanged() + .switchMap(requested -> { + if (!requested) { + return Observable.empty(); + } + final int size = collection.size(); + return ((LoadingMoreList) collection) + .loadRange(size, size + PRE_LOADING_COUNT) + .onErrorReturnItem(new ArrayList<>()) + .toObservable() + .doOnComplete(() -> moreAutoLoadingRequested.onNext(false)); + }); }); } @@ -124,7 +145,7 @@ public abstract class ObservableCollectionAdapter> getDelegates() { + return Collections.unmodifiableList(delegates); + } + + /** + * Adds {@link AdapterDelegate} 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; + } + } + delegates.add((AdapterDelegate) delegate); + notifyDataSetChanged(); + } + + /** + * Removes {@link AdapterDelegate} from adapter. + * + * @param delegate Delegate to remove. + */ + public void removeDelegate(@NonNull final AdapterDelegate delegate) { + delegates.remove((AdapterDelegate) delegate); + notifyDataSetChanged(); + } + + @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(); + } + } + return 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 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); + } + } + return super.getItemId(positionInAdapter); + } + @Override public int getItemCount() { return getHeadersCount() + innerCollection.size() + getFootersCount(); @@ -282,18 +371,40 @@ public abstract class ObservableCollectionAdapter delegate : delegates) { + if (delegate.getItemViewType() == viewType) { + return delegate.onCreateViewHolder(parent); + } + } + throw new ShouldNotHappenException("Add some AdapterDelegate or override this method"); + } - @SuppressWarnings({"unchecked", "deprecation"}) @Override - public void onBindViewHolder(@NonNull final BindableViewHolder holder, final int position) { + public void onBindViewHolder(@NonNull final BindableViewHolder holder, final int positionInAdapter) { lastUpdatedChangeNumber = innerCollection.getChangesCount(); - final int itemPosition = position - getHeadersCount(); - if (itemPosition < 0 || itemPosition >= innerCollection.size()) { + final int positionInCollection = positionInAdapter - getHeadersCount(); + if (positionInCollection < 0 || positionInCollection >= innerCollection.size()) { return; } + updateMoreAutoLoadingRequest(positionInCollection); + bindItemViewHolder(holder, 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); + } + + private void bindItemViewHolder(@NonNull final BindableViewHolder holder, @Nullable final List payloads, + final int positionInAdapter, final int positionInCollection) { final TItemViewHolder itemViewHolder; try { itemViewHolder = (TItemViewHolder) holder; @@ -301,29 +412,79 @@ public abstract class ObservableCollectionAdapter onItemClickListener.onItemClicked(item, position), getItemClickDelay()); + final TItem item = innerCollection.get(positionInCollection); + final int itemViewType = getItemViewType(positionInAdapter); + + updateClickListener(holder, item, positionInAdapter, positionInCollection); + for (final AdapterDelegate 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 (payloads == null) { + onBindItemToViewHolder(itemViewHolder, positionInAdapter, item); + } else { + onBindItemToViewHolder(itemViewHolder, positionInAdapter, item, payloads); + } + } + + private void updateClickListener(@NonNull final BindableViewHolder holder, @NonNull final TItem item, + final int positionInAdapter, final int positionInCollection) { + if (onItemClickListener != null && !isOnClickListenerDisabled(item, positionInAdapter, positionInCollection)) { + UiUtils.setOnRippleClickListener(holder.itemView, + () -> { + if (onItemClickListener instanceof OnItemClickListener) { + ((OnItemClickListener) onItemClickListener).onItemClicked(item); + } else if (onItemClickListener instanceof OnItemWithPositionClickListener) { + ((OnItemWithPositionClickListener) onItemClickListener).onItemClicked(item, positionInAdapter, positionInCollection); + } else { + Lc.assertion("Unexpected onItemClickListener type " + onItemClickListener); + } + }, + itemClickDelayMillis); + } + } + + private void updateMoreAutoLoadingRequest(final int positionInCollection) { + if (positionInCollection > innerCollection.size() - PRE_LOADING_COUNT) { + return; + } + moreAutoLoadingRequested.onNext(true); } /** * Method to bind item (from {@link #getObservableCollection()}) to item-specific ViewHolder. * It is not calling for headers and footer which counts are returned by {@link #getHeadersCount()} and @link #getFootersCount()}. * - * @param holder ViewHolder to bind item to; - * @param position Position of ViewHolder (NOT item!); - * @param item Item returned by position (WITH HEADER OFFSET!). + * @param holder ViewHolder to bind item to; + * @param positionInAdapter Position of ViewHolder (NOT item!); + * @param item Item returned by position (WITH HEADER OFFSET!). */ - protected abstract void onBindItemToViewHolder(@NonNull TItemViewHolder holder, int position, @NonNull TItem item); + protected abstract void onBindItemToViewHolder(@NonNull TItemViewHolder holder, int positionInAdapter, @NonNull TItem item); + + /** + * Method to bind item (from {@link #getObservableCollection()}) to item-specific ViewHolder with payloads. + * It is not calling for headers and footer which counts are returned by {@link #getHeadersCount()} and @link #getFootersCount()}. + * + * @param holder ViewHolder to bind item to; + * @param positionInAdapter Position of ViewHolder in adapter (NOT item!); + * @param item Item returned by position (WITH HEADER OFFSET!); + * @param payloads Payloads. + */ + protected void onBindItemToViewHolder(@NonNull final TItemViewHolder holder, final int positionInAdapter, @NonNull final TItem item, + @NonNull final List payloads) { + // do nothing by default + } @Nullable - public TItem getItem(final int position) { - final int positionInList = position - getHeadersCount(); - return positionInList < 0 || positionInList >= innerCollection.size() ? null : innerCollection.get(positionInList); + public TItem getItem(final int positionInAdapter) { + final int positionInCollection = positionInAdapter - getHeadersCount(); + return positionInCollection < 0 || positionInCollection >= innerCollection.size() ? null : innerCollection.get(positionInCollection); } /** @@ -332,26 +493,51 @@ public abstract class ObservableCollectionAdapter onItemClickListener) { + this.setOnItemClickListener(onItemClickListener, UiUtils.RIPPLE_EFFECT_DELAY); + } + + /** + * Sets item click listener. + * + * @param onItemClickListener Item click listener; + * @param itemClickDelayMillis Delay of calling click listener. + */ + public void setOnItemClickListener(@Nullable final OnItemClickListener onItemClickListener, final long itemClickDelayMillis) { this.onItemClickListener = onItemClickListener; + this.itemClickDelayMillis = itemClickDelayMillis; refreshUpdate(); } /** - * Returns delay of item click. By default returns delay of ripple effect duration. + * Sets item click listener. * - * @return Milliseconds delay of click. + * @param onItemClickListener Item click listener. */ - protected long getItemClickDelay() { - return UiUtils.RIPPLE_EFFECT_DELAY; + public void setOnItemClickListener(@Nullable final OnItemWithPositionClickListener onItemClickListener) { + this.setOnItemClickListener(onItemClickListener, UiUtils.RIPPLE_EFFECT_DELAY); + } + + /** + * Sets item click listener. + * + * @param onItemClickListener Item click listener; + * @param itemClickDelayMillis Delay of calling click listener. + */ + public void setOnItemClickListener(@Nullable final OnItemWithPositionClickListener onItemClickListener, final long itemClickDelayMillis) { + this.onItemClickListener = onItemClickListener; + this.itemClickDelayMillis = itemClickDelayMillis; + refreshUpdate(); } /** * Returns if click listening disabled or not for specific item. * - * @param item Item to check click availability; + * @param item Item to check click availability; + * @param positionInAdapter Position of clicked item in adapter (with headers); + * @param positionInCollection Position of clicked item in inner collection; * @return True if click listener enabled for such item. */ - public boolean isOnClickListenerDisabled(@NonNull final TItem item) { + public boolean isOnClickListenerDisabled(@NonNull final TItem item, final int positionInAdapter, final int positionInCollection) { return false; } @@ -365,78 +551,27 @@ public abstract class ObservableCollectionAdapter Type of item */ - public static class ViewHolder extends BindableViewHolder { - - //it is needed to avoid massive requests on initial view holders attaching (like if we will add 10 items they all will try to load history) - private static final long DELAY_BEFORE_LOADING_HISTORY = TimeUnit.SECONDS.toMillis(1); - - @Nullable - private Disposable historyPreLoadingSubscription; - @Nullable - private ObservableCollectionAdapter adapter; - - public ViewHolder(@NonNull final LifecycleBindable baseBindable, @NonNull final View itemView) { - super(baseBindable, itemView); - } + public interface OnItemWithPositionClickListener { /** - * Bind position to enable pre-loading for connected {@link ObservableCollection}. + * Calls when item have clicked. * - * @param position Position of ViewHolder. + * @param item Clicked item; + * @param positionInAdapter Position of clicked item in adapter (with headers); + * @param positionInCollection Position of clicked item in inner collection. */ - @SuppressWarnings("unchecked") - //unchecked: it's ok, we just need to load something more - public void bindPosition(final int position) { - if (historyPreLoadingSubscription != null) { - historyPreLoadingSubscription.dispose(); - historyPreLoadingSubscription = null; - } - if (adapter != null && position - adapter.getHeadersCount() > adapter.innerCollection.size() - PRE_LOADING_COUNT) { - historyPreLoadingSubscription = untilDestroy(adapter.historyPreLoadingObservable - .delaySubscription(DELAY_BEFORE_LOADING_HISTORY, TimeUnit.MILLISECONDS) - .onErrorResumeNext(Observable.empty())); - } - } - - @SuppressWarnings("PMD.DefaultPackage") - //it is for internal use only - @Deprecated - void setAdapter(@Nullable final ObservableCollectionAdapter adapter) { - this.adapter = adapter; - } - - /** - * Simply get String from resources. - * - * @param stringRes string resource id; - * @return Requested String that matches with provided string resource id. - */ - @NonNull - public String getString(@StringRes final int stringRes) { - return itemView.getContext().getString(stringRes); - } - - /** - * Simply get String from resources with arguments. - * - * @param stringRes String resource id; - * @param formatArgs The format arguments that will be used for substitution; - * @return Requested String that matches with provided string resource id. - */ - @NonNull - public String getString(@StringRes final int stringRes, @Nullable final Object... formatArgs) { - return itemView.getContext().getString(stringRes, formatArgs); - } + void onItemClicked(@NonNull TItem item, final int positionInAdapter, final int positionInCollection); }