Як покращити Dart-колекції з бібліотекою dartx

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.

Привіт! Мене звати Анна, я експертка з мобільної розробки, GDE з Dart та Flutter, досвідчена розробниця мобільних та веб додатків на Flutter.

Хочу поділитися своїм досвідом з покращення читабельності та лаконічності Dart-коду у роботі з колекціями за допомогою бібліотеки dartx на прикладі повсякденних завдань Flutter-розробника.

Read in English.

Навіщо читати далі?

У мові програмування Dart класи Iterable та List надають базовий набір методів для модифікації та фільтрації списків. Однак відсутність бібліотеки на кшталт LINQ-to-Objects у C# у багатьох випадках залишає розробників без лаконічних однорядкових операцій. Звісно ж, будь-яку задачу можна вирішити, використовуючи лише методи з dart.core, проте нерідко це потребує рішення в декілька рядків, і такий код виходить неочевидним і складнішим для розуміння.

Бібліотека dartx надає розробникам можливість комбінування і використання елегантних та читабельних однорядкових операцій над колекціями (та іншими типами у Dart) завдяки механізму екстеншенів.

Зараз ми розглянемо, як наступні задачі вирішуються за допомогою базових методів з мови програмування Dart та з використанням методів бібліотеки dartx:

  • отримати перший / останній елемент списку або null / значення за замовчуванням / за умовою;
  • трансформувати / відфільтрувати / відвідати елементи списку в залежності від їх індексу;
  • перетворити список на мапу;
  • впорядкувати список;
  • виокремити унікальні елементи списку;
  • отримати найменше / найбільше / середнє арифметичне значення певного атрибута елементів списку;
  • вилучити елементи списку, що є null.

Та розглянемо декілька додаткових корисних екстеншенів.

Всі приклади вимагають посилання на пакет dartx у pubspec.yaml файлі, а також import у файлі з кодом:

import 'package:dartx/dartx.dart';

Отримання першого / останнього елемента списку...

... або null

Цей типовий код для отримання першого та останнього елементу списку у чистому Dart:

final first = list.first;

final last = list.last;

призведе до ексепшену StateError під час виконання програми, якщо список list порожній. Щоб його позбутися, доведеться перевіряти чи список не порожній і явно повертати null:

final firstOrNull = list.isNotEmpty ? list.first : null;

final lastOrNull = list.isNotEmpty ? list.last : null;

З використанням dartx ця задача спрощується до:

final firstOrNull = list.firstOrNull;

final lastOrNull = list.lastOrNull;

Аналогічно, цей код поверне null, якщо index поза межами list:

final elementAtOrNull = list.elementAtOrNull(index);

... або значення за замовчуванням

Пам’ятаючи, що гетери .first та .last призведуть до ексепшена, коли список list порожній, отримання першого та останнього елементів або вказаного значення у чистому Dart має такий вигляд:

final firstOrDefault = (list.isNotEmpty ? list.first : null) ?? defaultValue;

final lastOrDefault = (list.isNotEmpty ? list.last : null) ?? defaultValue;

З пакетом dartx:

final firstOrDefault = list.firstOrDefault(defaultValue);

final lastOrDefault = list.lastOrElse(defaultValue);

Аналогічно, цей код поверне defaultValue, якщо index поза межами list:

final elementAtOrDefault = list.elementAtOrDefault(index, defaultValue);

... за умовою

Таким є код для одержання першого та останнього елементів списку, що відповідають деякій умові або null у чистому Dart:

final firstWhere = list.firstWhere((x) => x.matches(condition));

final lastWhere = list.lastWhere((x) => x.matches(condition));

Неважко здогадатись, що і цей код може призводити до StateError ексепшену. Цю ситуацію можна виправити параметром orElse:

final firstWhereOrNull = list.firstWhere((x) => x.matches(condition), orElse: () => null);

final lastWhereOrNull = list.lastWhere((x) => x.matches(condition), orElse: () => null);

З dartx імплементація скорочується до:

final firstWhereOrNull = list.firstOrNullWhere((x) => x.matches(condition));

final lastWhereOrNull = list.lastOrNullWhere((x) => x.matches(condition));

... елементів списку в залежності від їх індексу

Трансформація...

Нерідкою є необхідність формування нового списку, де кожен елемент певним чином залежить від свого індексу в оригінальному списку. Для прикладу, розглянемо задачу трансформації кожного елементу у рядок String з додаванням його індексу.

Це один з варіантів реалізації на чистому Dart:

final newList = list.asMap()
  .map((index, x) => MapEntry(index, '$index $x'))
  .values
  .toList();

З dartx все набагато простіше:

final newList = list.mapIndexed((index, x) => '$index $x').toList();

Цей код містить .toList(), бо цей та більшість інших методів повертають ліниві Iterable.

Фільтрація...

Для іншого прикладу розглянемо задачу колекціонування тільки елементів з непарними індексами.

На чистому Dart це:

final oddItems = [];
for (var i = 0; i < list.length; i++) {
  if (i.isOdd) {
    oddItems.add(list[i]);
  }
}

Або в одному ланцюзі:

final oddItems = list.asMap()
  .entries
  .where((entry) => entry.key.isOdd)
  .map((entry) => entry.value)
  .toList();

З dartx це:

final oddItems = list.whereIndexed((x, index) => index.isOdd).toList();

або ж:

final oddItems = list.whereNotIndexed((x, index) => index.isEven).toList();

Відвідання...

Як вивести вміст списку, вказуючи серед іншого індекси елементів?

На чистому Dart:

for (var i = 0; i < list.length; i++) {
  print('$i: ${list[i]}');
}

З dartx:

list.forEachIndexed((element, index) => print('$index: $element'));

Перетворення списку на мапу

Припустимо, необхідно конвертувати список унікальних об’єктів Person на Map<String, Person>, де ключами є person.id, а значеннями —  повні об’єкти Person.

На чистому Dart це має такий вигляд:

final peopleMap = people.asMap().map((index, person) => MapEntry(person.id, person));

З dartx:

final peopleMap = people.associate((person) => MapEntry(person.id, person));

або:

final peopleMap = people.associateBy((person) => person.id);

А ось як виглядає реалізація задачі формування мапи, де ключами є дата DateTime, а значеннями — список List<Person> людей, які народились у цей день.

На чистому Dart:

final peopleMapByBirthDate = people.fold<Map<DateTime, List<Person>>>(
  <DateTime, List<Person>>{}, 
  (map, person) {
    if (!map.containsKey(person.birthDate)) {
      map[person.birthDate] = <Person>[];
    }
    map[person.birthDate].add(person);
    return map;
  },
);

Значно простішим є варіант з використанням dartx:

final peopleMapByBirthDate = people.groupBy((person) => person.birthDate);

Впорядкування списку

При використанні методу з чистого Dart потрібно пам’ятати, що:

list.sort();

модифікує оригінальний список list, та щоб отримати нову колекцію, потрібно змінити код на:

final orderedList = [...list]..sort();

dartx надає метод, що повертає новий екземпляр списку:

final orderedList = list.sorted();

та:

final orderedDescendingList = list.sortedDescending();

Припустимо, необхідно впорядкувати список на основі деякої властивості елементів.

На чистому Dart:

final orderedPeople = [...people]
  ..sort((person1, person2) => person1.birthDate.compareTo(person2.birthDate));

З dartx:

final orderedPeople = people.sortedBy((person) => person.birthDate);

та:

final orderedDescendingPeople = people.sortedByDescending((person) => person.birthDate);

До речі, з dartx також можливо впорядковувати списки за кількома ознаками:

final orderedPeopleByAgeAndName = people
  .sortedBy((person) => person.birthDate)
  .thenBy((person) => person.name);

та:

final orderedDescendingPeopleByAgeAndName = people
  .sortedByDescending((person) => person.birthDate)
  .thenByDescending((person) => person.name);

Виокремлення унікальних елементів списку

Один з варіантів вирішення цієї задачі на чистому Dart такий:

final unique = list.toSet().toList();

Ця реалізація не гарантує збереження порядку розташування елементів, а альтернативою є тільки рішення у кілька рядків.

З dartx реалізація спрощується до:

final unique = list.distinct().toList();

та:

final uniqueFirstNames = people.distinctBy((person) => person.firstName).toList();

Найменше / найбільше / середнє арифметичне значення певного атрибута елементів списку

Для отримання найменшого / найбільшого елемента списку можна, наприклад, його впорядкувати й взяти перший / останній елементи:

final min = ([...list]..sort()).first;

final max = ([...list]..sort()).last;

Той самий підхід можна використати для впорядкування за певною ознакою:

final minAge = (people.map((person) => person.age).toList()..sort()).first;

final maxAge = (people.map((person) => person.age).toList()..sort()).last;

або ж:

final youngestPerson = 
  ([...people]..sort((person1, person2) => person1.age.compareTo(person2.age))).first;

final oldestPerson = 
  ([...people]..sort((person1, person2) => person1.age.compareTo(person2.age))).last;

Неважко здогадатись, що dartx пропонує значно простіші рішення, які, до речі, повернуть null для порожнього списку:

final youngestPerson = people.minBy((person) => person.age);

final oldestPerson = people.maxBy((person) => person.age);

Якщо елементи списку реалізують Comparable, можна також використовувати методи без селекторів:

final min = list.min();

final max = list.max();

Також не є складними задачі отримання середнього арифметичного:

final average = list.average();

або ж:

final averageAge = people.averageBy((person) => person.age);

та суми num елементів списку або num властивостей комплексних елементів:

final sum = list.sum();

або ж:

final totalChildrenCount = people.sumBy((person) => person.childrenCount);

Вилучення елементів списку, що є null

У чистому Dart:

final nonNullItems = list.where((x) => x != null).toList();

З dartx:

final nonNullItems = list.whereNotNull().toList();

Більше корисних екстеншенів

Бібліотека dartx надає широкий набір корисних методів. Надалі ми не будемо зупинятися на кожному детально, проте я сподіваюсь, що назви і код достатньо очевидні.

joinToString

final report = people.joinToString(
  separator: '\n',
  transform: (person) => '${person.firstName}_${person.lastName}',
  prefix: '<<️',
  postfix: '>>',
);

all (every) / none

final allAreAdults = people.all((person) => person.age >= 18);

final allAreAdults = people.none((person) => person.age < 18);

first / second / third / fourth

final first = list.first;
final second = list.second;
final third = list.third;
final fourth = list.fourth;

takeFirst / takeLast

final youngestPeople = people.sortedBy((person) => person.age).takeFirst(5);

final oldestPeople = people.sortedBy((person) => person.age).takeLast(5);

firstWhile / lastWhile

final orderedPeopleUnder50 = people
  .sortedBy((person) => person.age)
  .firstWhile((person) => person.age < 50)
  .toList();

final orderedPeopleOver50 = people
  .sortedBy((person) => person.age)
  .lastWhile((person) => person.age >= 50)
  .toList();

І наостанок

Додам, що бібліотека dartx пропонує ще більше зручних екстеншенів для Iterable, List та інших типів у Dart. Найкращим способом ознайомитися з усіма її можливостями є перегляд програмного коду.

Викажіть авторам пакету підтримку, поставивши йому 👍🏻 на pub.dev та ⭐️ на GitHub.

Буду вдячна за лайк або комент для натхнення створювати більше подібного контенту. Підписуйтесь на мій Twitter.

👍НравитсяПонравилось6
В избранноеВ избранном2
LinkedIn
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Дякую, гарна стаття!
А ще є магічний rxdart для роботи із Stream. =)

Його найбільш корисна функція — debounce.
Припустимо у вас є поле пошуку, яке ініціює виклик REST API, якщо його текст змінено. Виконання виклику API для кожної введеної літери — дорого.
Отже, ви хотіли б робити виклик, лише якщо користувач перестав вводити дані.
Для цього і підходить чарівний debounce(), який проігнорує всі вхідні дані, якщо за ними не буде паузи.

Дякую, Богдан!
rxdart — дійсно бібліотека, варта уваги. Доречі, якщо debounce — це єдиний сценарій використання rxdart, то можна скористатись дуже компактним пакетом easy_debounce.

Не єдиний, але інші речі в основному зручний синтаксичний цукор. =)

Подписаться на комментарии