Pull to refresh

О чём молчит developer.android.com про RecyclerView?

Reading time6 min
Views11K

Вопрос о жизненном цикле (life cycle) активности (activity) или фрагмента (fragment) андроид-приложения чрезвычайно важен для практикующего андроидчика (андроид-разработчика). Почему? Потому что порядок выполнения обратных вызовов всех методов, связанных с состоянием жизненного цикла (onCreate(), onStart() и т.д.), жёстко задан и неправильное его применение приведёт к неработоспособности приложения. При чём здесь жизненный цикл? — спросит внимательный хаброчитатель. Ведь в заголовке, вроде бы, речь не о нём? Отвечаю: между жизненным циклом активности и работой RecyclerView есть нечто общее — это НАЛИЧИЕ ЖЁСТКОГО ПОРЯДКА выполнения методов обратного вызова при использовании данного виджета, и, следовательно, необходимость ЕГО ПРАВИЛЬНО ПРИМЕНЯТЬ.

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

Минимальный адаптер для RecyclerView


Например. Есть такой адаптер списка со стандартным минимальным нополнением:

Листинг 1


public class RvCustomAdapter extends
    RecyclerView.Adapter<RvCustomAdapter.CustomViewHolder> {

    private final Frag1 frag;
    private final LayoutInflater lInflater;
    private ArrayList<JSONDataSet> dataSet;
    ... ... ...

    public RvCustomAdapter(final Frag1 fragment) {
        this.frag = fragment;
        this.lInflater = (LayoutInflater) fragment.getContext()
                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        this.dataSet = new ArrayList<>();
    }

    ... ... ...

    @Override
    public CustomViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        // Создание нового вью
        View view = lInflater.inflate(R.layout.recycler_view_data_item, 
                                      parent, false);
        /**
        *  Здесь можно программно менять атрибуты 
        *  лэйаута (size, margins, paddings и др.)
        */
        RecyclerView.LayoutParams params = 
           (RecyclerView.LayoutParams) view.getLayoutParams();
        params.height = RecyclerView.LayoutParams.WRAP_CONTENT;
        view.setLayoutParams(params);
        return new CustomViewHolder(view);
    }

    // Заменяет контент отдельного view (вызывается layout manager-ом)
    @Override
    public void onBindViewHolder(@NonNull CustomViewHolder holder,
                                   int position) {
        holder.showData(position);
    }

    @Override
    public int getItemCount() {
        return dataSet.size();
    }
    /**
     *  Класс view holder-а с помощью которого мы получаем 
     *  ссылку на каждый элемент
     *  отдельного пункта списка и обрабатываем его нажатие
     */
    class CustomViewHolder extends RecyclerView.ViewHolder {
        ... ... ...
        @BindView(R.id.ll_Data)
        LinearLayout ll_Data;
        @BindView(R.id.cb_Data)
        CheckBox cb_Data;
        ... ... ...
        private JSONDataSet cur;

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

       /**
       *   Методы, размещающие данные в элементах списка
       *    и определяющие их реакцию на действия пользователя.
       */
       ... ... ...
}

В коде метода onBindViewHolder() адаптера нашего списка, элементы которого содержат чек-бокс (CheckBox), есть обращение к методу обработчика (holder'a), в котором считываются данные из подключенной к адаптеру коллекции и на их основании устанавливается его — чек-бокса — состояние, а также к различным элементам интерфейса подключаются необходимые слушатели (Listener):

Листинг 2


        void showData(final int position) {
            cur = dataSet.get(position);
            cb_Data.setChecked(cur.isChecked());
            ... ... ...
            cb_Data.setOnCheckedChangeListener(cb_DataOnCheckedChangeListener);
            ll_Data.setOnClickListener(ll_DataOnClickListener);
        }

        private OnClickListener ll_DataOnClickListener =
            new OnClickListener() {
                @Override
                public void onClick(View view) {
                    cur.setChecked(!cur.isChecked());
                    cb_Data.setChecked(cur.isChecked());
                }
            };

        private OnCheckedChangeListener cb_DataOnCheckedChangeListener =
            new OnCheckedChangeListener() {
                @Override
                public void onCheckedChanged(CompoundButton compoundButton,
                                             boolean checked) {
                     cur.setChecked(checked);
                     compoundButton.setChecked(checked);
                     setItemsColor(checked);
                     if (checked) {
                         if (...) {
                             (frag).addSelectedItemsCounter(cur);
                         } else {
                             cur.setChecked(!checked);
                             compoundButton.setChecked(!checked);
                             setItemsColor(!checked);
                             if (...) {
                                 createPrimaryDialog();
                             } else {
                                 createSecondaryDialog();
                             }
                         }
                     } else {
                         (frag).remSelectedItemsCounter(cur);
                     }
                 }
            };

Слушатели при установке флага и выполнении некоего условия изменяют данные в коллекции, а при его невыполнении выводят на экран или одно, или другое диалоговое окно.

Получается примерно так:


На Рис-1 — сформированный список. На Рис-2 — Отмеченный элемент списка. На Рис-3 — диалог, сообщающий о нарушении условия при отметке очередного элемента.

Для получения результата с Рис-1 менеджером разметки списка (LayoutManager'ом) выполняется такой порядок вызова необходимых функций:

Алгоритм 1


  1. Rv_Adapter.getItemCount() — проверяется количество элементов в коллекции;
  2. Rv_Adapter.onAttachedToRecyclerView( ) — адаптер подключается к виджету;
  3. Пока элементами списка не будет заполнено пространство экрана, отведённое под список выполняются следующие шаги алгоритма 2:

Алгоритм 2


  1. Rv_Adapter.onCreateViewHolder( ) — для каждого элемента коллекции создаётся свой обработчк;
  2. CustomViewHolder( ) — выполняется конструктор обработчика;
  3. Rv_Adapter.onBindViewHolder( ) — для каждого экземпляра запускается построитель вида (view);
  4. Rv_Adapter.onViewAttachedToWindow( ) — сформированный вид подключается к окну;

Всё замечательно! Если бы не «Но». Вернее НО!

Проблема


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

Устранение проблемы


Причина в том, что при написании кода адаптера МЫ НЕ УЧЛИ ПОРЯДОК ВЫПОЛНЕНИЯ функций обратного вызова, перечисленных здесь и здесь при прокрутке. А он таков:

Алгоритм 3


  1. При скрытии за границей видимости каждого элемента списка для связанного с ним экземпляра обработчика выполняется метод Rv_Adapter.onViewDetachedFromWindow(), который отключает скрывшийся вид от окна;
  2. При появлении из-за границы видимости каждого нового элемента списка (itemView) для связанного с ним экземпляра обработчика (handler) выполняется Алгоритм 2;

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

По этому Алгоритм 3 на самом деле выглядит так:

Алгоритм 3'


//Признак направления прокрутки списка: прямое - true, обратное - false:
bool direction;
if(direction){
     /** 
    *   отключаем вью от окна и сдвигаем её в следующую 
    *    позицию прямой очереди отключенных вью
    *    (назовём её directDetachedViews) 
    */
    Rv_Adapter.onViewDetachedFromWindow(holder);
    /**
    * Если в прямой очереди отключенных от окна 
    * вью их более, чем max
    */
    if(directDetachedViews.size() > max) {
              /**
              *   то переносим связанный с вью обработчик (holder) 
              *     из хвоста прямой очереди отключенных
              *     в голову прямой очереди  утилизированных 
              *     (назовём её directRecycleredHolders)
              */
             Rv_Adapter.onViewRecycled(holder);
    }
    /** 
    *   Если позиция появляющегося элемента 
    *     (visiblePos) меньше объёма коллекции, то
    */
    if(visiblePos < Rv_Adapter.getItemCount()) {
        /** 
        *    Если в голове обратной очереди отключенных
        *    от окна вью (назовём её reverseDetachedViews) 
        *    отсутствует вью (itemView), соответствующее позиции 
        *    появляющегося элемента (назовём её visiblePos),
        */
        if(reverseDetachedViews.content(itemView)){
            /**
            *   то если в голове обратной очереди утилизированных
            *    обработчиков (назовём её reverseRecycleredHolders)
            *    элементов отсутствует  holder, соответствующий его позиции 
            *    в коллекции, равный visiblePos, то создаём его
            */
            Rv_Adapter.onCreateViewHolder(itemView) -> {
                holder = CustomViewHolder(itemView);
            };
        } else {
            /**
            *   иначе - извлекаем его из головы обратной очереди
            *    утилизированных обработчиков (reverseRecycleredHolders)
            */
            holder = reverseRecycleredHolders.getHolder(visiblePos);
        }
            /**
            *    и формируем вью на основе данных соответствующего 
            *    элемента коллекции
            */
        Rv_Adapter.onBindViewHolder(holder, visiblePos);
    } else {
        /**
       *     иначе - извлекаем его из обратной очереди 
       *     отключенных от окна вью (reverseDetachedViews)
       */
       holder = reverseDetachedViews.getHolder(visiblePos)
    }
    //и подключаем его к окну
    Rv_Adapter.onViewAttachedToWindow(holder);
} else {
    ... ... ... ... ...
}


Из приведённого Алгоритма 3' видно, что в случае пролистывания списка более, чем на max количество позиций вью в нём будут создаваться заново, для чего будет задействован метод Rv_Adapter.onBindViewHolder(holder, visiblePos), который будет повторять действия пользователя.

Вывод и рекомендация


Для того, чтобы избежать повторения операций в методе onBindViewHolder(holder, visiblePos) при скроллинге списка на количество позиции, большее, чем max необходимо:

  1. Дополнить элементы коллекции полем с признаком вытеснения связанного с ним вью в очередь утилизированных обработчиков, например bool recycled;
  2. Вставить в метод onViewRecycled(holder) инструкции по установке этого признака, например ....setRecycled(true);
  3. Вставить в метод onBindViewHolder(holder, visiblePos) проверку этого признака, например if(!handler.cur.isRecycled())...;
  4. Вставить в метод onViewAttachedToWindow(holder) инструкцию по снятию этого признака, например ....setRecycled(false);

Например, так:

Листинг 3


    @Override
    public void onViewRecycled(@NonNull CustomViewHolder holder) {
        super.onViewRecycled(holder);
        holder.cur.setRecycled(true);
    }

    @Override
    public void onBindViewHolder(@NonNull CustomViewHolder holder, int position) {
        if (!holder.cur.isRecycled()){
            ... ... ...
        }
    }

    @Override
    public void onViewAttachedToWindow(@NonNull CustomViewHolder holder) {
        super.onViewAttachedToWindow(holder);
        holder.cur.setRecycled(false);
    }
Tags:
Hubs:
+3
Comments6

Articles