Новое в Dart

В основном, вектор обновления направлен на развитие в сторону взаимодействия с JS и компиляцией в WebAssembly. Представили новый пакет для взаимодействия с Web API — package:web. Является новым способом для интеграции с js для Dart проектов. Миграция на этот пакет привнесет очень вкусный бонус — компиляция в WebAssembly. А секретом нового пакета является новая фича языка — extension type.

Extension type

Обёртка над указанным типом, которая имеет доступ к его методам и полям. Позволяет добавлять свои методы (переопределить методы можно тоже). Является compile-time объектом, который не имеет затраты на выделение типа, как написание обычного враппера.

Выглядит примерно так:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
/// Название extension - новый тип. В скобках указывается тип, который мы хотим расширить.
extension type NewTypeName(int typeThatWeWantToExtend) {

  // Мы можем добавлять поля и методы, которые будут доступны через новый тип.
  bool isPrime() {
    if (typeThatWeWantToExtend < 2) {
      return false;
    }
    for (int i = 2; i < typeThatWeWantToExtend; i++) {
      if (typeThatWeWantToExtend % i == 0) {
        return false;
      }
    }
    return true;
  }
}

Пример инициализации:

1
2
3
4
5
void main() {
  final newInt = NewTypeName(5); // используем наш новый тип, в который передаем тип, который мы расширяем
  print(newInt.isPrime()); // true; вызов метода из нашего нового типа
  print(newInt.typeThatWeWantToExtend); // обращение к типу, над котором делали обёртку
}

Повторять документацию я не вижу смысла, поэтому вот краткий список фич, которые умеют extension type:

  1. Конструкторы
  2. Поддерживаемые поля (функции, геттеры, сеттеры, операторы)
  3. Имплементация типа
  4. Переопределение поля

Но остановлюсь более подробно на 3 и 4 пункте.

Имплементация типа позволяет сделать так, что наш тип будет равен тому же типу, над которым мы пишем обёртку. Это удобно, например, для создания объекта Money:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
extension type Money(int amount) implements int {
  /// Сколько центов в одном экземпляре класса Money.
  ///
  /// Например, в 1 долларе или евро 100 центов.
  static const _base = 100;

  /// Создает экземпляр класса Money из текстового значения.
  /// 
  /// Допустимые форматы:
  /// - '100' - 100 долларов
  /// - '100.50' - 100 долларов и 50 центов
  /// - '100.' - 100 долларов
  /// - '.50' - 0 долларов и 50 центов
  factory Money.fromString(String amount) {
    final moneyValues = amount.split('.').map((e) => e.isEmpty ? '0' : e).toList();
    assert(moneyValues.length <= 2, 'Invalid money format');

    final (int dollars, int cents) = switch (moneyValues) {
      [String dollarsString] => (int.parse(dollarsString), 0),
      [String dollarsString, String centsString] => (
          int.parse(dollarsString),
          int.parse(centsString.padRight(2, '0')),
        ),
      [] => (0, 0),
      _ => throw FormatException('Invalid money format'),
    };

    return Money(dollars * _base + cents);
  }

  /// Возвращает отформатированное значение в формате долларов и центов.
  String get formatted {
    final dollars = amount ~/ 100;
    final cents = amount % 100;

    return '\$$dollars.${cents.toString().padLeft(2, '0')}';
  }
}

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
void main() {
  final moneyFromInt = Money(1000); // создаем Money из 1000 центов (10 долларов)
  final moneyFromString = Money.fromString('10'); // создаем Money из 10 долларов
  print(moneyFromInt.formatted); // $10.00
  print(moneyFromString.formatted); // $10.00

  print(moneyFromInt == moneyFromString); // true
  print(moneyFromInt == 1000); // true
  print(moneyFromInt + moneyFromString); // 2000 центов
  print(moneyFromInt - moneyFromString); // 0 центов
  print(moneyFromInt ~/ 3); // 333 цента
  print(Money(moneyFromInt ~/ 3).formatted); // $3.33
}

А теперь перейдем к грязи — @redeclare аннотация (package:meta). В extension type нельзя переопределить (@override) какое-то поле, поддерживая связь с родителем через super.method. Нам дали новую аннотацию, с помощью которой поведение функции меняется на новую:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
void main() {
  final someString = 'Hello World!';
  final myString = MyString(someString);

  print(someString[0]); // H
  print(myString[0]); // 72
  print(isSame(someString, myString)); // true
}

// метод, который принимает два String
bool isSame(String value1, String value2) {
  print(value1[0]); // H
  print(value2[0]); // H
  return value1 == value2;
}

extension type MyString(String _) implements String {
  // Заменяет 'String.operator[]'
  @redeclare
  int operator [](int index) => codeUnitAt(index);
}

То есть, мы можем переопределить для себя какие-то поля, и они будут отрабатывать согласно нашему новому типу. Но если какой-то метод будет запрашивать тип, над которым мы делали обёртку — то использовать он будет напрямую тип, а не обёртку. Это защищает от кейсов, когда автор библиотеки сделал final class , и ожидает одно поведение от метода класса, а мы написали обёртку, в которой метод будет возвращать совершенно другое значение. Dart умный, и всё взаимодействие у автора библиотеки будет происходить только с его классом.

Но если вы у себя в проекте сделаете что-то подобное, то главное не утоните в слоях пост-иронии и самообмана.

1
2
3
extension type int(String _) implements String {
  // ...
}

В заключение этой фичи, хочу сказать, что она предназначена для достаточно узких кейсов, либо когда функционал нового типа во многом повторяет какой-либо существующий. Но в то же время, это очень полезный инструмент для работы с JS. Возможно, в будущем комьюнити найдет более интересные способы применения этой конструкции.

Полезные ссылки:

  1. js_interop
  2. Gemini in Dart & Flutter apps

Новое во Flutter

Улучшения

Android

  • В DevTools добавлена секция “Deep Link”, которая поможет проверить правильность настройки deeplinks. От себя добавлю, что в Android Studio тоже можно проверять запуск ссылок с помощью инструмента “App Links Assistant”.
  • Контекстное меню, которое появляется после выбора текста, теперь поддерживает другие установленные приложения в системе. Например, при выделении номера, будет предложено открыть его в приложении “Телефон”. Также добавлена кнопка “Поделиться”, которая в Android-native отображается по умолчанию.
  • Добавлена поддержка Android OpenGL в Impeller (до этого был только Vulkan). Это значит, что почти все устройства на Android, теперь поддерживаются Impeller. Чтобы потыкать новый движок, используйте эту статью.

iOS

  • Системный шрифт теперь следует Apple Human Design: чем меньше размер шрифта, тем больше глифы удалены друг от друга; и наоборот — чем больше шрифт, тем ближе глифы находятся друг к другу.
  • Flutter теперь содержит Privacy Manifest, согласно новым требованиям Apple.
  • Прекращена поддержка iOS 11

Windows

  • Прекращена поддержка Windows 7, Windows 8
  • Добавлена поддержка Windows arm64

Важное

  • Флаг Paint.enableDithering удалён. Он был включен глобально уже много версий назад, а также проблема, которую он решал, была устранена.

Roadmap

Разработчики поделились с нами планами на 2024 год, и там очень много вкусного:

  1. Skia на iOS будет полностью убрана. На Android Impeller станет основным, но Skia можно будет включить по необходимости.
  2. Фреймворк перейдет полностью на Material 3, Material 2 со временем будет удалён. Я настоятельно рекомендую переводить ваши проекты на Material 3 и правильно настраивать тему. Используя Material 2, вы ограничиваете себя от новых виджетов, например IconButton.filled.
  3. Команда также ожидает вернуться к работе над Blackcanvas — проект, который позволит разработчикам создавать свои отдельные UI наборы с нуля. Проблема в том, что все существующие UI пакеты наследуются от Material, и редко от Cupertino. Разработчики хотят внести улучшения, чтобы подобные пакеты не зависели от уже существующих UI наборов.
  4. В Web продолжат пилить оптимизацию, хотят сделать CanvasKit рендером по умолчанию. Но самое интересное — поддержка platform view и компиляция в WebAssembly. Также, возобновляется работа над Hot Reload в вебе (сейчас приложение перезапускается при вызове Hot Reload).
  5. Поддержка platform views на macOS и Windows, которая позволит встраивать, например, WebView. На Linux приоритет добавить GTK4. И на всех платформах ведется работа по мультиоконному режиму в рамках одного Dart изолята и одного дерева виджетов. Первые превью этой фичи уже были на канале Flutter!
  6. В этом году в Dart ожидают решить, насколько поддержка метапрограммирования целесообразна, и либо выпустить первые этапы поддержки этой фичи, либо же отказаться вовсе. Метапрограммирование (или же макросы) — это что-то типа генератора кода внутри самого кода. В основном, комьюнити ждет эту фичу, чтобы писать сериализацию/десериализацию дата классов. Для такого случая рассматривают возможность добавления ValueClasses.
  7. Обсуждают интересные фичи для сокращения написания кода, например: структы или сокращенные импорты
  8. В этом году будет 4 стабильных релиза и 12 беток.