Использование паттерна ViewHolder
С этой статьи я бы хотел начать цикл кратких заметок о разработке под мобильные устройства. Приложения под телефоны и планшеты — мое давнее увлечение. К сожалению плотно заняться которым, в силу различных обстоятельств, я смог только сейчас. В прочем хватит лирики, начнём. В качестве платформы я выбрал Android, как наиболее массовую и открытую систему. В качестве языка программирования сегодня будем использовать C#, писать будем на Xamarin с использованием Android API.
Практически любое мобильное приложение это некий экран с набором однотипных информационных элементов, для отображения которых обычно используется список или какой-либо грид. Из-за того, что платформа мобильная и работает на устройстве с небольшим объёмом памяти и слабым процессором (хаха! В моем первом компьютере было памяти раз в 15 меньше и процессор существенно слабее, чем сейчас в среднем телефоне) операционная система вынуждена ресурсы экономить. Соответственно если речь идёт о списке (ListView в Android), а если точнее об адаптере, который предоставляет данные для списка (например ArrayAdapter в Android) мы сталкиваемся с одной из особенностей, вызванных необходимостью беречь ресурсы.
Такая особенность заключается в том, что лист не создаёт ячейки под каждую из записей подключенного источника данных. Он создаёт лишь то количество ячеек, сколько может уместиться на экране в данный момент плюс по одной лишней ячейке сверху и снизу. Такое поведение характерно не только для Android, но и для iOS.
При этом когда пользователь прокручивает список система не удаляет ячейки, она старается их переиспользовать.
Когда ячейка пропадает из поля видимости пользователя она передаётся в метод GetView() адаптера, в котором происходит заполнение ячейки данными. В этом методе можно решить, переиспользовать уже готовую ячейку или создать новую.
Все бы ничего, но если лэйаут ячейки сложный то мы напрямую сталкиваемся с необходимостью проводить поиск элементов в лэйауте (TextView, ImageView и т. п.) для заполнения их актуальными данными. Вроде бы ничего страшного, вызвал FindViewById() с нужным id элемента, получил его и нет проблем, но выполнение метода FindViewById() это длительная операция, потому что она тянет за собой траверс по XML-коду лэйаута нашей ячейки. В итоге на сложных лэйаутах имеем просадку по производительности скроллинга списка.
Чтобы решить данную проблему нам необходимо использовать паттерн ViewHolder. Кратко его суть заключается в следующем:
- Реализуем класс, который в конструкторе принимает нашу View, делает траверс и ищет все её элементы, которые нам потребуются для заполнения данных;
- Записываем ссылки на эти элементы в соответствующие поля класса, снабжаем их геттерами, чтобы потом к ним обращаться;
- При создании нашей View — создаём наш класс-ViewHolder и записываем ссылку на него в свойстве Tag у нашей View;
- Теперь при попытке переиспользовать View нам не нужно делать повторный поиск элементов в лэйауте, достаточно получить ссылку на ViewHolder из свойства Tag и обратиться к соответствующим геттерам холдера;
- PROFIT.
Если делать реализацию на Java сложностей возникнуть не должно, но если использовать C# и Xamarin мы столкнемся с корнями, растущими из Java :-)
Предположим есть такой класс:
public class MyViewHolder{ public TextView MyTextView {get; private set;} public MyViewHolder(View view){ ... } }
Если теперь попытаться присвоить ссылку на него свойству Tag нашей View у нас ничего не получится. Поскольку Tag принимает объект типа Java.Lang.Object, а наш класс наследован от типа Object. Прямое кастование к нужному типу так же не приведёт к успеху. Кастование сначала к Object, а потом к Java.Lang.Object не даст ошибки компиляции, зато упадёт в рантайме.
Что же делать? Все просто :-) наследовать наш класс от типа Java.Lang.Object и будет счастье!
Небольшой пример того, как все это работает:
public class TimetableItemAdapter : ArrayAdapter<TimetableItem> { /// <summary> /// Класс ViewHolder, нужен что бы исключить множественный траверс по элементам вьюхи, для производительности /// </summary> public class TimetableItemViewHolder : Java.Lang.Object{ public TextView TimeText { get; private set;} public TextView DescriptionText { get; private set;} public ImageView AcceptButton { get; private set;} public TimetableItem BoundedItem { get; set;} public TimetableItemViewHolder(View view, TimetableItem item){ TimeText = view.FindViewById<TextView>(Resource.Id.timetable_time_text); DescriptionText = view.FindViewById<TextView>(Resource.Id.timetable_description_text); AcceptButton = view.FindViewById<ImageView>(Resource.Id.timetable_accept); BoundedItem = item; } } private MainActivity _context; public TimetableItemAdapter(Context context, int resource, IList<TimetableItem> objects) : base(context, resource, objects) { _context = (MainActivity)context; } public override View GetView(int position, View convertView, ViewGroup parent) { View resultView = null; TimetableItemViewHolder holder = null; TimetableItem item = GetItem(position); if(convertView != null){ resultView = convertView; holder = (TimetableItemViewHolder)convertView.Tag; holder.BoundedItem = item; }else{ resultView = ((LayoutInflater)_context.GetSystemService(Context.LayoutInflaterService)) .Inflate(Resource.Layout.timetable_list_item, parent, false); holder = new TimetableItemViewHolder(resultView, item); resultView.Tag = holder; } if(item != null && holder != null){ holder.TimeText.Text = item.Time; holder.DescriptionText.Text = item.Description; } return resultView; } }
Во ViewHolder не обязательно хранить только ссылки на элементы лэйаута, иногда может потребоваться хранить и какие-либо сопутствующие данные. Например в коде выше во ViewHolder записывается дополнительно ссылка на элемент из источника данных, для последующего использования где-нибудь. Но в данном случае нужно помнить, что при переиспользовании View нужно не забыть записать в свойство ссылку на новый элемент источника данных.