Flutter і Dart 3: Records, Patterns та складні питання для співбесід

Dart 3 records, pattern matching та sealed classes з прикладами коду Flutter. Вичерпне зіставлення шаблонів, моделювання станів та питання для технічних інтерв'ю.

Dart 3 records patterns та sealed classes для розробки на Flutter

Dart 3 кардинально змінив архітектуру Flutter-застосунків, запровадивши три потужні мовні конструкції: records, pattern matching та sealed classes. Ці нововведення виходять далеко за межі синтаксичного цукру. Вони перебудовують підхід до моделювання даних, управління станом та забезпечення типобезпеки на етапі компіляції. Для розробників, що готуються до технічних співбесід з Flutter, вільне володіння цими концепціями стало обов'язковою вимогою у 2026 році.

Ключові тези для співбесіди

Dart 3 додає records (легковагі структурні типи даних), вичерпний pattern matching та sealed classes. Разом ці три механізми дозволяють моделювати складні стани з гарантованою типобезпекою на рівні компілятора, без залежності від сторонніх бібліотек.

Records: легковагі структурні типи даних

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

Позиційні records підходять для простих випадків, коли порядок полів достатньо ідентифікує кожне значення. Для складніших структур іменовані поля суттєво покращують читабельність та зменшують ймовірність помилок при переплутуванні параметрів.

user_repository.dartdart
// Returning multiple values with a positional record
(String, int) parseUserInput(String raw) {
  final parts = raw.split(':');
  return (parts[0].trim(), int.parse(parts[1]));
}

// Named fields improve readability for complex returns
({String name, String email, bool isVerified}) fetchUserProfile(String id) {
  // Simulated database lookup
  return (
    name: 'Alice Chen',
    email: 'alice@example.com',
    isVerified: true,
  );
}

Фундаментальною властивістю records є структурна рівність. Два records з однаковими значеннями вважаються рівними без необхідності імплементації оператора == чи використання пакетів на кшталт equatable. Ця характеристика значно спрощує модульне тестування та порівняння станів.

records_equality.dartdart
// Structural equality — no need for custom == operator
void main() {
  final a = (1, 'hello');
  final b = (1, 'hello');
  print(a == b); // true — records compare by value

  // Named fields also support equality
  final profile1 = (name: 'Alice', role: 'admin');
  final profile2 = (name: 'Alice', role: 'admin');
  print(profile1 == profile2); // true
}
Records не є класами

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

Pattern Matching: деструктуризація для ясності коду

Pattern matching у Dart 3 виходить далеко за межі простої деструктуризації. Механізм дозволяє витягувати значення, перевіряти типи та застосовувати захисні умови (guard clauses) в єдиному декларативному синтаксисі, що перевіряється на етапі компіляції.

Деструктуризація records усуває повторювані звернення за індексом або ім'ям поля. Патерни для списків з оператором rest (...) надають елегантний спосіб отримати перші елементи колекції.

pattern_basics.dartdart
// Destructuring a record with pattern matching
void main() {
  final (name, email, isVerified) = fetchUserProfile('u-123');
  // name, email, isVerified are now local variables

  // List patterns with rest operator
  final scores = [98, 87, 92, 76, 84];
  final [first, second, ...remaining] = scores;
  print('Top two: $first, $second'); // 98, 87
  print('Others: $remaining');        // [92, 76, 84]
}

Switch-вирази у Dart 3 являють собою суттєву еволюцію порівняно з традиційними switch-інструкціями. Вони повертають значення, підтримують захисні умови з ключовим словом when та забезпечують pattern matching за типами об'єктів. Компілятор перевіряє, що всі випадки охоплені, що виключає цілу категорію помилок, пов'язаних з необробленими гілками.

pattern_switch.dartdart
// Switch expression with guard clauses
String classifyScore(int score) => switch (score) {
  >= 90 => 'Excellent',
  >= 80 => 'Good',
  >= 70 => 'Average',
  >= 60 => 'Below Average',
  _ => 'Needs Improvement',
};

// Object pattern matching with type checking
String describeValue(Object value) => switch (value) {
  int n when n < 0   => 'Negative integer: $n',
  int n              => 'Positive integer: $n',
  String s when s.isEmpty => 'Empty string',
  String s           => 'String of length ${s.length}',
  List l when l.isEmpty  => 'Empty list',
  List l             => 'List with ${l.length} elements',
  _                  => 'Unknown type',
};

Ці switch-вирази ефективно замінюють ланцюги if-else у багатьох контекстах, зокрема при управлінні станом та побудові віджетів у Flutter-застосунках.

Sealed Classes: вичерпність, гарантована компілятором

Sealed classes визначають закритий набір підтипів. Компілятор має повну інформацію про всі можливі варіанти, що дозволяє перевіряти, чи кожен switch-вираз охоплює кожен випадок. Пропущений підтип спричинить помилку компіляції, а не баг у продакшні.

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

auth_state.dartdart
// Sealed class defining all possible authentication states
sealed class AuthState {}

class AuthInitial extends AuthState {}

class AuthLoading extends AuthState {}

class AuthAuthenticated extends AuthState {
  final String userId;
  final String displayName;
  AuthAuthenticated({required this.userId, required this.displayName});
}

class AuthError extends AuthState {
  final String message;
  final int? statusCode;
  AuthError({required this.message, this.statusCode});
}

Коли віджет споживає цей стан, вичерпний switch гарантує обробку кожного варіанту. Якщо згодом буде додано п'ятий стан (наприклад, AuthMfaRequired), компілятор негайно вкаже на всі місця у коді, що потребують оновлення.

auth_widget.dartdart
// Exhaustive switch — compiler error if a case is missing
Widget buildAuthUI(AuthState state) => switch (state) {
  AuthInitial()       => const LoginPrompt(),
  AuthLoading()       => const CircularProgressIndicator(),
  AuthAuthenticated(
    displayName: final name
  )                   => Text('Welcome, $name'),
  AuthError(
    message: final msg,
    statusCode: final code,
  )                   => ErrorBanner(
    message: msg,
    code: code,
  ),
};

Поєднання sealed class та вичерпного switch усуває потребу у clauses default або генеричних випадках _. Кожен шлях виконання є явним, що покращує як супровідність, так і неявну документованість коду.

Комбінування Records, Patterns та Sealed Classes

Справжня потужність Dart 3 розкривається при поєднанні всіх трьох функціональностей. Генеричний тип результату на основі sealed class може інкапсулювати як дані успішного виконання (з метаданими у вигляді record), так і помилки.

api_result.dartdart
// Generic sealed result type for API operations
sealed class ApiResult<T> {}

class ApiSuccess<T> extends ApiResult<T> {
  final T data;
  final ({int statusCode, Map<String, String> headers}) metadata;
  ApiSuccess(this.data, {required this.metadata});
}

class ApiFailure<T> extends ApiResult<T> {
  final String error;
  final int? statusCode;
  ApiFailure(this.error, {this.statusCode});
}

На рівні віджетів pattern matching дозволяє обробити кожну комбінацію стану та даних з точністю. Вкладені патерни витягують метадані з record, одночасно фільтруючи за HTTP-кодом, а захисні умови розрізняють порожні та заповнені списки.

product_screen.dartdart
// Consuming the sealed result with pattern matching
Widget buildProductList(ApiResult<List<Product>> result) => switch (result) {
  ApiSuccess(
    data: final products,
    metadata: (statusCode: 200, headers: _),
  ) when products.isNotEmpty => ListView.builder(
    itemCount: products.length,
    itemBuilder: (_, i) => ProductCard(products[i]),
  ),
  ApiSuccess(data: final products) when products.isEmpty =>
    const EmptyState(message: 'No products found'),
  ApiFailure(statusCode: 401) =>
    const AuthExpiredBanner(),
  ApiFailure(error: final msg) =>
    ErrorDisplay(message: msg),
};

Цей патерн виключає ручні перевірки типів, приведення (casts) та нетестовані гілки else. Кожен шлях виконання типізований та перевірений на етапі компіляції.

Pattern matching також застосовується для безпечного витягування даних із JSON-структур, що є типовим випадком при обробці API-відповідей.

null_pattern.dartdart
// If-case for null-safe extraction
void processUser(Map<String, dynamic> json) {
  if (json case {'name': String name, 'age': int age}) {
    // name and age are non-nullable here
    print('$name is $age years old');
  } else {
    print('Invalid user data');
  }
}

Конструкція if-case одночасно перевіряє наявність ключів, їхні типи та витягує значення у локальні non-nullable змінні. Цей патерн замінює ланцюги викликів containsKey та ручних приведень типів.

Готовий до співбесід з Flutter?

Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.

Питання для технічних співбесід із Flutter

Наступні питання регулярно зустрічаються на технічних інтерв'ю з Flutter для оцінки глибинного розуміння Dart 3.

П: Яка різниця між record та класом у Dart 3?

Record є анонімним типом зі структурною рівністю, незмінним за своєю природою. Два records з однаковими значеннями рівні між собою. Клас має об'єктну ідентичність: два екземпляри з ідентичними значеннями не вважаються рівними за замовчуванням (окрім явної імплементації == та hashCode). Records не можуть мати методів, наслідування чи імплементації інтерфейсів. Їх слід використовувати для транспортування даних та множинних повернень, а класи залишати для інкапсульованої бізнес-логіки.

П: Чому sealed classes переважають enums для управління станом?

Enums не здатні нести дані, специфічні для кожного варіанту. AuthState у вигляді enum не дозволив би прив'язати userId виключно до варіанту authenticated. Sealed classes поєднують вичерпність перевірки (аналогічно enums) із можливістю зберігати гетерогенні дані для кожного підтипу. Компілятор гарантує, що кожен підтип оброблений у switch-виразах.

П: Як pattern matching взаємодіє з null safety?

Pattern matching у Dart 3 нативно інтегрується із системою null safety. Конструкція if-case з типовими патернами витягує змінні, гарантовано вільні від null, без використання bang-оператора (!). Патерни для Map одночасно перевіряють наявність ключів та типи їхніх значень, виключаючи небезпечний доступ до динамічних структур.

П: У чому перевага switch-виразів порівняно з switch-інструкціями?

Switch-вирази повертають значення, що робить їх придатними для присвоєнь, повернення з функцій та параметрів конструкторів. Вони вимагають вичерпності: компілятор відмовляється компілювати код, якщо випадок не охоплений. Традиційні switch-інструкції не забезпечують жодної з цих гарантій. У Flutter switch-вирази особливо доречні для патерну builder, де кожен стан повинен повертати віджет.

П: Як records у Dart 3 впливають на продуктивність?

Records алокуються в купі (heap) аналогічно класам, проте їхня структурна рівність усуває потребу в проміжних об'єктах для порівнянь. Компілятор здатен агресивніше оптимізувати позиційні records малого розміру порівняно з еквівалентними класами. На практиці вплив на продуктивність є незначним для переважної більшості сценаріїв використання. Головна перевага полягає у скороченні шаблонного коду та підвищенні читабельності, що зменшує кількість помилок та прискорює розробку.

Стратегія міграції на Dart 3

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

Перший крок полягає у виявленні функцій, що повертають Map, List<dynamic> або саморобні кортежі, для їх заміни на records. Цей рефакторинг є локальним та не потребує каскадних змін.

Другий крок спрямований на ієрархії класів, що використовуються для моделювання кінцевих станів. Абстрактні класи з відомим набором підтипів є ідеальними кандидатами на перетворення в sealed classes. Конвертація спричиняє помилки компіляції всюди, де випадок не оброблений, що виявляє пропущені гілки.

Третій крок замінює ланцюги if-else та switch-інструкції на switch-вирази з pattern matching. Ця трансформація підвищує читабельність та додає перевірку вичерпності компілятором.

Dart 3.10 та dot shorthands

Dart 3.10 запроваджує dot shorthands — скорочений синтаксис, що дозволяє опускати назву типу, коли контекст робить її очевидною. Наприклад, Color color = .red замість Color color = Color.red. Ця функціональність додатково зменшує синтаксичний шум та природно поєднується з pattern matching.

Висновки

  • Records забезпечують легковагі структурні типи даних з порівнянням за значенням, усуваючи потребу у шаблонних класах для множинних повернень
  • Pattern matching надає декларативну деструктуризацію з перевіркою типів, захисними умовами та вичерпністю, гарантованою компілятором
  • Sealed classes визначають закриті набори підтипів, унеможливлюючи пропуск випадку у switch-виразі
  • Комбінація цих трьох функціональностей дозволяє моделювати складні потоки (автентифікація, API-виклики, навігація) з повною типобезпекою
  • Null-safe витягування через if-case замінює ручні перевірки ключів та небезпечні приведення типів при обробці JSON
  • Міграція з Dart 2 відбувається інкрементально: records для множинних повернень, sealed classes для кінцевих станів, switch-вирази для умовних гілок
  • Ці можливості активно перевіряються на технічних співбесідах з Flutter — розуміння архітектурних причин важить не менше за знання синтаксису

Починай практикувати!

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

Теги

#flutter
#dart
#pattern-matching
#sealed-classes
#records
#interview

Поділитися

Пов'язані статті