ListView является наиболее сложным, общий вид виджета в Android SDK. Он является частью семейства виджетов, известных как Adapter Views . Это представления, которые используют класс Adapter для передачи между виджетом представления и данными списка, которые он отображает.
Задача адаптера — подготовить представления, которые будут отображаться в ListView для каждого элемента в наборе данных. Это, как правило, связано со многими поисками findViewById (int), которые довольно затратны по времени ЦП. Согласно действующим рекомендациям для Android, адаптеры используют так называемый шаблон ViewHolder для снижения стоимости этих поисков.
Классы адаптеров, как правило, являются горячей точкой вонючего программного кода в Android, потому что они лежат между обязанностями View и Controller модели MVC (1) . В своей части выступления « Чистый код в приложениях для Android» на XebiCon 2013 я продемонстрировал, почему адаптеры становятся вонючими, почему ViewHolder не помогает и как использовать пользовательское представление для решения этой проблемы. Продолжайте читать для повторения.
Эта проблема
Рассмотрим следующее приложение:
На скриншоте показано простое приложение со списком контактов. Главный экран представляет собой один ListView, а элементы списка следуют только трем правилам форматирования:
- Поля адреса электронной почты и почтового адреса скрыты, если у них нет данных.
- Если в записи контакта нет имени, адрес электронной почты отображается в поле имени, а поле адреса электронной почты скрыто.
- Если в записи контакта нет ни имени, ни адреса электронной почты, в поле имени отображается значение по умолчанию.
Эти простые правила приводят к методу getView в ContactListAdapter, который уже содержит около тридцати строк кода:
// https://github.com/xebia/xebicon-2013__cc-in-aa/blob/1-_naive_adapter/src/com/xebia/xebicon2013/cciaa/ContactListAdapter.java public View getView(int position, View convertView, ViewGroup parent) { final Contact item = getItem(position); final View view = (convertView == null) ? inflater.inflate(R.layout.list_item, null) : convertView; TextView nameView = ((TextView) view.findViewById(R.id.contact_name)); if (item.getName() != null) { nameView.setText(item.getName()); } else if (item.getEmail() != null) { nameView.setText(item.getEmail()); } else { nameView.setText(R.string.unidentified); } TextView emailView = (TextView) view.findViewById(R.id.contact_email); if (item.getEmail() != null) { emailView.setText(item.getEmail()); emailView.setVisibility(item.getName() == null ? View.GONE : View.VISIBLE); } else { emailView.setVisibility(View.GONE); } TextView addressView = (TextView) view.findViewById(R.id.contact_address); if (item.getAddressLines() != null) { addressView.setText(item.getAddressLines()); addressView.setVisibility(View.VISIBLE); } else { addressView.setVisibility(View.GONE); } return view; }
Теперь представьте больше полей в вашем дочернем представлении, больше дочерних типов в вашем адаптере и больше наборов правил форматирования. Количество кода форматирования растет в геометрической прогрессии. Вы можете использовать рефакторинг метода extract, чтобы разрезать метод getView на более мелкие части, но это устраняет симптомы, игнорируя причину. Реальная проблема заключается в том , что код плохо структурирован, и это очевидно , когда вы представляете , какие компоненты взаимодействуют:
Шаблон ViewHolder не является решением
В предыдущем примере не используется шаблон ViewHolder, упомянутый во введении. В лучших практиках использования адаптера вида посоветуют использовать этот шаблон , и это довольно популярные . ViewHolder — это вспомогательный класс, который содержит ссылки на все дочерние элементы представления элемента списка. Он хранится в теге корневого представления каждого элемента списка. Таким образом, вы должны заполнить эти ссылки только один раз. Вот типичный класс ViewHolder:
// https://github.com/xebia/xebicon-2013__cc-in-aa/blob/2-_ViewHolder_pattern/src/com/xebia/xebicon2013/cciaa/ViewHolder.java public class ViewHolder { public final TextView nameView; public final TextView emailView; public final TextView addressView; public ViewHolder(View listItem) { nameView = (TextView) listItem.findViewById(R.id.contact_name); emailView = (TextView) listItem.findViewById(R.id.contact_email); addressView = (TextView) listItem.findViewById(R.id.contact_address); listItem.setTag( this ); } }
Вот как выглядит ContactListAdapter при реализации с использованием шаблона ViewHolder:
// https://github.com/xebia/xebicon-2013__cc-in-aa/blob/2-_ViewHolder_pattern/src/com/xebia/xebicon2013/cciaa/ContactListAdapter.java public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder; if (convertView == null) { convertView = inflater.inflate(R.layout.list_item, null); holder = new ViewHolder(convertView); } else { holder = (ViewHolder) convertView.getTag(); } Contact item = getItem(position); String name = item.getName(); String email = item.getEmail(); String address = item.getAddressLines(); if (name != null) { holder.nameView.setText(name); } else if (email != null) { holder.nameView.setText(email); } else { holder.nameView.setText(R.string.unidentified); } if (email != null) { holder.emailView.setText(email); holder.emailView.setVisibility(name == null ? View.GONE : View.VISIBLE); } else { holder.emailView.setVisibility(View.GONE); } if (address != null) { holder.addressView.setText(address); holder.addressView.setVisibility(View.VISIBLE); } else { holder.addressView.setVisibility(View.GONE); } return convertView; }
ViewHolder не упрощает код в классе адаптера. Это оптимизация производительности, чтобы избежать затрат на повторный поиск findViewById (int). Взгляд на структуру компонентов показывает, что это усложняет ситуацию:
Пользовательская ViewGroup позволяет вам съесть свой торт и иметь его
Вы можете сделать намного лучше, используя Custom View Group для своих элементов списка. Вы получаете преимущества ViewHolder, и ясность вашего кода улучшается. Это все, что осталось от оригинального метода getView:
// https://github.com/xebia/xebicon-2013__cc-in-aa/blob/3-_custom_ViewGroup/src/com/xebia/xebicon2013/cciaa/ContactListAdapter.java public View getView(int position, View convertView, ViewGroup parent) { ContactView view; if (convertView == null) { view = (ContactView) inflater.inflate(R.layout.list_item, null); } else { view = (ContactView) convertView; } Contact item = getItem(position); view.showContact(item); return view; }
Этот выигрыш достигается путем разделения обязанностей View и Controller. Новый класс View имеет общедоступный API-интерфейс, определенный полностью в терминах модели вашего домена, освобождая класс адаптера для обработки только ответственности контроллера. Структурная схема раскрывает эту простоту:
Как создать пользовательскую группу представлений?
Создание настраиваемой группы представлений аналогично созданию настраиваемого представления. Вы начинаете с создания подкласса существующего класса View. В нашем примере корневым элементом list_item.xml является <LinearLayout, поэтому мы расширяем класс android.widget.LinearLayout. Вам нужно только добавить конструкторы суперкласса, чтобы получить работающее настраиваемое представление:
public class ContactView extends LinearLayout { /** Inherited constructor. */ public ContactView(Context context) { super(context); } /** Inherited constructor. */ public ContactView(Context context, AttributeSet attrs) { super(context, attrs); } /** Inherited constructor. */ public ContactView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } }
Чтобы интегрировать его в макет, просто измените корневой тег XML макета с <LinearLayout на полное имя класса пользовательской группы представлений <com.xebia.xebicon2013.cciaa.ContactView. Вот файл layout / list_item.xml, измененный для использования новой пользовательской группы представлений:
<!-- https://github.com/xebia/xebicon-2013__cc-in-aa/blob/3-_custom_ViewGroup/res/layout/list_item.xml --></p> <com.xebia.xebicon2013.cciaa.ContactView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:orientation="vertical" android:layout_height="match_parent"> <!-- view group contents unchanged --> </com.xebia.xebicon2013.cciaa.ContactView>
Пользовательское представление только с конструкторами бессмысленно. Чтобы воспользоваться преимуществами пользовательского класса представления, реализуйте обратный вызов onFinishInflate (), чтобы найти ссылки на представление и поместить код приложения в остальную часть класса. В данном случае это метод showContact (Contact), который мы видели в наших предыдущих адаптерах:
// https://github.com/xebia/xebicon-2013__cc-in-aa/blob/3-_custom_ViewGroup/src/com/xebia/xebicon2013/cciaa/ContactView.java public class ContactView extends LinearLayout { public static final Contact EMPTY = new Contact(null, null, null, null); private TextView nameView; private TextView emailView; private TextView addressView; private Contact contact = EMPTY; /** Inherited constructors as before. */ @Override protected void onFinishInflate() { super.onFinishInflate(); nameView = (TextView) findViewById(R.id.contact_name); emailView = (TextView) findViewById(R.id.contact_email); addressView = (TextView) findViewById(R.id.contact_address); } public void showContact(Contact contact) { this.contact = (contact != null ? contact : EMPTY); String name = contact.getName(); String email = contact.getEmail(); String address = contact.getAddressLines(); if (name != null) { nameView.setText(name); } else if (email != null) { nameView.setText(email); } else { nameView.setText(R.string.unidentified); } if (email != null) { emailView.setText(email); emailView.setVisibility(name == null ? View.GONE : View.VISIBLE); } else { emailView.setVisibility(View.GONE); } if (address != null) { addressView.setText(address); addressView.setVisibility(View.VISIBLE); } else { addressView.setVisibility(View.GONE); } } }
Чтобы подвести итог
Настраиваемый подход к группе представлений имеет ряд структурных преимуществ по сравнению с шаблоном ViewHolder:
- Он демонстрирует более высокую когезию и более низкую связь, чем подход ViewHolder.
- Класс адаптера работает на естественном уровне абстракции — дочерних представлениях — без погружения в детали низкого уровня.
- Код больше не использует нетипизированное и изменяемое извне свойство тега корневого представления.
- Подробная условная логика не исчезла (это существенная сложность ), но она была ограничена максимально узкой областью применения. Некоторые из этих сложностей могут быть перенесены в другие пользовательские представления.
- Если одна и та же группа представлений используется как дочерняя в более чем одном адаптере, ни один из низкоуровневого кода не нужно копировать в новый адаптер.
Пользовательская группа представлений имеет только преимущества для ViewHolder, поскольку сохраняется преимущество в производительности, позволяющее избегать ненужных поисков findViewById (int). Количество пользовательских классов одинаково, и экземпляра объекта на один меньше.
Означает ли все это, что ViewHolder Pattern действительно вреден? Это слишком резко. Тем не менее, преимущества использования пользовательской ViewGroup таковы, что я считаю, что пришло время удалить шаблон ViewHolder.
Полный пример кода для этой статьи находится на github: xebia / xebicon-2013__cc-in-aa . Три подхода находятся в трех разных ветвях: 1-_naieve_adapter , 2-_ViewHolder_Pattern и 3-_custom_ViewGroup .
1) Я использую «MVC» в качестве имени семейства шаблонов, которое включает MVP и MVVM, а также MVC. То, что реализует Android, — упражнение для читателя.