Что такое Extension-методы в Dart и как их применять во Flutter?

Ответ

Extension-методы (расширения) в Dart — это мощный механизм, позволяющий добавлять новую функциональность к существующим классам, даже к тем, исходный код которых вы не можете изменить (например, классы из стандартной библиотеки или внешних пакетов). Это делается без создания подкласса и без использования утилитарных классов.

Базовый синтаксис:

extension MyStringExtensions on String {
  // Добавляем метод capitalize() ко всем строкам
  String capitalize() {
    if (isEmpty) return this;
    return '${this[0].toUpperCase()}${substring(1).toLowerCase()}';
  }

  // Добавляем геттер
  bool get isPalindrome => reversed == this;
}

void main() {
  print('dart'.capitalize()); // Вывод: Dart
  print('racecar'.isPalindrome); // Вывод: true
}

Практическое применение во Flutter-разработке:

  1. Упрощение работы с контекстом:

    extension ContextExtensions on BuildContext {
      // Быстрое получение размеров экрана
      Size get screenSize => MediaQuery.of(this).size;
      double get screenWidth => screenSize.width;
      double get screenHeight => screenSize.height;
    
      // Упрощённый вызов навигации
      void popToRoot() => Navigator.of(this).popUntil((route) => route.isFirst);
    }
    
    // Использование в виджете
    @override
    Widget build(BuildContext context) {
      if (context.screenWidth > 600) {
        return _buildTabletLayout();
      }
      return _buildMobileLayout();
    }
  2. Расширение встроенных типов для предметной области:

    extension DateTimeExtensions on DateTime {
      // Форматирование даты для отображения в UI
      String toDisplayFormat() {
        return DateFormat('dd.MM.yyyy HH:mm').format(this);
      }
    
      // Проверка, является ли дата сегодняшним днём
      bool get isToday {
        final now = DateTime.now();
        return year == now.year && month == now.month && day == now.day;
      }
    }
    
    // Использование
    Text(DateTime.now().toDisplayFormat());
  3. Добавление вспомогательных методов к коллекциям:

    extension ListExtensions<T> on List<T> {
      // Безопасное получение элемента или null
      T? elementAtOrNull(int index) => (index >= 0 && index < length) ? this[index] : null;
    
      // Разделение списка на подсписки фиксированного размера (чанки)
      List<List<T>> chunk(int size) {
        return [
          for (var i = 0; i < length; i += size)
            sublist(i, i + size > length ? length : i + size),
        ];
      }
    }

Важные ограничения и особенности:

  • Расширения не могут добавлять поля (переменные экземпляра) в класс, только методы и геттеры/сеттеры.
  • Они работают на этапе компиляции и не влияют на производительность во время выполнения.
  • Имя расширения (например, MyStringExtensions) используется для разрешения конфликтов, если к одному классу применяются несколько расширений с одинаковыми именами методов.
  • Расширения должны быть явно импортированы в файл, где вы хотите их использовать (import 'extensions/string_extensions.dart';).

Ответ 18+ 🔞

Слушай, вот тебе про Dart-расширения, но так, чтобы понятно было, а не как в этих ихних документациях, где от скуки сдохнуть можно. Представь, что ты купил машину, а потом тебе говорят: «А давай мы к ней прикрутим ништяки, которых с завода нет?» Вот extension-методы — это и есть такие ништяки для классов. Хочешь к стандартной строке или к какому-нибудь виджету BuildContext прилепить свою функцию? Без проблем, ёпта! Не надо городить подклассы или отдельные утилитки — просто навешиваешь, как ксенон на дедову десятку.

Как это выглядит, блядь:

extension MyStringExtensions on String {
  // Делаем так, чтобы любая строка могла сама себя с большой буквы написать
  String capitalize() {
    if (isEmpty) return this; // На пустую не зачем тратиться
    return '${this[0].toUpperCase()}${substring(1).toLowerCase()}';
  }

  // Или вот геттер, который проверяет, читается ли строка одинаково с обеих сторон
  bool get isPalindrome => reversed == this;
}

void main() {
  print('dart'.capitalize()); // Выведет: Dart — красота же!
  print('racecar'.isPalindrome); // Выведет: true — а это вообще палиндром, ядрёна вошь!
}

А теперь, где это реально вкатывает во Flutter, чтобы жизнь мёдом не казалась:

  1. Контекст — твой лучший друг и главная боль. Сколько раз ты писал MediaQuery.of(context).size.width? Рука отваливается, честно. Давай сделаем красиво:

    extension ContextExtensions on BuildContext {
      // Быстро хватаем размеры экрана
      Size get screenSize => MediaQuery.of(this).size;
      double get screenWidth => screenSize.width;
      double get screenHeight => screenSize.height;
    
      // Или вот — нахуй все экраны к чёртовой матери, вернуться в самое начало
      void popToRoot() => Navigator.of(this).popUntil((route) => route.isFirst);
    }
    
    // Используем в виджете без этой всей ебалы с вложенностью
    @override
    Widget build(BuildContext context) {
      // Смотри как лаконично, аж терпения ебать ноль, чтобы по-старому писать
      if (context.screenWidth > 600) {
        return _buildTabletLayout();
      }
      return _buildMobileLayout();
    }
  2. Даты и время. Ну что за мудёж каждый раз с DateFormat? Сделаем один раз и забудем, как страшный сон.

    extension DateTimeExtensions on DateTime {
      // Форматируем для показа человеку, а не машине
      String toDisplayFormat() {
        return DateFormat('dd.MM.yyyy HH:mm').format(this);
      }
    
      // Проверка, сегодня ли это число? Чтобы не сравнивать вручную год, месяц и день
      bool get isToday {
        final now = DateTime.now();
        return year == now.year && month == now.month && day == now.day;
      }
    }
    
    // Использование — одна строка, а не три
    Text(DateTime.now().toDisplayFormat()); // "27.05.2024 15:30" — сразу видно
  3. Списки (List). Стандартный Dart иногда скудноват. Хочешь безопасно получить элемент или разбить список на куски?

    extension ListExtensions<T> on List<T> {
      // Берём элемент, а если индекс кривой — возвращаем null, а не Exception в рожу
      T? elementAtOrNull(int index) => (index >= 0 && index < length) ? this[index] : null;
    
      // Делим большой список на мелкие порции (чанки). Овердохуища полезно для пагинации или сеток.
      List<List<T>> chunk(int size) {
        return [
          for (var i = 0; i < length; i += size)
            sublist(i, i + size > length ? length : i + size),
        ];
      }
    }

Но, чувак, есть важные подводные булыжники, не обдери ноги:

  • Поля добавить нельзя. Только методы, геттеры и сеттеры. Нельзя воткнуть в String новую переменную mySuperField. Это как пытаться приварить пятую дверь к седану — технически возможно, но всем будет понятно, что ты ебанько.
  • Всё решается при компиляции. На скорости выполнения это не сказывается, можно не бздеть.
  • Имя расширения — это твоё клеймо. Если у двух расширений для String будет метод superMethod(), то придётся указывать, какое именно ты используешь, или импортировать с префиксом. Конфликты, блядь, как в коммуналке.
  • Надо явно импортировать. Не сработает по волшебству. Подключил файл с расширениями — пользуйся. Не подключил — сиди и пиши всё руками, распиздяй.

Короче, инструмент — огонь. Позволяет писать код чище, быстрее и без лишнего бойлерплейта. Главное — не увлекаться и не начать расширять int методом toPizza(), а то потом сам от себя охуеешь при ревью.