Введение




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

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

  • как облегчить изменение и масштабирование типов ячеек
  • как минимизировать количество мест для изменения, снизив риск потенциальных ошибок
  • как избавиться от 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. Подход выглядит добротным, решение — изящным. Рекомендую.
Поделиться с друзьями
-->

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


  1. Alexey_Bespaly
    23.01.2017 17:38
    -1

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


    Вы про switch слышали?


    1. FirsofMaxim
      23.01.2017 17:46
      +5

      Хрень редьки не слаще :)


    1. Dimezis
      24.01.2017 17:03

      Switch имеет ровно те же недостатки в данном случае.


  1. r_ii
    23.01.2017 19:02

    Еще один очевидный метод (и правильный с точки зрения ООП) это дать возможность элементу «обработать» себя самому.
    Но это потребует расширения всех классов данных определенным интерфейсом, что может быть не очень удобным.
    Рассмотренный метод мне нравится больше.


    1. KamiSempai
      23.01.2017 20:13

      Но это потребует расширения всех классов данных определенным интерфейсом
      Не обязательно. Можно обернуть объект с данными в объект реализующий этот интерфейс. При такой реализации можно избавиться от приведения элементов к нужному типу. Да и конечная реализация получится гибче.

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


      1. ZemtsovVU
        24.01.2017 06:43

        Все правильно, именно такой подход позволяет избавиться от проверок на тип и преобразований типов. И это самый очевидный подход для решения этой задачи.

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

        Плюс только лишь оборачивание не решает всех поставленных вопросов.


    1. ZemtsovVU
      24.01.2017 06:38

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

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


  1. terrakok
    23.01.2017 19:04
    -1

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


    abstract RecyclerView.ViewHolder holder(ViewGroup parent);

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


    abstract RecyclerView.ViewHolder createHolder(Context ctx);

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


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


    1. ZemtsovVU
      24.01.2017 06:50

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


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

      Цель статьи — максимально просто и доступно донести идею. А как вы это реализуете под себя — вопрос десятый.


  1. Sirikid
    24.01.2017 01:11

    Можно написать CellType ещё компактнее
    private enum CellType {
        PROGRESS(R.layout.cell_progress, ProgressVo.class, ProgressViewHolder::new) {
            @Override
            void bind(RecyclerView.ViewHolder holder, Object item) {
                // Do nothing.
            }
        },
        AD(R.layout.cell_ad, AdVo.class, AdViewHolder::new) {
            @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(R.layout.cell_user, UserVo.class, UserViewHolder::new) {
            @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);
                }
            }
        };
    
        private final int type;
        private final Class<?> clazz;
        private final Function<View, RecyclerView.ViewHolder> viewHolderConstructor;
    
        CellType(int type, Class<?> clazz, Function<View, RecyclerView.ViewHolder> viewHolderConstructor) {
            this.type = type;
            this.clazz = clazz;
            this.viewHolderConstructor = viewHolderConstructor;
        }
    
        int getType() {
            return type;
        }
    
        boolean isInstance(Object obj) {
            return clazz.isInstance(obj);
        }
    
        RecyclerView.ViewHolder createViewHolder(ViewGroup parent) {
            final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
            final View view = inflater.inflate(type, parent, false);
            return viewHolderConstructor.apply(view);
        }
    
        abstract void bind(RecyclerView.ViewHolder holder, Object item);
    
        static CellType get(Object item) {
            for (CellType cellType : CellType.values()) {
                if (cellType.isInstance(item)) {
                    return cellType;
                }
            }
            throw new NoSuchRecyclerItemTypeException();
        }
    
        static CellType get(int viewType) {
            for (CellType cellType : CellType.values()) {
                if (cellType.getType() == viewType) {
                    return cellType;
                }
            }
            throw new NoSuchRecyclerViewTypeException();
        }
    }


    1. ZemtsovVU
      24.01.2017 06:48
      +1

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

      В статье enum использован для упрощения материала и простоты восприятия идеи. С этой же цель явно прописаны все методы в контракте enum и локальные переменные.


  1. r_ii
    24.01.2017 03:37

    Я попробовал развить идею и вот что вышло:

    Класс 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));
    	}
    }
    


    1. ZemtsovVU
      24.01.2017 06:55

      Достойно :)


      1. r_ii
        24.01.2017 11:28

        Там есть некоторые ошибки, но я думаю не составит труда их исправить тому кто надумает использовать :)


    1. r_ii
      24.01.2017 15:54

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


  1. turlir
    24.01.2017 06:31

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


  1. zagayevskiy
    24.01.2017 16:45
    +2

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


    1. ZemtsovVU
      25.01.2017 09:29

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


      Ребят, ну надо понимать что в статье используется максимально упрощенная модель. Все описано в одном классе, используется 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...