В этом посте мы рассмотрим, как оптимизировать производительность ListView NativeScript с помощью шаблонов с несколькими элементами. Примеры кода нацелены на NativeScript с Angular, но те же идеи применимы, если вы используете NativeScript без Angular.

Фон

Под капотом NativeScript использует собственные элементы управления списком для визуализации ListView. Плавная, как масло прокрутка в этих элементах управления зависит от двух основных функций:

  • Виртуализация пользовательского интерфейса - представления создаются только для тех элементов, которые в данный момент видны. Меньше элементов в памяти = ›меньше памяти.
  • Повторное использование представления - всякий раз, когда элемент прокручивается из порта просмотра, представление, которое его отображало, не уничтожается. Он помещается в пул переработанных представлений и повторно используется, когда появляется новый элемент.

Рендеринг различных элементов

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

Использование единого шаблона

Если вы какое-то время использовали angular, у вас может возникнуть соблазн применить ngIf/ngSwitch магию. Вы, вероятно, получите что-то вроде этого:

<ListView [items]="items">
  <ng-template let-item="item">
    <StackLayout>
      <GridLayout *ngIf="item.type === 'big'">
        <!-- big item template -->
      </GridLayout>
      <GridLayout *ngIf="item.type === 'small' && item.imageUrl">
        <!-- small item with image -->
      </GridLayout>
      <GridLayout *ngIf="item.type === 'small' && !item.imageUrl">
        <!-- small item with no image -->
      </GridLayout>
    </StackLayout>
  </ng-template>
</ListView>

Здесь происходит то, что переработка отходов больше не работает должным образом.

Допустим, большой элемент прокручивается из поля зрения, а его шаблон представления помещается в пул корзины, а затем повторно используется как элемент маленькое изображение. Модуль рендеринга angular должен будет создать все представления, содержащиеся во втором ngIf, и уничтожит все представления из первого ngIf. Единственной повторно используемой частью будет упаковка StackLayout (которая на самом деле не нужна).

Это означает, что это будет медленным и в то же время генерировать мусор. Вот как это выглядит:

Это было медленно! Мы ясно видим, как GC останавливает прокрутку.

Лучший подход

К счастью, есть лекарство. Существует способ указать ListView использовать различные шаблоны элементов в зависимости от заданных вами критериев элементов. Хорошая часть состоит в том, что он будет хранить переработанные представления в разных пулах и повторно использовать представление из правильного пула, когда это необходимо для рендеринга следующего элемента. Единственное, что нужно будет изменить, это привязки - не создавать / уничтожать представления.

Нет необходимости в ngIf и, следовательно, нет чрезмерного создания / уничтожения представлений пользовательского интерфейса каждый раз, когда представление повторно используется. В качестве бонуса мы можем избавиться от StackLayout, который раньше просто содержал 3 разных шаблона.

И вот код для этого:

<ListView [items]="items" [itemTemplateSelector]="templateSelector">
  <ng-template nsTemplateKey="big" let-item="item">
    <!-- big item template -->
  </ng-template>
  <ng-template nsTemplateKey="small" let-item="item">
    <!-- small item with image -->
  </ng-template>
  <ng-template nsTemplateKey="small-no-image" let-item="item">
    <!-- small item with no image -->
  </ng-template>
</ListView>

В разметке мы определяем 3 разных <template> элемента, дающих каждому имя с помощью директивы nsTemplateKey. Мы также даем ListView функцию itemTemplateSelector, которая определена в коде компонента. Он должен возвращать имя шаблона, который будет использоваться с фактическим элементом:

public templateSelector(item: NewsItem, index: number, items: NewsItem[]) {
  if (item.type === "big") {
    return "big"
  } 
  
  if (item.type === "small" && item.imageUrl) {
    return "small";
  }
  if (item.type === "small" && item.imageUrl) {
    return "small-no-image";
  }
  
  throw new Error("Unrecognized template!")
}

Посмотрим, как это ведет себя:

Аккуратный! Это именно то исполнение, которое мы ищем!

Заключение

Вот код как для сверхмедленного * ngIf шаблона, так и для сверхбыстрого селектора шаблонов. Заключительные мысли:

  • ngIf в шаблонах элементов ListView: 👎🐌👎
  • Несколько шаблонов + селектор шаблонов: 👍🚀👍