Clean Recycler Adapter. Часть 1

Введение




Списки, списки, списки… Вертикальные, горизонтальные, комбинированные. Практически ни одно мобильное приложение не обходится без них. Более того, нередко приложения состоят из одних только списков.

И если в “однородных” списках нет ничего страшного, то разные типы ячеек уже могут вызывать вопросы, основные из которых:

  • как облегчить изменение и масштабирование типов ячеек
  • как минимизировать количество мест для изменения, снизив риск потенциальных ошибок
  • как избавиться от if-else уродства
  • как избавиться от уродливых проверок на тип и опасных приведений типов

Что не так с if-else


В целом в “ифах” (сюда относятся конструкции вида if-else и switch-case) нет ничего страшного… пока они используются для бинарного выбора (например, пресловутая проверка на нулл). Но когда количество вариантов превышает два ответа, то это уже повод задуматься что не так с руками кодом и как это можно исправить.

Так почему обилие операторов выбора это плохо?

Ну во-первых, потому что “ифы” в одном месте практически всегда порождают “ифы” и в других местах кода. Таким образом получаем от одного до бесконечности (в пределе) мест для правки. И в случае необходимости внесения изменений очень просто забыть изменить “еще одно место”.

Во-вторых, если снова посмотреть на ситуацию в пределе, то мы можем получить бесконечное количество вариантов выбора. Что в коде “основного” класса будет выглядеть уродливо и станет местом потенциальных (и очень вероятных) ошибок.

Ну и в-третьих, множество “ифов” — это по-сути моветон, признак того что с архитектурной точки зрения все не так уж и радужно как могло показаться на первый взгляд.

Ну и как же можно исправить ситуацию?

Существует по крайней мере два пути решения проблемы.

Во-первых, воспользоваться так называемым “табличным методом” (Стив Макконнелл “Совершенный код”) и заменить логику выбора набором подготовленных данных. Что во-первых, избавляет от объема уродливого кода, а во-вторых, позволяет использовать внешние источники для предоставления этих самых данных, тем самым избавляя от необходимости внесения правок в сам код.

Во-вторых, можно использовать паттерн фабрика (Банда Четырех “Паттерны проектирования”) и инкапсулировать логику выбора в одном месте — в фабрике (помимо основной обязанности — сокрытия порождения новых однотипных объектов — фабрика также может инкапсулировать и логику выбора). Это не избавляет от “ифов” полностью, как предыдущий метод, но позволяет сократить количество таких мест до одного. Соответственно, код становится более красивым и легко поддерживаемым, т.к. в случае внесения изменений это нужно будет сделать ровно в одном месте.

Что не так с проверкой на тип


Проверка на тип сама по себе не несет ничего плохого. Более того, просматривая исходный код даже от самых крупных игроков в мире андроида, я нередко натыкался на такие проверки.

Но все же проверка на тип, на мой взгляд, это упущение в архитектуре (кстати, Скотт Майерс со мной солидарен). И если есть возможность избавиться от таких проверок, то это обязательно нужно сделать.

Как?

Первое что приходит на ум это уже знакомый “табличный метод”. Можно, например, подготовить коллекцию типа map, где заранее задать соответствие типов.

И второе. Но тут уже нет таких четких рекомендаций. Все будет зависеть от конкретного случая. Можно попробовать использовать Java Generics где это возможно. Можно очень внимательно посмотреть на такое свойство системы как полиморфизм. Как говорится, “Interfaces still working everywhere”.

Что не так с приведением типов


Хоть приведение типов можно встретить даже в Android SDK (например findViewById() или getSystemService()), это не делает эту процедуру безопасной. Приведение типов всегда несет в себе потенциальную угрозу падения приложения по ClassCastException.

Оборачивание “кастов” в блоки try-catch не лучший выход. Во-первых, сама эта конструкция выглядит достаточно уродливо. А во-вторых, такую проблему довольно непросто отлавливать, т.к. падений нет, а приложение ведет себя непредсказуемо.

Как вариант
Неплохим решением, кстати, здесь будет настройка Fabric на отправку всех non-fatal исключений. Бывают ситуации, когда трудозатраты на “правильное” решение перевешивают выгоду от его использования. Поэтому, как я уже неоднократно повторял, это повод задуматься как можно исправить ситуацию. И если решение слишком “дорогое”, то… это повод задуматься.

В любом случае, приведение типов не лучший выбор. И лучше его избегать.

Как?

Из основных рецептов это Java Generics и полиморфизм. Также нелишним будет учесть существование паттерна Посетитель (Банда Четырех “Паттерны проектирования”).

“Традиционный” подход


С проблемами разобрались. Теперь давайте вспомним, как решается задача показа “разнородного” списка “традиционным” способом. “Традиционный” он потому что во-первых, интернет нам подсказывает действовать именно так, а во-вторых, по моим личным наблюдениям подавляющее количество джуниоров и неподавляющее количество мидлов именно так и действуют.

К примеру, имеем три типа ячеек:

ProgressVo.java
/**
* Just a marker for progress header/footer.
*/
public class ProgressVo {
}


AdVo.java
public class AdVo {
   private String title;
   private String description;

   // Getters, Setters, Builder, etc.
}


UserVo.java
public class UserVo {
   private String firstName;
   private String lastName;
   private String age;

   // Getters, Setters, Builder, etc.
}


Сначала необходимо объявить константы под каждый тип ячейки:

private static final int TYPE_PROGRESS = 10;
private static final int TYPE_AD = 20;
private static final int TYPE_USER = 30;

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

@Override
public int getItemViewType(int position) {
   Object item = itemList.get(position);
   if (item instanceof ProgressVo) {
       return TYPE_PROGRESS;
   } else if (item instanceof AdVo) {
       return TYPE_AD;
   } else if (item instanceof UserVo) {
       return TYPE_USER;
   } else {
       throw new NoSuchRecyclerItemTypeException();
   }
}

И создать соответствующий холдер:

@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
   LayoutInflater inflater = LayoutInflater.from(parent.getContext());
   if (viewType == TYPE_PROGRESS) {
       View view = inflater.inflate(R.layout.cell_progress, parent, false);
       return new UsersRecyclerAdapter.ProgressViewHolder(view);
   } else if (viewType == TYPE_AD) {
       View view = inflater.inflate(R.layout.cell_ad, parent, false);
       return new UsersRecyclerAdapter.AdViewHolder(view);
   } else if (viewType == TYPE_USER) {
       View view = inflater.inflate(R.layout.cell_user, parent, false);
       return new UsersRecyclerAdapter.UserViewHolder(view);
   } else {
       throw new NoSuchRecyclerViewTypeException();
   }
}

А после еще и связать наш холдер с данными:

@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
   if (holder instanceof ProgressViewHolder) {
       // Do nothing.
   } else if (holder instanceof AdViewHolder) {
       ((AdViewHolder) holder).bind((AdVo) itemList.get(position));
   } else if (holder instanceof UserViewHolder) {
       ((UserViewHolder) holder).bind((UserVo) itemList.get(position));
   }
}

Весь класс выглядит следующим образом:

UsersUglyRecyclerAdapter.java
public class UsersUglyRecyclerAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
   private static final int TYPE_PROGRESS = 10;
   private static final int TYPE_AD = 20;
   private static final int TYPE_USER = 30;

   private List itemList = new ArrayList();

   public UsersUglyRecyclerAdapter() {
       itemList.add(new ProgressVo());
   }

   @Override
   public int getItemViewType(int position) {
       Object item = itemList.get(position);
       if (item instanceof ProgressVo) {
           return TYPE_PROGRESS;
       } else if (item instanceof AdVo) {
           return TYPE_AD;
       } else if (item instanceof UserVo) {
           return TYPE_USER;
       } else {
           throw new NoSuchRecyclerItemTypeException();
       }
   }

   @Override
   public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
       LayoutInflater inflater = LayoutInflater.from(parent.getContext());
       if (viewType == TYPE_PROGRESS) {
           View view = inflater.inflate(R.layout.cell_progress, parent, false);
           return new UsersRecyclerAdapter.ProgressViewHolder(view);
       } else if (viewType == TYPE_AD) {
           View view = inflater.inflate(R.layout.cell_ad, parent, false);
           return new UsersRecyclerAdapter.AdViewHolder(view);
       } else if (viewType == TYPE_USER) {
           View view = inflater.inflate(R.layout.cell_user, parent, false);
           return new UsersRecyclerAdapter.UserViewHolder(view);
       } else {
           throw new NoSuchRecyclerViewTypeException();
       }
   }

   @Override
   public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
       if (holder instanceof ProgressViewHolder) {
           // Do nothing.
       } else if (holder instanceof AdViewHolder) {
           ((AdViewHolder) holder).bind((AdVo) itemList.get(position));
       } else if (holder instanceof UserViewHolder) {
           ((UserViewHolder) holder).bind((UserVo) itemList.get(position));
       }
   }

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

   public void setUsers(List<UserVo> users) {
       itemList.clear();
       itemList.addAll(users);
       decorateItemList();
       notifyDataSetChanged();
   }

   private void decorateItemList() {
       int listSize = itemList.size();
       int shift = 0;
       for (int i = 1; i < listSize; i++) {
           if (i % 7 == 0) {
               itemList.add(i + shift, new AdVo());
               shift++;
           }
       }
       itemList.add(new ProgressVo());
   }

   protected static class ProgressViewHolder extends RecyclerView.ViewHolder {

       public ProgressViewHolder(View itemView) {
           super(itemView);
       }
   }

   protected static class AdViewHolder extends RecyclerView.ViewHolder {

       public AdViewHolder(View itemView) {
           super(itemView);
       }

       public void bind(AdVo ad) {
           // Bind ad...
       }
   }

   protected static class UserViewHolder extends RecyclerView.ViewHolder {

       public UserViewHolder(View itemView) {
           super(itemView);
       }

       public void bind(UserVo user) {
           // Bind user...
       }
   }
}


Что мы имеем в итоге? Три места с логикой выбора, обилие “ифоф”, проверок на тип и приведения типов. В случае нобходимости внесения изменений у нас аж 4 места для этого (без учета создания нового холдера при масштабировании).

Как бы непорядок и все такое. Давайте разбираться как можно исправить ситуацию.

Более “чистый” подход


Путей решения обозначенной проблемы может быть несколько. В рамках данной статьи мы используем адаптацию “табличного метода”, где в качестве “таблицы” будет выступать enum.

Наша цель — привести код в “однострочный” вид:

@Override
public int getItemViewType(int position) {
   return CellType.get(itemList.get(position)).type();
}

@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
   return CellType.get(viewType).viewHolder(parent);
}

@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
   CellType.get(itemList.get(position)).bind(holder, itemList.get(position));
}

Итак, для начала нам нужно определить типы используемых ячеек:

private enum CellType {
   PROGRESS,
   AD,
   USER
}

Начнем с того, что нам необходимо для определения типа ячейки и создания соответствующего холдера:

private enum CellType {
   PROGRESS {
       @Override
       int type() {
           return R.layout.cell_progress;
       }

       @Override
       RecyclerView.ViewHolder viewHolder(ViewGroup parent) {
           LayoutInflater inflater = LayoutInflater.from(parent.getContext());
           View view = inflater.inflate(R.layout.cell_progress, parent, false);
           return new ProgressViewHolder(view);
       }
   },
   AD {
       @Override
       int type() {
           return R.layout.cell_ad;
       }

       @Override
       RecyclerView.ViewHolder viewHolder(ViewGroup parent) {
           LayoutInflater inflater = LayoutInflater.from(parent.getContext());
           View view = inflater.inflate(R.layout.cell_ad, parent, false);
           return new AdViewHolder(view);
       }
   },
   USER {
       @Override
       int type() {
           return R.layout.cell_user;
       }

       @Override
       RecyclerView.ViewHolder viewHolder(ViewGroup parent) {
           LayoutInflater inflater = LayoutInflater.from(parent.getContext());
           View view = inflater.inflate(R.layout.cell_user, parent, false);
           return new UserViewHolder(view);
       }
   };

   abstract int type();

   abstract RecyclerView.ViewHolder viewHolder(ViewGroup parent);
}

Хочу обратить внимание, что в качестве viewType используется id разметки ячейки. Таким образом во-первых, нет нужды в определении констант, и во-вторых, уникальные id исключают конфликтные ситуации. Некоторые библиотеки могут резервировать под себя определенные константы или же текущий code-base делает это. А такие вещи легко забываются, что в итоге приводит к неприятным последствиям.

Т.к. android SDK в методах getItemViewType() и onBindViewHolder() использует позицию элемента в коллекции, а в методе onCreateViewHolder() переменную viewType, то нам потребуется два метода для получения соответствующего enum:

private enum CellType {
   PROGRESS {
       @Override
       boolean is(Object item) {
           return item instanceof ProgressVo;
       }
    ...
   },
   AD {
       @Override
       boolean is(Object item) {
           return item instanceof AdVo;
       }
   ...
   },
   USER {
       @Override
       boolean is(Object item) {
           return item instanceof UserVo;
       }
   ...
   };

   static CellType get(Object item) {
       for (CellType cellType : CellType.values()) {
           if (cellType.is(item)) {
               return cellType;
           }
       }
       throw new NoSuchRecyclerItemTypeException();
   }

   static CellType get(int viewType) {
       for (CellType cellType : CellType.values()) {
           if (cellType.type() == viewType) {
               return cellType;
           }
       }
       throw new NoSuchRecyclerViewTypeException();
   }

   abstract boolean is(Object item);
   ...
}

Метод is() в данном случае используется только для “внутренних нужд”.

Осталось только связать холдер с данными:

private enum CellType {
   PROGRESS {
   ...
       @Override
       void bind(RecyclerView.ViewHolder holder, Object item) {
           // Do nothing.
       }
   },
   AD {
    ...
       @Override
       void bind(RecyclerView.ViewHolder holder, Object item) {
           try {
               AdViewHolder adViewHolder = (AdViewHolder) holder;
               AdVo ad = (AdVo) item;
               adViewHolder.bind(ad);
           } catch (ClassCastException e) {
               L.printStackTrace(e);
           }
       }
   },
   USER {
    ...
       @Override
       void bind(RecyclerView.ViewHolder holder, Object item) {
           try {
               UserViewHolder userViewHolder = (UserViewHolder) holder;
               UserVo user = (UserVo) item;
               userViewHolder.bind(user);
           } catch (ClassCastException e) {
               L.printStackTrace(e);
           }
       }
   };
    ...
   abstract void bind(RecyclerView.ViewHolder holder, Object item);
}

Получившийся класс выглядит следующим образом:

UsersRecyclerAdapter.java
public class UsersRecyclerAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
   private List itemList = new ArrayList();

   public UsersRecyclerAdapter() {
       itemList.add(new ProgressVo());
   }

   @Override
   public int getItemViewType(int position) {
       return CellType.get(itemList.get(position)).type();
   }

   @Override
   public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
       return CellType.get(viewType).viewHolder(parent);
   }

   @Override
   public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
       Object item = itemList.get(position);
       CellType.get(item).bind(holder, item);
   }

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

   public void setUsers(List<UserVo> users) {
       itemList.clear();
       itemList.addAll(users);
       decorateItemList();
       notifyDataSetChanged();
   }

   private void decorateItemList() {
       int listSize = itemList.size();
       int shift = 0;
       for (int i = 1; i < listSize; i++) {
           if (i % 7 == 0) {
               itemList.add(i + shift, new AdVo());
               shift++;
           }
       }
       itemList.add(new ProgressVo());
   }

   private enum CellType {
       PROGRESS {
           @Override
           boolean is(Object item) {
               return item instanceof ProgressVo;
           }

           @Override
           int type() {
               return R.layout.cell_progress;
           }

           @Override
           RecyclerView.ViewHolder viewHolder(ViewGroup parent) {
               LayoutInflater inflater = LayoutInflater.from(parent.getContext());
               View view = inflater.inflate(R.layout.cell_progress, parent, false);
               return new ProgressViewHolder(view);
           }

           @Override
           void bind(RecyclerView.ViewHolder holder, Object item) {
               // Do nothing.
           }
       },
       AD {
           @Override
           boolean is(Object item) {
               return item instanceof AdVo;
           }

           @Override
           int type() {
               return R.layout.cell_ad;
           }

           @Override
           RecyclerView.ViewHolder viewHolder(ViewGroup parent) {
               LayoutInflater inflater = LayoutInflater.from(parent.getContext());
               View view = inflater.inflate(R.layout.cell_ad, parent, false);
               return new AdViewHolder(view);
           }

           @Override
           void bind(RecyclerView.ViewHolder holder, Object item) {
               try {
                   AdViewHolder adViewHolder = (AdViewHolder) holder;
                   AdVo ad = (AdVo) item;
                   adViewHolder.bind(ad);
               } catch (ClassCastException e) {
                   L.printStackTrace(e);
               }
           }
       },
       USER {
           @Override
           boolean is(Object item) {
               return item instanceof UserVo;
           }

           @Override
           int type() {
               return R.layout.cell_user;
           }

           @Override
           RecyclerView.ViewHolder viewHolder(ViewGroup parent) {
               LayoutInflater inflater = LayoutInflater.from(parent.getContext());
               View view = inflater.inflate(R.layout.cell_user, parent, false);
               return new UserViewHolder(view);
           }

           @Override
           void bind(RecyclerView.ViewHolder holder, Object item) {
               try {
                   UserViewHolder userViewHolder = (UserViewHolder) holder;
                   UserVo user = (UserVo) item;
                   userViewHolder.bind(user);
               } catch (ClassCastException e) {
                   L.printStackTrace(e);
               }
           }
       };

       static CellType get(Object item) {
           for (CellType cellType : CellType.values()) {
               if (cellType.is(item)) {
                   return cellType;
               }
           }
           throw new NoSuchRecyclerItemTypeException();
       }

       static CellType get(int viewType) {
           for (CellType cellType : CellType.values()) {
               if (cellType.type() == viewType) {
                   return cellType;
               }
           }
           throw new NoSuchRecyclerViewTypeException();
       }

       abstract boolean is(Object item);

       abstract int type();

       abstract RecyclerView.ViewHolder viewHolder(ViewGroup parent);

       abstract void bind(RecyclerView.ViewHolder holder, Object item);
   }

   protected static class ProgressViewHolder extends RecyclerView.ViewHolder {

       public ProgressViewHolder(View itemView) {
           super(itemView);
       }
   }

   protected static class AdViewHolder extends RecyclerView.ViewHolder {

       public AdViewHolder(View itemView) {
           super(itemView);
       }

       public void bind(AdVo ad) {
           // Bind ad...
       }
   }

   protected static class UserViewHolder extends RecyclerView.ViewHolder {

       public UserViewHolder(View itemView) {
           super(itemView);
       }

       public void bind(UserVo user) {
           // Bind user...
       }
   }
}


Еще чуть больше “чистоты”


В качестве альтернативы проверку на тип можно заменить еще одним “табличным методом”. Для проверки соответствия типов можно использовать коллекцию map.

Убираем метод is() и инициализируем соответствующую коллекцию map:

private enum CellType {
    ...
    static Map<Class, CellType> typeTable = new HashMap<>();

    static {
        typeTable.put(ProgressVo.class, PROGRESS);
        typeTable.put(AdVo.class, AD);
        typeTable.put(UserVo.class, USER);
    }

    static CellType get(Object item) {
        return typeTable.get(item.getClass());
    }
    …
}

Данный подход стоит рассматривать именно как альтернативный. Т.е. это такая полумера (решили вопрос с проверкой на тип, но не затронули преобразование типов), которая к тому же упрощает контракт enum.

Чем это грозит?

А тем что можно в горячке боя внесения изменений очень легко позабыть об этом typeTable и получить NPE.

Поддерживая же “полный” контракт (речь идет о методе is()) такая ситуация исключена.

Заключение


Итак, что мы получили на выходе?

Начали с этого:

Ugly Adapter
    private static final int TYPE_PROGRESS = 10;
    private static final int TYPE_AD = 20;
    private static final int TYPE_USER = 30;

    @Override
    public int getItemViewType(int position) {
        Object item = itemList.get(position);
        if (item instanceof ProgressVo) {
            return TYPE_PROGRESS;
        } else if (item instanceof AdVo) {
            return TYPE_AD;
        } else if (item instanceof UserVo) {
            return TYPE_USER;
        } else {
            throw new NoSuchRecyclerItemTypeException();
        }
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        LayoutInflater inflater = LayoutInflater.from(parent.getContext());
        if (viewType == TYPE_PROGRESS) {
            View view = inflater.inflate(R.layout.cell_progress, parent, false);
            return new UsersRecyclerAdapter.ProgressViewHolder(view);
        } else if (viewType == TYPE_AD) {
            View view = inflater.inflate(R.layout.cell_ad, parent, false);
            return new UsersRecyclerAdapter.AdViewHolder(view);
        } else if (viewType == TYPE_USER) {
            View view = inflater.inflate(R.layout.cell_user, parent, false);
            return new UsersRecyclerAdapter.UserViewHolder(view);
        } else {
            throw new NoSuchRecyclerViewTypeException();
        }
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        if (holder instanceof ProgressViewHolder) {
            // Do nothing.
        } else if (holder instanceof AdViewHolder) {
            ((AdViewHolder) holder).bind((AdVo) itemList.get(position));
        } else if (holder instanceof UserViewHolder) {
            ((UserViewHolder) holder).bind((UserVo) itemList.get(position));
        }
    }


И пришли к этому:

Clean Adapter
    @Override
    public int getItemViewType(int position) {
        return CellType.get(itemList.get(position)).type();
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        return CellType.get(viewType).holder(parent);
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        Object item = itemList.get(position);
        CellType.get(item).bind(holder, item);
    }

    private enum CellType {
        PROGRESS {
            @Override
            boolean is(Object item) {
                return item instanceof ProgressVo;
            }

            @Override
            int type() {
                return R.layout.cell_progress;
            }

            @Override
            RecyclerView.ViewHolder holder(ViewGroup parent) {
                LayoutInflater inflater = LayoutInflater.from(parent.getContext());
                View view = inflater.inflate(R.layout.cell_progress, parent, false);
                return new ProgressViewHolder(view);
            }

            @Override
            void bind(RecyclerView.ViewHolder holder, Object item) {
                // Do nothing.
            }
        },
        AD {
            @Override
            boolean is(Object item) {
                return item instanceof AdVo;
            }

            @Override
            int type() {
                return R.layout.cell_ad;
            }

            @Override
            RecyclerView.ViewHolder holder(ViewGroup parent) {
                LayoutInflater inflater = LayoutInflater.from(parent.getContext());
                View view = inflater.inflate(R.layout.cell_ad, parent, false);
                return new AdViewHolder(view);
            }

            @Override
            void bind(RecyclerView.ViewHolder holder, Object item) {
                try {
                    AdViewHolder adViewHolder = (AdViewHolder) holder;
                    AdVo ad = (AdVo) item;
                    adViewHolder.bind(ad);
                } catch (ClassCastException e) {
                    L.printStackTrace(e);
                }
            }
        },
        USER {
            @Override
            boolean is(Object item) {
                return item instanceof UserVo;
            }

            @Override
            int type() {
                return R.layout.cell_user;
            }

            @Override
            RecyclerView.ViewHolder holder(ViewGroup parent) {
                LayoutInflater inflater = LayoutInflater.from(parent.getContext());
                View view = inflater.inflate(R.layout.cell_user, parent, false);
                return new UserViewHolder(view);
            }

            @Override
            void bind(RecyclerView.ViewHolder holder, Object item) {
                try {
                    UserViewHolder userViewHolder = (UserViewHolder) holder;
                    UserVo user = (UserVo) item;
                    userViewHolder.bind(user);
                } catch (ClassCastException e) {
                    L.printStackTrace(e);
                }
            }
        };

        static CellType get(Object item) {
            for (CellType cellType : CellType.values()) {
                if (cellType.is(item)) {
                    return cellType;
                }
            }
            throw new NoSuchRecyclerItemTypeException();
        }

        static CellType get(int viewType) {
            for (CellType cellType : CellType.values()) {
                if (cellType.type() == viewType) {
                    return cellType;
                }
            }
            throw new NoSuchRecyclerViewTypeException();
        }

        abstract boolean is(Object item);

        abstract int type();

        abstract RecyclerView.ViewHolder holder(ViewGroup parent);

        abstract void bind(RecyclerView.ViewHolder holder, Object item);
    }


Пройдемся по обозначенным в начале статьи вопросам.

  • облегчить изменение и масштабирование типов ячеек
  • минимизировать количество мест для изменения, снизив риск потенциальных ошибок

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

  • избавиться от if-else уродства

Громоздкой и уродливой логики выбора больше нет. Нет и трех мест где эта логика использовалась. Риск ошибки в связи с этим исключен. Да и коллеги теперь не засмеют.

  • избавиться от уродливых проверок на тип и опасных приведений типов

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

Три с половиной из четырех поставленных вопросов решены. Так почему я предложил данный способ, если он не решает всех вопросов?

Ну во-первых, информации на одну статью и так в достатке.

Во-вторых, данный подход решает большинство вопросов и упрощает разработку и поддержку кода.

И в-третьих, этот способ очень прост, лаконичен и быстр. Т.е. чаша весов трудозатраты — выгода здесь однозначно на стороне выгоды. А выгоду получаем немалую.

Да, тема выглядит несколько холиварной неоднозначной и может породить споры. Но т.к. списки занимают чуть ли не ключевую роль в мобильных приложениях, а Android SDK не предоставляет красивого способа работы с разнотипными ячейками из коробки, то я посчитал нужным поделиться одним из неплохих способов решения данной проблемы.

До встречи во второй части, где мы поговорим о том как можно избавиться от проверок на тип и приведения типов.

Update


Вынужден написать небольшое пояснение к статье, т.к. в комментах встретил некоторое недопонимание. В статье представлена идея, показан подход к решению проблемы. Максимально простым языком, максимально простым кодом (один класс, enum, явные локальные переменные и т.д.).

Реализация этой идеи может быть различной, исходя из личных предпочтений. Но, как показала практика, от статьи люди ждут готового решения. Т.е. не метода решения проблемы, а конкретную реализацию этого метода.

Что ж, приму к сведению.

В комментах товарищ r_ii включил мозг и показал свою реализацию идеи. Ниже я поделюсь своей (дабы предвосхитить следующую волну возможных вопросов сразу включу в код и вариант обработки кликов).

Моя реализация идеи

public class UsersArbitraryCellAdapter extends ArbitraryCellAdapter {
    private ProgressArbitraryCell progressArbitraryCell = new ProgressArbitraryCell();
    private AdArbitraryCell adArbitraryCell = new AdArbitraryCell();
    private UserArbitraryCell userArbitraryCell = new UserArbitraryCell();

    public UsersArbitraryCellAdapter() {
        arbitraryCellSelector.addCell(progressArbitraryCell);
        arbitraryCellSelector.addCell(adArbitraryCell);
        arbitraryCellSelector.addCell(userArbitraryCell);
    }

    public Observable<AdVo> asAdObservable() {
        return adArbitraryCell.asAdObservable();
    }

    public Observable<UserVo> asUserObservable() {
        return userArbitraryCell.asUserObservable();
    }

	// Set Users, Ads, Progress...
}

public abstract class ArbitraryCellAdapter
        extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    protected ArbitraryCellSelector arbitraryCellSelector = new ArbitraryCellSelector();
    protected List itemList = new ArrayList();

    @Override
    public final int getItemViewType(int position) {
        return arbitraryCellSelector.getCell(itemList.get(position)).type();
    }

    @Override
    public final RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        return arbitraryCellSelector.getCell(viewType).holder(parent);
    }

    @Override
    public final void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        Object item = itemList.get(position);
        arbitraryCellSelector.getCell(item).bind(holder, item);
    }

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

public abstract class ArbitraryCellHolder<T> extends RecyclerView.ViewHolder {

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

    public abstract void bind(T item);
}

public final class ArbitraryCellSelector {
    private List<Cell> cellList = new ArrayList<>();

    public void addCell(Cell cell) {
        cellList.add(cell);
    }

    public void removeCell(Cell cell) {
        cellList.remove(cell);
    }

    public Cell getCell(Object item) {
        for (Cell cell : cellList) {
            if (cell.is(item)) {
                return cell;
            }
        }
        throw new NoSuchRecyclerRowException();
    }

    public Cell getCell(int viewType) {
        for (Cell cell : cellList) {
            if (cell.type() == viewType) {
                return cell;
            }
        }
        throw new NoSuchRecyclerRowException();
    }

    public interface Cell {

        boolean is(Object item);

        int type();

        RecyclerView.ViewHolder holder(ViewGroup parent);

        void bind(RecyclerView.ViewHolder holder, Object item);
    }
}

public class AdArbitraryCell implements ArbitraryCellSelector.Cell {
    private PublishSubject<AdVo> adPublishSubject = PublishSubject.create();

    @Override
    public boolean is(Object item) {
        return item instanceof AdVo;
    }

    @Override
    public int type() {
        return R.layout.cell_ad;
    }

    @Override
    public RecyclerView.ViewHolder holder(ViewGroup parent) {
        LayoutInflater inflater = LayoutInflater.from(parent.getContext());
        View view = inflater.inflate(R.layout.cell_ad, parent, false);
        return new AdViewHolder(view);
    }

    @Override
    public void bind(RecyclerView.ViewHolder holder, Object item) {
        try {
            AdViewHolder adViewHolder = (AdViewHolder) holder;
            AdVo ad = (AdVo) item;
            adViewHolder.bind(ad);
        } catch (ClassCastException e) {
            L.printStackTrace(e);
        }
    }

    public Observable<AdVo> asAdObservable() {
        return adPublishSubject;
    }

    protected class AdViewHolder extends ArbitraryCellHolder<AdVo> {
        @BindView(R.id.ad_text_view)
        protected TextView adTextView;

        public AdViewHolder(View itemView) {
            super(itemView);
        }

        @Override
        public void bind(AdVo item) {
            adTextView.setText(item.getTitle());

            itemView.setOnClickListener(view -> adPublishSubject.onNext(item));
        }
    }
}

// Other arbitrary cells...


Товарищ zagayevskiy обратил внимание на библиотеку Hannes Dorfmann, которая носит название AdapterDelegates. Подход выглядит добротным, решение — изящным. Рекомендую.
Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 18

    –1
    избавиться от if-else уродства


    Вы про switch слышали?
      +5
      Хрень редьки не слаще :)
        0
        Switch имеет ровно те же недостатки в данном случае.
        0
        Еще один очевидный метод (и правильный с точки зрения ООП) это дать возможность элементу «обработать» себя самому.
        Но это потребует расширения всех классов данных определенным интерфейсом, что может быть не очень удобным.
        Рассмотренный метод мне нравится больше.
          0
          Но это потребует расширения всех классов данных определенным интерфейсом
          Не обязательно. Можно обернуть объект с данными в объект реализующий этот интерфейс. При такой реализации можно избавиться от приведения элементов к нужному типу. Да и конечная реализация получится гибче.

          Однако, тут «вылезает» другой минус — необходимо оборачивать каждый объект данных в объект обработчик. По сути, адаптер должен работать не со списком данных, а со списком обработчиков этих данных. Выглядит ужасающе, но для небольших списков будет весьма элегантным решением.
            0
            Все правильно, именно такой подход позволяет избавиться от проверок на тип и преобразований типов. И это самый очевидный подход для решения этой задачи.

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

            Плюс только лишь оборачивание не решает всех поставленных вопросов.
            0
            Я сторонник «тонких» моделей. Т.е. модель здесь понимается в самом узком смысле — это просто DataObject. И вся его обязанность сводится к тому, чтобы предоставлять нам доступ к данным. Если обязать этот объект заниматься обработкой самого себя, то это уже будет нарушением принципа SRP.

            Но такой подход возможен. И даже может позволить избежать проверок на тип и приведений типов.
            –1

            Спасибо за статью.
            Подход выглядит неплохо.
            Смутило, что метод


            abstract RecyclerView.ViewHolder holder(ViewGroup parent);

            создает новый холдер, а из названия это совсем неясно. Лучше


            abstract RecyclerView.ViewHolder createHolder(Context ctx);

            Еще я бы избавился от неявных зависимостей и вынес весь класс CellType наружу.


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

              0
              Еще я бы избавился от неявных зависимостей и вынес весь класс CellType наружу.


              В боевых условиях именно так и делается :)

              Цель статьи — максимально просто и доступно донести идею. А как вы это реализуете под себя — вопрос десятый.
            • НЛО прилетело и опубликовало эту надпись здесь
                +1
                В идеале от класса CellType нужно избавляться. Точнее заменять его на не статичный объект. Не секрет, что enum в Java является статичным объектом, а статика может в определенных ситуациях стать головной болью и причиной падений.

                В статье enum использован для упрощения материала и простоты восприятия идеи. С этой же цель явно прописаны все методы в контракте enum и локальные переменные.
                0
                Я попробовал развить идею и вот что вышло:
                Класс CleanAdapter
                public class CleanAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
                	private final AdapterTypesMap typesMap = new AdapterTypesMap();
                
                	private List itemList; // TODO
                
                	public CleanAdapter() {
                		// Здесь компилятор следит чтоб ViewHolder соответствовал классу данных. Т.е. передача AdVo.class, ProgressViewHolder.class вызовет ошибку компиляции
                		typesMap.putItem(R.layout.cell_progress, R.layout.cell_progress, ProgressVo.class, ProgressViewHolder.class);
                		typesMap.putItem(R.layout.cell_ad, R.layout.cell_ad, AdVo.class, AdViewHolder.class);
                		typesMap.putItem(R.layout.cell_user, R.layout.cell_user, UserVo.class, UserViewHolder.class);
                	}
                
                	@Override
                	public int getItemCount() {
                		return itemList.size();
                	}
                
                	@Override
                	public int getItemViewType(int position) {
                		return typesMap.getItemViewType(itemList.get(position));
                	}
                
                	@Override
                	public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
                		return typesMap.createViewHolder(parent, viewType);
                	}
                
                	@Override
                	public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
                		typesMap.bindViewHolder(holder, itemList.get(position));
                	}
                }
                



                Класс AdapterTypesMap
                public class AdapterTypesMap {
                	private final Map<Integer /* view type */, AdapterItemType> viewTypesMap = new HashMap<>();
                	private final Map<Class /* adapter list item class */, AdapterItemType> listItemTypesMap = new HashMap<>();
                
                	public <T, V extends ViewHolderBase<T>> void putItem(int viewType, int resourceId, Class<T> dataItemClass, Class<V> viewHolderClass) {
                		AdapterItemType adapterItemType = new AdapterItemType(viewType, resourceId, viewHolderClass);
                		listItemTypesMap.put(dataItemClass, adapterItemType);
                		viewTypesMap.put(viewType, adapterItemType);
                	}
                
                	public int getItemViewType(Object obj) {
                		return listItemTypesMap.get(obj).getViewType();
                	}
                
                	public RecyclerView.ViewHolder createViewHolder(ViewGroup parent, int viewType) {
                		return viewTypesMap.get(viewType).createViewHolder(parent);
                	}
                
                	public void bindViewHolder(RecyclerView.ViewHolder holder, Object obj) {
                		listItemTypesMap.get(obj).bind(holder, obj);
                	}
                }
                



                Интерфейс IAdapterItemType
                public interface IAdapterItemType {
                	int getViewType();
                	RecyclerView.ViewHolder createViewHolder(ViewGroup parent);
                	void bind(RecyclerView.ViewHolder holder, Object item);
                }
                



                Реализация AdapterItemType
                public class AdapterItemType<T extends ViewHolderBase> implements IAdapterItemType {
                	private final Class<T> viewHolderClass;
                	private final int viewType;
                	private final int resourceId;
                
                	public AdapterItemType(int viewType, int resourceId, Class<T> viewHolderClass) {
                		this.viewHolderClass = viewHolderClass;
                		this.viewType = viewType;
                		this.resourceId = resourceId;
                	}
                
                	@Override
                	public int getViewType() {
                		return viewType;
                	}
                
                	@Override
                	public ViewHolderBase createViewHolder(ViewGroup parent) {
                		try {
                			View view = LayoutInflater.from(parent.getContext()).inflate(resourceId, parent, false);
                			// здесь не обойтись без рефлексии
                			return viewHolderClass.getConstructor(View.class).newInstance(view);
                		}
                		catch (Exception e) {
                			e.printStackTrace();
                			return null;
                		}
                	}
                
                	@Override
                	public void bind(RecyclerView.ViewHolder holder, Object item) {
                		ViewHolderBase holder2 = (ViewHolderBase) holder;
                		holder2.bind(item);
                	}
                }
                



                Базовый клас ViewHolderBase
                public abstract class ViewHolderBase<T> extends RecyclerView.ViewHolder {
                	public ViewHolderBase(View itemView) {
                		super(itemView);
                	}
                
                	public abstract void bind(T item);
                }
                



                И по одному наследнику классу ViewHolderBase для соответствующих классов данных. Типа такого:
                public class UserViewHolder extends ViewHolderBase<UserVo> {
                	public UserViewHolder(View itemView) {
                		super(itemView);
                	}
                
                	@Override
                	public void bind(UserVo item) {
                		// TODO
                	}
                }
                


                  0
                  Достойно :)
                    0
                    Там есть некоторые ошибки, но я думаю не составит труда их исправить тому кто надумает использовать :)
                    0
                    О — даже один минус есть. Т.е. кто-то предпочел-бы чтоб этого комментария здесь не было.
                    Хорошо если этот кто-то хоть что-то сделал для развития общества.
                    0

                    ООП-подход представлен в статье Writing Better Adapters

                      +2

                      Про Adapter Delegates автор не слышал? По-моему, второй вариант смотрится тоже не особо красиво.

                        0
                        По-моему, второй вариант смотрится тоже не особо красиво


                        Ребят, ну надо понимать что в статье используется максимально упрощенная модель. Все описано в одном классе, используется enum и т.д. Это сделано умышленно для упрощения восприятия идеи. Как вы реализуете эту идею — уже другой вопрос.

                        Вот как выглядит моя боевая реализация:

                        Вариант боевой реализации идеи
                        public class UsersArbitraryCellAdapter extends ArbitraryCellAdapter {
                        
                            public UsersArbitraryCellAdapter() {
                                this.arbitraryCellSelector.addCell(new ProgressArbitraryCell());
                                this.arbitraryCellSelector.addCell(new AdArbitraryCell());
                                this.arbitraryCellSelector.addCell(new UserArbitraryCell());
                            }
                        
                            public void setUsers(List<UserVo> userList) {
                        		// Set users, ads, progress...
                            }
                        }
                        
                        public abstract class ArbitraryCellAdapter
                        		extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
                            protected ArbitraryCellSelector arbitraryCellSelector = new ArbitraryCellSelector();
                            protected List itemList = new ArrayList();
                        
                            @Override
                            public final int getItemViewType(int position) {
                                return arbitraryCellSelector.getCell(itemList.get(position)).type();
                            }
                        
                            @Override
                            public final RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
                                return arbitraryCellSelector.getCell(viewType).holder(parent);
                            }
                        
                            @Override
                            public final void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
                                Object item = itemList.get(position);
                                arbitraryCellSelector.getCell(item).bind(holder, item);
                            }
                        
                            @Override
                            public final int getItemCount() {
                                return itemList.size();
                            }
                        }
                        
                        public abstract class ArbitraryCellHolder<T> extends RecyclerView.ViewHolder {
                        
                            public ArbitraryCellHolder(View itemView) {
                                super(itemView);
                                ButterKnife.bind(this, itemView);
                            }
                        
                            public abstract void bind(T item);
                        }
                        
                        public final class ArbitraryCellSelector {
                            private List<Cell> cellList = new ArrayList<>();
                        
                            public void addCell(Cell cell) {
                                cellList.add(cell);
                            }
                        
                            public void removeCell(Cell cell) {
                                cellList.remove(cell);
                            }
                        
                            public Cell getCell(Object item) {
                                for (Cell cell : cellList) {
                                    if (cell.is(item)) {
                                        return cell;
                                    }
                                }
                                throw new NoSuchRecyclerRowException();
                            }
                        
                            public Cell getCell(int viewType) {
                                for (Cell cell : cellList) {
                                    if (cell.type() == viewType) {
                                        return cell;
                                    }
                                }
                                throw new NoSuchRecyclerRowException();
                            }
                        
                            public interface Cell {
                        
                                boolean is(Object item);
                        
                                int type();
                        
                                RecyclerView.ViewHolder holder(ViewGroup parent);
                        
                                void bind(RecyclerView.ViewHolder holder, Object item);
                            }
                        }
                        
                        public class AdArbitraryCell implements ArbitraryCellSelector.Cell {
                        
                            @Override
                            public boolean is(Object item) {
                                return item instanceof AdVo;
                            }
                        
                            @Override
                            public int type() {
                                return R.layout.cell_ad;
                            }
                        
                            @Override
                            public RecyclerView.ViewHolder holder(ViewGroup parent) {
                                LayoutInflater inflater = LayoutInflater.from(parent.getContext());
                                View view = inflater.inflate(R.layout.cell_ad, parent, false);
                                return new AdViewHolder(view);
                            }
                        
                            @Override
                            public void bind(RecyclerView.ViewHolder holder, Object item) {
                                try {
                                    AdViewHolder adViewHolder = (AdViewHolder) holder;
                                    AdVo ad = (AdVo) item;
                                    adViewHolder.bind(ad);
                                } catch (ClassCastException e) {
                                    L.printStackTrace(e);
                                }
                            }
                        
                            protected class AdViewHolder extends ArbitraryCellHolder<AdVo> {
                                @BindView(R.id.ad_text_view)
                                protected TextView adTextView;
                        
                                public AdViewHolder(View itemView) {
                                    super(itemView);
                                }
                        
                                @Override
                                public void bind(AdVo item) {
                                    adTextView.setText(item.getTitle());
                        
                                    itemView.setOnClickListener(view -> adPublishSubject.onNext(item));
                                }
                            }
                        }
                        
                        // Other ArbitraryCells...
                        



                        Про Adapter Delegates автор не слышал?


                        Действительно не слышал. Как и многие другие, исходя из моего немалого опыта и оценки статьи сообществом.

                        Решение изящное, признаю. Что использовать у себя в проекте — импортировать библиотеку или написать один вспомогательный класс ArbitraryCellSelector — дело личного каждого.

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

                      Самое читаемое