DartSeniorExperience

Как понять, что команда использует Dart идиоматично, а не переносит привычки из другого языка?

Идиоматичный Dart: sealed classes + pattern matching, коллекционные литералы с if/for, extension methods вместо utils, повсеместный const, async/await вместо .then(), и Records для множественных возвратов.

Признаки идиоматичного Dart в команде

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

1. Null safety используется полноценно, не обходится

Признак переноса привычек: повсеместное использование ! (bang operator) и dynamic.

// НЕ идиоматично — Java/Python-привычки
String name = getData()!;  // force unwrap везде
dynamic config = loadConfig();

// Идиоматично
final String? name = getData();
final displayName = name ?? 'Anonymous';

// Или через pattern matching (Dart 3.0+)
switch (result) {
  case Success(:final data): print(data);
  case Failure(:final error): print(error);
}

2. Коллекционные литералы с if/for/spread

Java/Kotlin-разработчики пишут императивные циклы там, где Dart предлагает декларативный синтаксис.

// НЕ идиоматично
List<String> tags = [];
for (final item in items) {
  if (item.isActive) tags.add(item.tag);
}

// Идиоматично
final tags = [
  for (final item in items)
    if (item.isActive) item.tag,
];

3. Использование sealed classes и pattern matching (Dart 3.0+)

Команды, перенёсшие JS-привычки, используют String-enum или magic strings вместо sealed-иерархий.

// НЕ идиоматично
class ApiResult {
  final String status; // 'success' | 'error'
  final dynamic data;
  ApiResult(this.status, this.data);
}

// Идиоматично
sealed class ApiResult<T> {}
final class ApiSuccess<T> extends ApiResult<T> {
  final T data;
  const ApiSuccess(this.data);
}
final class ApiError<T> extends ApiResult<T> {
  final String message;
  const ApiError(this.message);
}

// Использование с exhaustive switch
String describe(ApiResult result) => switch (result) {
  ApiSuccess(:final data) => 'OK: $data',
  ApiError(:final message) => 'Error: $message',
};

4. Extension-методы вместо utility-классов

Java-разработчики создают статические utility-классы. Dart предлагает extension methods прямо на типах.

// НЕ идиоматично
class StringUtils {
  static String capitalize(String s) => s[0].toUpperCase() + s.substring(1);
}

// Идиоматично
extension StringExtensions on String {
  String get capitalized => this[0].toUpperCase() + substring(1);
}

// Использование
final name = 'dart'.capitalized; // 'Dart'

5. Правильный async — StreamSubscription, StreamController

Dart async-экосистема строится на Stream, а не только на Future. Команды с JS-фоном часто злоупотребляют Future.then() вместо async/await и Stream.

// НЕ идиоматично — callback hell из JS
fetchUser().then((user) {
  fetchPosts(user.id).then((posts) {
    display(posts);
  });
});

// Идиоматично
Future<void> loadAndDisplay() async {
  final user = await fetchUser();
  final posts = await fetchPosts(user.id);
  display(posts);
}

6. Records и destructuring (Dart 3.0+)

Признак идиоматичности в 2026: использование Records для возврата нескольких значений вместо Map или самодельных классов.

// НЕ идиоматично
Map<String, dynamic> getNameAndAge() => {'name': 'Alice', 'age': 30};

// Идиоматично
(String name, int age) getNameAndAge() => ('Alice', 30);

void main() {
  final (name, age) = getNameAndAge();
  print('$name is $age');
}

7. const везде, где возможно

Dart позволяет compile-time constants не только для primitives, но и для объектов. В Flutter это напрямую влияет на производительность.

Подводные камни

  • Переход на sealed classes требует Dart 3.0+ — в старых проектах (Dart 2.x) этого паттерна нет, не стоит ожидать его как признак идиоматичности.
  • Extension methods из разных файлов могут конфликтовать при импорте — нужно явное сокрытие через show/hide.
  • Exhaustive switch на sealed class не проверяется компилятором для обычных switch-statements в старом синтаксисе — только для switch-выражений.
  • cascade operator (..) не идиоматичен для функциональных объектов — только для builder-паттернов с мутацией.
  • Records не поддерживают именованные поля через деструктуризацию так же гибко, как Kotlin data class — есть ограничения.
  • Команды часто путают mixin и abstract class — мixin не может иметь конструктор с параметрами.
  • Идиоматичность =/= наличие кодстайла; важно проверять именно использование языковых фич, а не только форматирование (dartfmt/dart format автоматизирует форматирование).
  • Чрезмерное использование функционального стиля (where/map/reduce цепочки) без readable имён — антипаттерн даже если синтаксически идиоматично.

What hurts your answer

  • Сразу обвинять Dart, не проверив соседние слои системы
  • Чинить симптом без минимального воспроизведения и evidence
  • Не учитывать версии, конфигурацию, окружение и recent changes

What they're listening for

  • Умеет локализовать проблему вокруг Dart
  • Двигается от симптома к гипотезам и проверкам
  • Отличает баг инструмента от ошибки использования или окружения

Related topics