Pull to refresh

Clean Recycler Adapter. Часть 1

Reading time 18 min
Views 15K

Введение




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

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

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

Articles