Функция защиты от ошибок RxJava2 не работает должным образом в RecyclerView - Android

Я пытаюсь создать Custom ImageButton, который будет накапливать клики и запускать событие, когда пользователь перестает нажимать кнопку в течение 1 секунды.

Для этого я использовал функцию debounce.

Пользовательский ImageButton:

public class MBImageButton extends ImageButton {

    private AtomicInteger mCounter;
    private Disposable mDisposable;
    private Observable<Object> observable;
    private OnAccumulatedRequestsRead mOnAccumulatedRequestsRead;
    private OnEverClickListener mOnEverClickListener;
    private int emitEveryMilli = 1000; // every 1 second by default
    private boolean shouldDisposeOnDetachFromWindow = true;

    public MBImageButton(Context context) {
        super(context);
        init();
    }

    public MBImageButton(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public MBImageButton(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    public void setAccumulatedClickListeners(OnEverClickListener onEverClickListener,
                                             OnAccumulatedRequestsRead onAccumulatedRequestsRead) {
        setOnAccumulatedRequestsRead(onAccumulatedRequestsRead);
        setOnEverClickListener(onEverClickListener);
        initClickObservable();
        subscribe();
    }

    private void initClickObservable() {
        observable = Observable.create(emitter -> {
            emitter.setCancellable(() -> setOnClickListener(null));
            try {
                setOnClickListener(view -> {
                    try {
                        final int currentCount = mCounter.incrementAndGet();
                        Timber.d("Clicked: " + currentCount);
                        if (mOnEverClickListener != null) {
                            mOnEverClickListener.onEveryClickListener(currentCount);
                        }
                        emitter.onNext(new Object());
                    } catch (Exception e) {
                        emitter.onError(e);
                    }
                });
            } catch (Exception e) {
                emitter.onError(e);
            }
        }).doOnSubscribe(disposable -> mDisposable = disposable)
                               .debounce(emitEveryMilli, TimeUnit.MILLISECONDS);
    }

    private void subscribe() {
        observable.subscribeOn(Schedulers.newThread()).observeOn(AndroidSchedulers.mainThread())
                .subscribe(o -> {
                    try {
                        final int count = mCounter.get();
                        if(count==0) return;
                        mCounter.set(0);
                        Timber.d("Accumulated Clicks: " + count);
                        if (mOnAccumulatedRequestsRead != null) {
                            mOnAccumulatedRequestsRead.onAccumulatedRequestsReady(count);
                        }
                    } catch (Exception e) {
                        Timber.e(e);
                    }
                }, Timber::e);
    }

    private void init() {
        mCounter = new AtomicInteger(0);
    }

    public void disposeAccumulatedClickListeners() {
        if (mDisposable != null) {
            mDisposable.dispose();
        }
    }

    public void shouldDisposeOnDetachFromWindow(boolean shouldDisposeOnDetachFromWindow) {
        this.shouldDisposeOnDetachFromWindow = shouldDisposeOnDetachFromWindow;
    }

    public void setEmitEveryMilliseconds(int emitEveryMilli) {
        this.emitEveryMilli = emitEveryMilli;
        initClickObservable();
        subscribe();
    }

    private void setOnEverClickListener(OnEverClickListener onEverClickListener) {
        mOnEverClickListener = onEverClickListener;
    }

    private void setOnAccumulatedRequestsRead(OnAccumulatedRequestsRead onAccumulatedRequestsRead) {
        mOnAccumulatedRequestsRead = onAccumulatedRequestsRead;
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        if (shouldDisposeOnDetachFromWindow) {
            if (mDisposable != null) {
                mDisposable.dispose();
            }
        }
    }

    public interface OnAccumulatedRequestsRead {
        void onAccumulatedRequestsReady(int count);
    }

    public interface OnEverClickListener {
        void onEveryClickListener(int currentCount);
    }
}

Функция, которая настраивает этот imageButton для устранения неполадок, следующая:

setAccumulatedClickListeners(OnEverClickListener, OnAccumulatedRequestsRead)

Когда я настраиваю это представление из RecyclerView, все работает правильно,

результат выглядит так, когда я нажимаю последовательно 9 раз:

D/MBImageButton: Clicked: 1
D/MBImageButton: Clicked: 2
D/MBImageButton: Clicked: 3
D/MBImageButton: Clicked: 4
D/MBImageButton: Clicked: 5
D/MBImageButton: Clicked: 6
D/MBImageButton: Clicked: 7
D/MBImageButton: Clicked: 8
D/MBImageButton: Clicked: 9
D/MBImageButton: Accumulated Clicks: 9

Когда я добавляю этот настраиваемый ImageButton в RecyclerView ViewHolder, результат неверен для того же количества кликов:

D/MBImageButton: Clicked: 1
D/MBImageButton: Clicked: 2
D/MBImageButton: Clicked: 1
D/MBImageButton: Clicked: 3
D/MBImageButton: Clicked: 2
D/MBImageButton: Clicked: 4
D/MBImageButton: Accumulated Clicks: 4
D/MBImageButton: Clicked: 1
D/MBImageButton: Accumulated Clicks: 2
D/MBImageButton: Clicked: 2
D/MBImageButton: Accumulated Clicks: 2
D/MBImageButton: Accumulated Clicks: 0
D/MBImageButton: Clicked: 1
D/MBImageButton: Accumulated Clicks: 1
D/MBImageButton: Accumulated Clicks: 0
D/MBImageButton: Accumulated Clicks: 0

это еще один результат для 9 последовательных нажатий:

D/MBImageButton: Clicked: 1
D/MBImageButton: Clicked: 2
D/MBImageButton: Clicked: 1
D/MBImageButton: Clicked: 3
D/MBImageButton: Clicked: 2
D/MBImageButton: Clicked: 4
D/MBImageButton: Clicked: 3
D/MBImageButton: Accumulated Clicks: 4
D/MBImageButton: Clicked: 1
D/MBImageButton: Accumulated Clicks: 3
D/MBImageButton: Accumulated Clicks: 1
D/MBImageButton: Accumulated Clicks: 0
D/MBImageButton: Clicked: 1
D/MBImageButton: Accumulated Clicks: 1
D/MBImageButton: Accumulated Clicks: 0
D/MBImageButton: Accumulated Clicks: 0
D/MBImageButton: Accumulated Clicks: 0

В обоих случаях я нажимал с одинаковым интервалом времени, но когда эта кнопка добавлена ​​в RecyclerView, она дает странный результат.

В чем может быть проблема?

Обновление:

Упрощенная реализация моего адаптера:

public class ProductRVAdapter extends BaseProductRVAdapter<ProductRVAdapter.ProductVH> {

    private ProductAdapterListener mProductAdapterListener;

    public ProductRVAdapter(List<Product> productList, ProductAdapterListener productAdapterListener) {
        super(productList);
        mProductAdapterListener = productAdapterListener;
    }

    @Override
    public ProductVH onCreateViewHolder(ViewGroup parent, int viewType) {
        final View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_product, parent, false);
        return new ProductVH(view);
    }

    @Override
    public void onBindViewHolder(ProductVH holder, int position) {
        holder.bind(productList.get(position), position);

        holder.ib_decrease.setAccumulatedClickListeners(currentCount -> onProductAdapterDecreaseClicked.onClick(holder.ib_decrease),
                count -> {
                    if (mProductAdapterListener != null) {
                        mProductAdapterListener.onProductAdapterProductChangeToWSListener(productList.get(position), position, -count);
                    }
                });

        holder.ib_add.setAccumulatedClickListeners(currentCount -> onProductAdapterAddToCartClicked.onClick(holder.ib_add),
                count -> {
                    if (mProductAdapterListener != null) {
                        mProductAdapterListener.onProductAdapterProductChangeToWSListener(productList.get(position), position, count);
                    }
                });
    }

    public interface ProductAdapterListener {

        void onProductAdapterAddToCartClicked(Product product, int position);

        void onProductAdapterProductChangeToWSListener(Product product, int position, int amountChanged);

        void onProductAdapterDecreaseClicked(Product product, int position);
    }

    public static class ProductVH extends RecyclerView.ViewHolder {
        @BindView(R.id.iv_productImage)
        ImageView iv_producImage;
        @BindView(R.id.tv_productName)
        TextView tv_productName;
        @BindView(R.id.tv_prodAmount)
        MBTextView tv_prodAmount;

        @BindView(R.id.ib_decrease)
        MBImageButton ib_decrease;
        @BindView(R.id.ib_add)
        MBImageButton ib_add;

        ProductVH(View itemView) {
            super(itemView);
            ButterKnife.bind(this, itemView);
        }


        public void bind(Product product, int position) {
            tv_productName.setText(product.getName());
            tv_prodAmount.setText(product.getAmount());

            ImageLoader.loadImage(iv_producImage.getContext(), iv_producImage, product.getImg(), R.drawable.ic_category_item);

            ib_decrease.setTag(position);
            ib_add.setTag(position);
        }
    }
}

person MBH    schedule 27.02.2017    source источник
comment
Можете ли вы показать реализацию вашего адаптера?   -  person azizbekian    schedule 27.02.2017
comment
@azizbekian Я добавил реализацию адаптера   -  person MBH    schedule 27.02.2017
comment
Я не вижу реализации getItemCount(), которая предположительно находится в BaseProductRVAdapter. Сколько у вас предметов?   -  person azizbekian    schedule 27.02.2017
comment
Вы вызываете setAccumulatedClickListeners(), который подписывается на наблюдаемое для каждого элемента в списке. Например. у вас есть 10 элементов в recyclerview, вы подписались бы 10 раз на наблюдаемое, когда recyclerview помещает эти элементы. Это не та функциональность, которую вы хотели бы иметь, верно?   -  person azizbekian    schedule 27.02.2017
comment
Это так: public int getItemCount() { return productList == null ? 0 : productList.size(); } @azizbekian   -  person MBH    schedule 27.02.2017
comment
В элементах есть кнопки «Добавить» и «Удалить» для каждого элемента, поэтому пользователь щелкнет «Добавить» 10 раз, после того, как он перестанет нажимать, я отправлю 1 запрос в веб-сервис с 10 добавленными элементами. Итак, для каждого элемента есть 2 подписки, для добавления и удаления @azizbekian   -  person MBH    schedule 27.02.2017
comment
Позвольте нам продолжить это обсуждение в чате.   -  person MBH    schedule 27.02.2017
comment
Опубликуйте проект там, где легко воспроизвести проблему, потому что я не могу воспроизвести ее в своем проекте.   -  person Divers    schedule 03.03.2017


Ответы (1)


У меня есть решение

Основная проблема заключается в том, что когда я прокручиваю RecyclerView Adapter, виртуальные объекты отделяются от окна и вызывается функция onDetachedFromWindow в этой функции i unsubscribe (dispose) из наблюдаемого.

Решения пробовали:

1. Я добавил функцию attachFunction

@Override
protected void onAttachedToWindow() {
    super.onAttachedToWindow();
    if (shouldDisposeOnDetachFromWindow) {
        if (mDisposable != null) {
            if (mDisposable.isDisposed()) {
                initClickObservable();
                subscribe();
            }
        }
    }
}

2. Удалены вызовы notifyItemChanged (position)

Обычно я обновляю представление после некоторой модификации, используя notifyItemChange(). Теперь я обновляю представление напрямую с помощью setText или аналогичных функций.

Теперь он работает нормально.

person MBH    schedule 09.03.2017