Часто при разработке клиента мы сталкиваемся с задачей отображения какой-либо информации с сервера, базы данных или еще чего-нибудь в виде списка. И при прокручивании списка данные должны автоматически подгружаться и вставляться в список незаметно для пользователя. У пользователя вообще должно сложиться впечатление, что он скроллит бесконечный список.

В данной статье я бы хотел рассказать вам о том, как сделать автоподгружаемый список простейшим в реализации для разработчика и максимально эффективным и быстрым для пользователя. А также о том, как нам в этом здорово поможет RxJava с ее главной догмой — «Everything is Stream!»

Как нам реализовать такое в Android?


Для начала определимся с исходными данными:
  1. За отображение списка отвечает компонент RecyclerView (надеюсь, про ListView уже успели все забыть:) ) и все необходимые для настройки RecyclerView классы.
  2. Подгрузка данных будет осуществляться при помощи запросов (в сеть, в БД и т.д.) с классическими для такой задачи параметрами offset и limit

Далее опишем приблизительный алгоритм работы автоподгужаемого списка:
  1. Загружаем первую порцию данных для списка. Отображаем эти данные.
  2. При скроллинге списка мы должны отслеживать, какие по номеру элементы отображаются на экране. А конкретно, порядковый номер первого или последнего видимого для пользователя элемента.
  3. При наступлении какого-либо события, например, последний видимый на экране элемент является и последним вообще в списке, мы должны подгружать новую порцию данных. Также необходимо не допустить отправки одинаковых запросов. То есть нужно как-то отписаться от «прослушивания» скроллинга списка.
  4. Новые данные отправить в список. Список необходимо обновить. Снова подписаться на «прослушку» скроллинга.
  5. Пункты 2, 3, 4 необходимо повторять до тех пор, пока уже все данные не будут загружены, ну или при наступлении другого необходимого нам события.

И при чем тут RxJava?

Помните, я в начале говорил про главную догму Rx — «Everything is Stream!». Если в ООП мы мыслим категориями объектов, то в Реактивном — категориями потоков.

Например, взглянем на второй пункт алгоритма. Первое, на чем мы здесь остановимся, это скроллинг и соответственно изменяющийся порядковый номер первого или последнего видимого на экране элемента (в рассматриваемом нами ниже примере — последнего). То есть список при скроллинге постоянно «излучает» номера последнего элемента на протяжении всей своей жизни. Ничего не напоминает? Конечно же, это классический «hot observable». А если быть более конкретным, это PublishSubject. Второе, на роль «слушателя» отлично подойдет Subscriber.

RxJava подобно огромному набору «конструкторов», из которых разработчик может создавать самые различные конструкции. Начало конструкции уже положено — список «выпускает» элементы, являющиеся номерами последней видимой строки списка. Далее можно вставить конструктора (они же по сути и потоки), отвечающие за обработку полученных значений, отправку запросов на подгрузку новых данных и встраивания их в список. Ну обо всем по порядку.

Даешь реактивный код!

А теперь к практической части.

Так как мы создаем новый автоподгружаемый список, то унаследуемся от RecyclerView.

public class AutoLoadingRecyclerView<T> extends RecyclerView

Далее мы должны задать списку параметр limit, отвечающий за размер порции подгружаемых данных за раз.

private int limit;
public int getLimit() {
    if (limit <= 0) {
        throw new AutoLoadingRecyclerViewExceptions("limit must be initialised! And limit must be more than zero!");
    }
    return limit;
}

/**
 * required method
 */
public void setLimit(int limit) {
    this.limit = limit;
}

Теперь AutoLoadingRecyclerView должен «излучать» порядковый номер последнего видимого на экране элемента.

Однако «излучение» просто порядкового номера не очень удобно в дальнейшем. Ведь это значение нужно обрабатывать. Да и наш канал (он же «излучатель») будет изрядно флудить, что также накладывает проблему на backpressure. Тогда немного усовершенствуем «излучатель». Пусть на выходе мы будем получать сразу уже готовые значения offset и limit, объединенные в следующую модель:
public class OffsetAndLimit {
    private int offset;
    private int limit;

    public OffsetAndLimit(int offset, int limit) {
        this.offset = offset;
        this.limit = limit;
    }

    public int getOffset() {
        return offset;
    }

    public int getLimit() {
        return limit;
    }
}

Уже лучше. А теперь уменьшим «флуд» канала. Пусть канал «излучает» элементы только тогда, когда это необходимо, то есть когда нужно подгрузить новую порцию данных.

Взглянем на код.

private PublishSubject<OffsetAndLimit> scrollLoadingChannel = PublishSubject.create();

// старт работы канала
private void startScrollingChannel() {
    addOnScrollListener(new RecyclerView.OnScrollListener() {
        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            int position = getLastVisibleItemPosition();
            int limit = getLimit();
            int updatePosition = getAdapter().getItemCount() - 1 - (limit / 2);
            if (position >= updatePosition) {
                int offset = getAdapter().getItemCount() - 1;
                OffsetAndLimit offsetAndLimit = new OffsetAndLimit(offset, limit);
                scrollLoadingChannel.onNext(offsetAndLimit);
            }
        }
    });
}

// получение порядкового номера последнего видимого на экране элемента списка
// в зависимости от конкретного LayoutManager
private int getLastVisibleItemPosition() {
    Class recyclerViewLMClass = getLayoutManager().getClass();
    if (recyclerViewLMClass == LinearLayoutManager.class || LinearLayoutManager.class.isAssignableFrom(recyclerViewLMClass)) {
        LinearLayoutManager linearLayoutManager = (LinearLayoutManager)getLayoutManager();
        return linearLayoutManager.findLastVisibleItemPosition();
    } else if (recyclerViewLMClass == StaggeredGridLayoutManager.class || StaggeredGridLayoutManager.class.isAssignableFrom(recyclerViewLMClass)) {
        StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager)getLayoutManager();
        int[] into = staggeredGridLayoutManager.findLastVisibleItemPositions(null);
        List<Integer> intoList = new ArrayList<>();
        for (int i : into) {
            intoList.add(i);
        }
        return Collections.max(intoList);
    }
    throw new AutoLoadingRecyclerViewExceptions("Unknown LayoutManager class: " + recyclerViewLMClass.toString());
}

Вы, наверное, хотите спросить, откуда я взял вот это условие:

int updatePosition = getAdapter().getItemCount() - 1 - (limit / 2);

Выявлено оно было чисто эмпирическим путем при предположении, что среднее время запроса — 200-300мс. При данном условии «плавность скроллинга» никак не страдает от параллельной догрузки данных. Если у вас время запроса больше, то можно попробовать либо увеличить limit, либо немного поменять данное условие, чтобы подгрузка данных происходила немного пораньше.

Но все равно полностью от флуда канала мы не избавились. Когда условие начала подгрузки выполняется и скроллинг продолжается, канал продолжает нас «заваливать» сообщениями. И мы имеем все возможности послать несколько раз одинаковые запросы на подгрузку данных, да и backpressure никто не отменял — сетевой клиент может сломаться. Поэтому, как только мы получаем первое сообщение от канала, мы сразу же отписываемся от него, запускаем подгрузку данных, обновляем адаптер и список, и потом снова подписываемся к каналу, который уже не будет «флудить», так как поменяется условие (количество элементов в списке увеличится):

int updatePosition = getAdapter().getItemCount() - 1 - (limit / 2);

И так по циклу. А теперь внимание на код:

// метод подписки к каналу
private void subscribeToLoadingChannel() {
    Subscriber<OffsetAndLimit> toLoadingChannelSubscriber = new Subscriber<OffsetAndLimit>() {
        @Override
        public void onCompleted() {
        }

        @Override
        public void onError(Throwable e) {
            Log.e(TAG, "subscribeToLoadingChannel error", e);
        }

        @Override
        public void onNext(OffsetAndLimit offsetAndLimit) {
            // отписываемся от канала
            unsubscribe();
            // подгружаем новые данные
            loadNewItems(offsetAndLimit);
        }
    };
    // scrollLoadingChannel - это наш канал. смотри код выше
    subscribeToLoadingChannelSubscription = scrollLoadingChannel
            .subscribe(toLoadingChannelSubscriber);
}

// метод подгрузки данных
private void loadNewItems(OffsetAndLimit offsetAndLimit) {
    Subscriber<List<T>> loadNewItemsSubscriber = new Subscriber<List<T>>() {
        @Override
        public void onCompleted() {

        }

        @Override
        public void onError(Throwable e) {
            Log.e(TAG, "loadNewItems error", e);
            subscribeToLoadingChannel();
        }

        @Override
        public void onNext(List<T> ts) {
            // добавляем в адаптер подгруженные данные
            // конечно же, в стандартном адаптере нет метода addNewItems. мы используем кастомный адаптер, о нем ниже
            getAdapter().addNewItems(ts);
            // обновляем список
            getAdapter().notifyItemInserted(getAdapter().getItemCount() - ts.size());
            // если в ответе на запрос не пришли данные, значит их уже нет на сервере(БД), а значит цикл подгрузки можно заканчивать.
            // в противном случае начинается новая итерация цикла
            if (ts.size() > 0) {
                // обратно подписываемся к каналу
                subscribeToLoadingChannel();
            }
        }
    };
    // getLoadingObservable().getLoadingObservable(offsetAndLimit) - подгрузка данных через переданный AutoLoadingRecyclerView Observable. о нем тоже ниже
    loadNewItemsSubscription = getLoadingObservable().getLoadingObservable(offsetAndLimit)
            // подгрузка происходит в не UI потоке
            .subscribeOn(Schedulers.from(BackgroundExecutor.getSafeBackgroundExecutor()))
            // обработка результата происходит в UI (это добавление данных к адаптеру и обновление списка)
            // поэтому проблем с синхронизацией нет (доступ к списку элементов в адаптере с нескольких потоков исключен), 
            // и обновление View происходит в UI потоке  
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(loadNewItemsSubscriber);
}

Самое сложное позади. Нам удалось организовать безопасный цикл обновления списка. И все это внутри нашего AutoLoadingRecyclerView.

А для того, чтобы у нас сложилось целостное впечатление, внимание на полный код ниже:

OffsetAndLimit
/**
 * Offset and limit for {@link AutoLoadingRecyclerView AutoLoadedRecyclerView channel}
 *
 * @author e.matsyuk
 */
public class OffsetAndLimit {

    private int offset;
    private int limit;

    public OffsetAndLimit(int offset, int limit) {
        this.offset = offset;
        this.limit = limit;
    }

    public int getOffset() {
        return offset;
    }

    public int getLimit() {
        return limit;
    }

    @Override
    public String toString() {
        return "OffsetAndLimit{" +
                "offset=" + offset +
                ", limit=" + limit +
                '}';
    }
}


AutoLoadingRecyclerViewExceptions
/**
 * @author e.matsyuk
 */
public class AutoLoadingRecyclerViewExceptions extends RuntimeException {

    public AutoLoadingRecyclerViewExceptions() {
        super("Exception in AutoLoadingRecyclerView");
    }

    public AutoLoadingRecyclerViewExceptions(String detailMessage) {
        super(detailMessage);
    }
}


ILoading
/**
 * @author e.matsyuk
 */
public interface ILoading<T> {

    Observable<List<T>> getLoadingObservable(OffsetAndLimit offsetAndLimit);

}


AutoLoadingRecyclerViewAdapter
/**
 * Adapter for {@link AutoLoadingRecyclerView AutoLoadingRecyclerView}
 *
 * @author e.matsyuk
 */
public abstract class AutoLoadingRecyclerViewAdapter<T> extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

    private List<T> listElements = new ArrayList<>();

    public void addNewItems(List<T> items) {
        listElements.addAll(items);
    }

    public List<T> getItems() {
        return listElements;
    }

    public T getItem(int position) {
        return listElements.get(position);
    }

    @Override
    public int getItemCount() {
        return listElements.size();
    }
}


LoadingRecyclerViewAdapter
/**
 * @author e.matsyuk
 */
public class LoadingRecyclerViewAdapter extends AutoLoadingRecyclerViewAdapter<Item> {

    private static final int MAIN_VIEW = 0;

    static class MainViewHolder extends RecyclerView.ViewHolder {

        TextView textView;

        public MainViewHolder(View itemView) {
            super(itemView);
            textView = (TextView) itemView.findViewById(R.id.text);
        }
    }

    @Override
    public long getItemId(int position) {
        return getItem(position).getId();
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if (viewType == MAIN_VIEW) {
            View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_item, parent, false);
            return new MainViewHolder(v);
        }
        return null;
    }

    @Override
    public int getItemViewType(int position) {
        return MAIN_VIEW;
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        switch (getItemViewType(position)) {
            case MAIN_VIEW:
                onBindTextHolder(holder, position);
                break;
        }
    }

    private void onBindTextHolder(RecyclerView.ViewHolder holder, int position) {
        MainViewHolder mainHolder = (MainViewHolder) holder;
        mainHolder.textView.setText(getItem(position).getItemStr());
    }

}


AutoLoadingRecyclerView
/**
 * @author e.matsyuk
 */
public class AutoLoadingRecyclerView<T> extends RecyclerView {

    private static final String TAG = "AutoLoadingRecyclerView";
    private static  final int START_OFFSET = 0;

    private PublishSubject<OffsetAndLimit> scrollLoadingChannel = PublishSubject.create();
    private Subscription loadNewItemsSubscription;
    private Subscription subscribeToLoadingChannelSubscription;
    private int limit;
    private ILoading<T> iLoading;
    private AutoLoadingRecyclerViewAdapter<T> autoLoadingRecyclerViewAdapter;

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

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

    public AutoLoadingRecyclerView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    /**
     * required method
     * call after init all parameters in AutoLoadedRecyclerView
     */
    public void startLoading() {
        OffsetAndLimit offsetAndLimit = new OffsetAndLimit(START_OFFSET, getLimit());
        loadNewItems(offsetAndLimit);
    }

    private void init() {
        startScrollingChannel();
    }

    private void startScrollingChannel() {
        addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                int position = getLastVisibleItemPosition();
                int limit = getLimit();
                int updatePosition = getAdapter().getItemCount() - 1 - (limit / 2);
                if (position >= updatePosition) {
                    int offset = getAdapter().getItemCount() - 1;
                    OffsetAndLimit offsetAndLimit = new OffsetAndLimit(offset, limit);
                    scrollLoadingChannel.onNext(offsetAndLimit);
                }
            }
        });
    }

    private int getLastVisibleItemPosition() {
        Class recyclerViewLMClass = getLayoutManager().getClass();
        if (recyclerViewLMClass == LinearLayoutManager.class || LinearLayoutManager.class.isAssignableFrom(recyclerViewLMClass)) {
            LinearLayoutManager linearLayoutManager = (LinearLayoutManager)getLayoutManager();
            return linearLayoutManager.findLastVisibleItemPosition();
        } else if (recyclerViewLMClass == StaggeredGridLayoutManager.class || StaggeredGridLayoutManager.class.isAssignableFrom(recyclerViewLMClass)) {
            StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager)getLayoutManager();
            int[] into = staggeredGridLayoutManager.findLastVisibleItemPositions(null);
            List<Integer> intoList = new ArrayList<>();
            for (int i : into) {
                intoList.add(i);
            }
            return Collections.max(intoList);
        }
        throw new AutoLoadingRecyclerViewExceptions("Unknown LayoutManager class: " + recyclerViewLMClass.toString());
    }

    public int getLimit() {
        if (limit <= 0) {
            throw new AutoLoadingRecyclerViewExceptions("limit must be initialised! And limit must be more than zero!");
        }
        return limit;
    }

    /**
     * required method
     */
    public void setLimit(int limit) {
        this.limit = limit;
    }

    @Deprecated
    @Override
    public void setAdapter(Adapter adapter) {
        if (adapter instanceof AutoLoadingRecyclerViewAdapter) {
            super.setAdapter(adapter);
        } else {
            throw new AutoLoadingRecyclerViewExceptions("Adapter must be implement IAutoLoadedAdapter");
        }
    }

    /**
     * required method
     */
    public void setAdapter(AutoLoadingRecyclerViewAdapter<T> autoLoadingRecyclerViewAdapter) {
        if (autoLoadingRecyclerViewAdapter == null) {
            throw new AutoLoadingRecyclerViewExceptions("Null adapter. Please initialise adapter!");
        }
        this.autoLoadingRecyclerViewAdapter = autoLoadingRecyclerViewAdapter;
        super.setAdapter(autoLoadingRecyclerViewAdapter);
    }

    public AutoLoadingRecyclerViewAdapter<T> getAdapter() {
        if (autoLoadingRecyclerViewAdapter == null) {
            throw new AutoLoadingRecyclerViewExceptions("Null adapter. Please initialise adapter!");
        }
        return autoLoadingRecyclerViewAdapter;
    }

    public void setLoadingObservable(ILoading<T> iLoading) {
        this.iLoading = iLoading;
    }

    public ILoading<T> getLoadingObservable() {
        if (iLoading == null) {
            throw new AutoLoadingRecyclerViewExceptions("Null LoadingObservable. Please initialise LoadingObservable!");
        }
        return iLoading;
    }

    private void subscribeToLoadingChannel() {
        Subscriber<OffsetAndLimit> toLoadingChannelSubscriber = new Subscriber<OffsetAndLimit>() {
            @Override
            public void onCompleted() {
            }

            @Override
            public void onError(Throwable e) {
                Log.e(TAG, "subscribeToLoadingChannel error", e);
            }

            @Override
            public void onNext(OffsetAndLimit offsetAndLimit) {
                unsubscribe();
                loadNewItems(offsetAndLimit);
            }
        };
        subscribeToLoadingChannelSubscription = scrollLoadingChannel
                .subscribeOn(Schedulers.from(BackgroundExecutor.getSafeBackgroundExecutor()))
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(toLoadingChannelSubscriber);
    }

    private void loadNewItems(OffsetAndLimit offsetAndLimit) {
        Subscriber<List<T>> loadNewItemsSubscriber = new Subscriber<List<T>>() {
            @Override
            public void onCompleted() {

            }

            @Override
            public void onError(Throwable e) {
                Log.e(TAG, "loadNewItems error", e);
                subscribeToLoadingChannel();
            }

            @Override
            public void onNext(List<T> ts) {
                getAdapter().addNewItems(ts);
                getAdapter().notifyItemInserted(getAdapter().getItemCount() - ts.size());
                if (ts.size() > 0) {
                    subscribeToLoadingChannel();
                }
            }
        };

        loadNewItemsSubscription = getLoadingObservable().getLoadingObservable(offsetAndLimit)
                .subscribeOn(Schedulers.from(BackgroundExecutor.getSafeBackgroundExecutor()))
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(loadNewItemsSubscriber);
    }

    /**
     * required method
     * call in OnDestroy(or in OnDestroyView) method of Activity or Fragment
     */
    public void onDestroy() {
        scrollLoadingChannel.onCompleted();
        if (subscribeToLoadingChannelSubscription != null && !subscribeToLoadingChannelSubscription.isUnsubscribed()) {
            subscribeToLoadingChannelSubscription.unsubscribe();
        }
        if (loadNewItemsSubscription != null && !loadNewItemsSubscription.isUnsubscribed()) {
            loadNewItemsSubscription.unsubscribe();
        }
    }

}


По AutoLoadingRecyclerView нужно еще отметить, что мы не должны забывать про жизненный цикл и возможные утечки памяти со стороны RxJava. Поэтому, когда мы «убиваем» наш список, мы должны не забыть и отписаться от всех Subscribers.
А теперь взглянем на конкретное практическое применение нашего списка:
/**
 * A placeholder fragment containing a simple view.
 */
public class MainActivityFragment extends Fragment {

    private final static int LIMIT = 50;
    private AutoLoadingRecyclerView<Item> recyclerView;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {

        View rootView = inflater.inflate(R.layout.fragment_main, container, false);
        init(rootView);
        return rootView;
    }

    @Override
    public void onResume() {
        super.onResume();
        // старт подгрузки первой порции данных для отображения в списке
        // после этого включается уже автоматический режим догрузки данных
        recyclerView.startLoading();
    }

    private void init(View view) {
        recyclerView = (AutoLoadingRecyclerView) view.findViewById(R.id.RecyclerView);
        GridLayoutManager recyclerViewLayoutManager = new GridLayoutManager(getActivity(), 1);
        recyclerViewLayoutManager.supportsPredictiveItemAnimations();
        LoadingRecyclerViewAdapter recyclerViewAdapter = new LoadingRecyclerViewAdapter();
        recyclerViewAdapter.setHasStableIds(true);
        recyclerView.setLayoutManager(recyclerViewLayoutManager);
        recyclerView.setLimit(LIMIT);
        recyclerView.setAdapter(recyclerViewAdapter);
        recyclerView.setLoadingObservable(offsetAndLimit -> EmulateResponseManager.getInstance().getEmulateResponse(offsetAndLimit.getOffset(), offsetAndLimit.getLimit()));
    }

    @Override
    public void onDestroyView() {
        recyclerView.onDestroy();
        super.onDestroyView();
    }

}

Отличие AutoLoadingRecyclerView от стандартного RecyclerView лишь в добавлении методов setLimit, setLoadingObservable, onDestroy и startLoading. А в коробке у нас самоподгружаемый список. По-моему, это очень удобно, емко и красиво.

Исходный код с практическим примером вы можете посмотреть на GitHub. Пока что AutoLoadingRecyclerView представляет собой больше практическую реализацию идеи, нежели класс, который без проблем настраивается под любые нужды разработчика. Поэтому я буду очень рад вашим комментариям, предложениям, своим видением AutoLoadingRecyclerView и замечаниям.

Отдельную благодарность хотел бы выразить пользователю lNevermore за подготовку данного материала.

Комментарии (6)


  1. Zabelnikov
    19.10.2015 11:42

    Отличная статья, как раз столкнулся с задачей пагинации с использованием rx.


  1. Artem_zin
    20.10.2015 01:03

    Неплохо! Есть всякие мелочи по коду, но в целом норм :)

    Правда, offset, конечно, редко применяют в виде количества загруженных элементов т.к. пока пользователь смотрит текущий стейт данные могут добавиться/удалиться и будут проблемы (неотрисовка части данных или дубликаты), но это уже оффтопик.


    1. kolipass
      20.10.2015 04:46

      Есть архитектурная серебряная пуля для такой задачи? Что-то похожее на моей практики решалось целиком на сервере — клиент передавал timestamp запроса первой страницы, что конечно же лишало его «свежака», но по крайней мере это работало).


    1. xoxol_89
      20.10.2015 11:24

      Да, кстати. Если на сервере что-то параллельно меняется, то мы попадем в рассинхрон.
      Хорошо бы прикрутить еще абстракцию, позволяющую учитывать этот момент. Например, вынесение вычисления offset наружу через какой-нибудь интерфейс.


  1. lNevermore
    22.10.2015 00:39
    +1

    Хорошая статья, приятно читается и написана хорошо, спасибо вам.

    Хотел немного порассуджать про Subject. По моему мнению они выглядят как костыль над самими потоками. Потому что под источником подразумевается обычно замкнутая система, которая сама решает, как порождать ей элементы и когда это делать. И, как и любое костыль над контрактом, сабджекты провоцируют людей чаще их (сабджектов) использовать и само их использование иногда ведет к нарушению идеологии реактивных потоков:) Безусловно, бывает, что их применение оправдано. Обычно это делается в том случае, когда по-другому источник невозможно создать или в целях оптимизации.

    Когда мы говорили о задаче, я подразумевал, что мы будем создавать свой Observable, который будет эммитить объекты самостоятельно, а не делать это через сабджекты.

    Например, он может эммитить дату последнего айтема, чтобы загружать с этой даты новые, или adapter.getItemCount(), чтобы эммитить сразу оффсет, по которому идти, как у вас и сделано.
    Что-то типа:

    public final class ScrollObservable {
        public static Observable<Integer> from(final RecyclerView rv) {
            return Observable.create(subscriber -> {
                final RecyclerView.OnScrollListener sl = new RecyclerView.OnScrollListener() {
                    @Override
                    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                        if (!subscriber.isUnsubscribed()) {
                            final int position = getLastVisibleItemPosition();
                            final int limit = getLimit();
                            final int updatePosition = rv.getAdapter().getItemCount() - 1 - (limit / 2);
                            if (position >= updatePosition) {
                                subscriber.onNext(rv.getAdapter().getItemCount());
                            }
                        }
                    }
                };
                rv.addOnScrollListener(sl);
                subscriber.add(Subscriptions.create(() -> rv.removeOnScrollListener(sl)));
            });
        }
    }
    


    В этом случае получается Обсервабл, который начинает бешено эммитить айтемы при достижении определеного порога. Чтобы это происходило всего один раз, нам не надо делать подписки и отписки от сабджектов или что-то еще, просто применяем distinctUntilChanged и получаем новые оффсеты только раз:

    final Observable<Integer> offsetRequestObs = ScrollObservable.from(recyclerView).distinctUntilChanged();
    


    Соответственно в onNext мы не будем запускать новый Observable, потому что это тоже не очень круто, тк они получаются оторваны от жизненного цикла родителя. Просто делаем switchMap и потом перенаправляем на ui, где обрабатываем:

    offsetRequestObs.switchMap(offset -> getLoadingObservable(offset))
                    .subscribeOn(Schedulers.from(BackgroundExecutor.getSafeBackgroundExecutor()))
                    .observeOn(AndroidSchedulers.mainThread())
                    .subscribe(loadNewItemsSubscriber);
    


    В целом это решение делает то же самое, но, как мне кажется, оно лучше поддается расширению и это полноценный Observable, с котором можно делать все, что угодно. Кроме того, имеем связанный жизненный цикл потоков, что тоже важно.

    Как я и говорил выше, сабджекты обычно зло и без них очень часто можно сделать лучше и лаконичнее. Не понимаю, почему их все часто используют. Кроме того, важным фактом тут является еще то, что мы не делаем нового наследника RecyclerView, но и в вашем примере можно добиться того же.


    1. xoxol_89
      22.10.2015 07:42

      Спасибо, очень хороший комментарий!