1 В избранное 0 Ответвления 0

OSCHINA-MIRROR/CarGuo-GSYFlutterBook

Присоединиться к Gitlife
Откройте для себя и примите участие в публичных проектах с открытым исходным кодом с участием более 10 миллионов разработчиков. Приватные репозитории также полностью бесплатны :)
Присоединиться бесплатно
Клонировать/Скачать
Flutter-macros.md 21 КБ
Копировать Редактировать Web IDE Исходные данные Просмотреть построчно История
Отправлено 10.03.2025 00:06 5767d61

Важные обновления Flutter 2024 года: поддержка макросов в Dart, спасение JSON сериализации

Речь о макросах программирования может показаться знакомой многим, но для разработчиков Flutter и Dart это всегда было недостатком. Этот недостаток проявляется в отсутствии поддержки изменения кода во время разработки, особенно при работе с JSON сериализацией в Dart.

Например, в настоящее время JSON сериализация в Dart сильно зависит от использования build_runner, чтобы генерировать Dart код. Например, в реальной жизни нам требуется:

  • Использование библиотеки json_serializable
  • Объявление объекта Event через аннотацию
  • Выполнение команды flutter packages pub run build_runner build для создания файла
  • Получение файла Event.g.dart, который используется для реализации JSON сериализации и десериализации

Основной проблемой является необходимость генерации проектного файла через командную строку, а также возможность его редактирования вручную. Это не очень удобно и элегантно с точки зрения разработки.

Макросы объявлены как пользовательские Dart классы, которые могут реализовать один или несколько новых встроенных макросов. Макросы в Dart создаются с помощью обычного императивного Dart кода, нет отдельного "языка макросов".> Большинство макросов не просто создают новый код с нуля, они добавляют код на основе существующих свойств программы. Например, макрос для добавления JSON сериализации к классу может анализировать объявленные поля класса и создавать метод toJson(), который сериализует эти поля в JSON объект.

Давайте рассмотрим пример из официального демонстрационного материала:

  • Класс MyState имеет пользовательскую аннотацию @AutoDispose(). Это макрос, реализованный разработчиками, который наследуется от класса State и содержит метод dispose.
  • Внутри MyState есть несколько объектов a, a2, b и c, где a, a2, b реализуют интерфейс Disposable и имеют метод dispose.
  • Несмотря на то что методы dispose() объектов a, a2, b и MyState происходят из разных базовых классов, благодаря реализации @AutoDispose(), вызов state.dispose() приводит к тому, что методы dispose всех этих объектов будут выполнены одновременно.
import 'package:macro_proposal/auto_dispose.dart';

void main() {
  var state = MyState(a: ADisposable(), b: BDisposable(), c: 'Привет, мир');
  state.dispose();
}

@AutoDispose()
class MyState extends State {
  final ADisposable a;
  final ADisposable? a2;
  final BDisposable b;
  final String c;

  MyState({required this.a, this.a2, required this.b, required this.c});

  @override
  String toString() => 'MyState!';
}

class State {
  void dispose() {
    print('disposing of $this');
  }
}

class ADisposable implements Disposable {
  void dispose() {
    print('disposing of ADisposable');
  }
}

class BDisposable implements Disposable {
  void dispose() {
    print('disposing of BDisposable');
  }
}
```На следующем рисунке можно видеть, что несмотря на то, что `MyState` не вызывает метод `dispose` для переменных `a`, `a2` и `b` явно, а также хотя эти переменные и метод `dispose` в `MyState` происходят из разных базовых классов, все методы `dispose` успешно выполняются. Это достигается благодаря макросу `@AutoDispose()`.

![](http://img.cdn.guoshuyu.cn/2bk20240202_macros/image3.png)

На следующем рисунке показана реализация макроса `@AutoDispose()`. **Ключевым словом здесь является `macro`, которое служит маркером для макроса.** Оставшаяся часть кода представляет собой реализацию скрипта Dart, где внутри `macro` реализованы методы `ClassDeclarationsMacro` и `buildDeclarationsForClass`. В этом коде можно легко заметить реализацию относительно `super.dispose();` и `disposeCalls`.

```dart
import 'package:_fe_analyzer_shared/src/macros/api.dart';

// Интерфейс для объектов, которые могут освобождать ресурсы.
abstract class Disposable {
  void dispose();
}

macro class AutoDispose implements ClassDeclarationsMacro, ClassDefinitionMacro {
  const AutoDispose();

  @override
  void buildDeclarationsForClass(
      ClassDeclaration clazz, MemberDeclarationBuilder builder) async {
    var methods = await builder.methodsOf(clazz);
    if (methods.any((d) => d.identifier.name == 'dispose')) {
      // Не требуется добавлять метод dispose, он уже существует.
      return;
    }
}
``````dart
builder.declareInType(DeclarationCode.fromParts([
  // TODO: Remove the external keyword once CFE supports it.
  'external void dispose();',
]));
@override
Future<void> buildDefinitionForClass(ClassDeclaration clazz, TypeDefinitionBuilder builder) async {
  var disposableIdentifier = 
    // ignore: deprecated_member_use
    await builder.resolveIdentifier(Uri.parse('package:macro_proposal/auto_dispose.dart'), 'Disposable');
  var disposableType = await builder.resolve(NamedTypeAnnotationCode(name: disposableIdentifier));
}
var disposeCalls = [];
var поля = await builder.поля(clazz);
for (var поле in поля) {
  var тип = await builder.определение(поле.тип.код);
  if (!(await тип.являетсяПодтипом(disposableType))) continue;
  disposeCalls.add(RawCode.изЧастей([
    '\n',
    поле.идентификатор,
    если (поле.тип.можетБытьNull) '?',
    '.dispose();',
  ]));
}
``````markdown
    // Улучшаем метод dispose, внедряя все новые вызовы dispose после либо вызова `augmented()`, либо `super.dispose()`, в зависимости от того, существует ли уже тело метода.
    //
    // Если есть существующее тело, то это тело должно вызывать `super.dispose()`.
    var disposeMethod = (await builder.методы(clazz)).первыйГде((method) => method.идентификатор.название == 'dispose');
    var disposeBuilder = await builder.построитьМетод(disposeMethod.идентификатор);
    disposeBuilder.украшение(FunctionBodyCode.изЧастей([
      '{\n',
      'if (disposeMethod.внешнийИлиОтсутствуетТело)',
      '  super.dispose();',
      'else',
      '  augmented();',
      ...disposeCalls,
      '}',
    ]));
  }
}

До этого момента вы должны были почувствовать мощь макросов, приведённый выше пример взят из dart-language / macros/example/auto_dispose_main , где код в папке bin/ является примером запуска скрипта, а код в папке lib/ демонстрирует реализацию макросов:

https://github.com/dart-lang/language/tree/main/working/macros/example

Конечно, поскольку этот этап экспериментальный, API и стабильность ещё требуют доработки, поэтому чтобы запустить эти примеры, вам потребуется выполнить некоторые дополнительные шаги, например, сильная связь версий, как в случае с примером auto_dispose_main:

```- Перейдите в папку example и выполните команду dart pub get, процесс может занять некоторое время.

Изображение

  • Наконец, выполните команду dart --enable-experiment=macros bin/auto_dispose_main.dart. Обратите внимание, что этот dart — это версия dart, которую вы указали.

Кроме того, есть еще один пример от третьей стороны, предоставленный millsteed проектом macros. Это простой демонстрационный пример реализации сериализации JSON, который можно использовать без необходимости дополнительно скачивать dark-sdk, используя встроенный в flutter dart-sdk с версией: 3.19.0-12.0.pre.

В локальной директории Flutter переключитесь на версию git checkout 3.19.0-12.0.pre, а затем выполните команду flutter doctor, чтобы инициализировать dark sdk.

Реализация кода довольно проста. Сначала рассмотрите пример в папке bin, где через @Model() объявлены как GetUsersResponse, так и User как объекты JSON. При запуске программы макросы автоматически добавят методы fromJson и toJson.

import 'dart:convert';

import 'package:macros/model.dart';

@Model()
class User {
  User({required this.username, required this.password});

  final String username;
  final String password;
}

@Model()
class GetUsersResponse {
  GetUsersResponse(
      {required this.users, required this.pageNumber, required this.pageSize});
``````dart
include 'dart:convert';
include 'package:_fe_analyzer_shared/stream/macros/api.dart';

macro class Model implements ClassDeclarationsMacro {
  const Model();

  static const _baseTypes = ['bool', 'double', 'int', 'num', 'String'];
  static const _collectionTypes = ['List'];

  @override
  Future<void> buildDeclarationsForClass(
    ClassDeclaration classDeclaration,
    MemberDeclarationBuilder builder,
  ) async {
    final className = classDeclaration.identifier.name;

    final fields = await builder.fieldsOf(classDeclaration);

    final fieldNames = <String>[];
    final fieldTypes = <String, String>{};
    final fieldGenerics = <String, List<String>>{};

    for (final field in fields) {
      final fieldName = field.identifier.name;
      fieldNames.add(fieldName);

      final fieldType = (field.type.code as NamedTypeAnnotationCode).name.name;
      fieldTypes[fieldName] = fieldType;

      if (_collectionTypes.contains(fieldType)) {
        final generics = (field.type.code as NamedTypeAnnotationCode)
            .typeArguments
            .map((e) => (e as NamedTypeAnnotationCode).name.name)
            .toList();
        fieldGenerics[fieldName] = generics;
      }
    }
}
```    final fieldTypesWithGenerics = fieldTypes.map(
      (name, type) {
        final generics = fieldGenerics[name];
        return MapEntry(
          name,
          generics == null ? type : '${type}<${generics.join(", ")}> ',
        );
      },
    );```markdown
void _buildFromJson(
    MemberDeclarationBuilder builder,
    String className,
    List<String> fieldNames,
    Map<String, String> fieldTypes,
    Map<String, List<String>> fieldGenerics,
) {
    final code = [
        'factory $className.fromJson(Map<String, dynamic> json) {'.indent(2),
        'return $className('.indent(4),
        for (final fieldName in fieldNames) ...[
            if (_baseTypes.contains(fieldTypes[fieldName])) ...[
                "$fieldName: json['$fieldName'] as ${fieldTypes[fieldName]},"
                    .indent(6),
            ] else if (_collectionTypes.contains(fieldTypes[fieldName])) ...[
                "$fieldName: (json['$fieldName'] as List<dynamic>)".indent(6),
                '.whereType<Map<String, dynamic>>()'.indent(10),
                '.map(${fieldGenerics[fieldName]?.first}.fromJson)'.indent(10),
                '.toList(),'.indent(10),
            ] else ...[
                '$fieldName: ${fieldTypes[fieldName]}'
                        ".fromJson(json['$fieldName'] "
                        'as Map<String, dynamic>),'
                    .indent(6),
            ],
        ],
        ');'.indent(4),
        '}'.indent(2),
    ].join('\n');
    builder.declareInType(DeclarationCode.fromString(code));
}
void _buildToJson(
    MemberDeclarationBuilder builder,
    List<String> fieldNames,
    Map<String, String> fieldTypes,
) {
    final code = [
        'Map<String, dynamic> toJson() {'.indent(2),
        'return {'.indent(4),
``````markdown
        for (final fieldName in fieldNames) . . . [
             if (_baseTypes.contains(fieldTypes[fieldName])) . . . [
                 "'\$fieldName': \$fieldName,".indent(6),
             ] else if (_collectionTypes.contains(fieldTypes[fieldName])) . . . [
                 "'\$fieldName': \$fieldName.map((e) => e.toJson()).toList(),".indent(6),
             ] else . . . [
                 "'\$fieldName': \$fieldName.toJson(),".indent(6),
             ],
         ],
         '};'.indent(4),
         '}'.indent(2),
     ].join('\n');
     builder.declareInType(DeclarationCode.fromString(code));
}
``````markdown
void _buildCopyWith(
    MemberDeclarationBuilder builder,
    String className,
    List<String> fieldNames,
    Map<String, String> fieldTypes,
) {
    final code = [
        '$className copyWith({'.indent(2),
        for (final fieldName in fieldNames) ...[
            '${fieldTypes[fieldName]}? $fieldName,'.indent(4),
        ],
        '}) {'.indent(2),
        'return $className('.indent(4),
        for (final fieldName in fieldNames) ...[
            '$fieldName: $fieldName ?? this.$fieldName,'.indent(6),
        ],
        ');'.indent(4),
        '}'.indent(2),
    ].join('\n');
    builder.declareInType(DeclarationCode.fromString(code));
}

void _buildToString(
    MemberDeclarationBuilder builder,
    String className,
    List<String> fieldNames,
) {
    final code = [
        '@override'.indent(2),
        'String toString() {'.indent(2),
        "return '$className('".indent(4),
        for (final fieldName in fieldNames) ...[
            if (fieldName != fieldNames.last) ...[
                "'$fieldName: \$$fieldName, '".indent(8),
            ] else ...[
                "'$fieldName: \$$fieldName'".indent(8),
            ],
        ],
        "')';".indent(8),
        '}'.indent(2),
    ].join('\n');
    builder.declareInType(DeclarationCode.fromString(code));
}
void _buildEquals(
    MemberDeclarationBuilder builder,
    String className,
    List<String> fieldNames,
) {
    final code = [
        '@override'.indent(2),
        'bool operator ==(Object other) {'.indent(2),
        'return other is $className &&'.indent(4),
        'runtimeType == other.runtimeType &&'.indent(8),
        for (final fieldName in fieldNames) ...[
            if (fieldName != fieldNames.last) ...[
                '$fieldName == other.$fieldName &&'.indent(8),
            ] else ...[
                '$fieldName == other.$fieldName;'.indent(8),
            ],
        ],
        '}'.indent(2),
    ].join('\n');
    builder.declareInType(DeclarationCode.fromString(code));
}

Построение хэш-кода

void _buildHashCode(
    MemberDeclarationBuilder builder,
    String className,
    List<String> fieldNames,
) {
    final code = [
        '@override'.indent(2),
        'int get hashCode => Object.hash('.indent(2),
        for (final fieldName in fieldNames) ...[
            if (fieldName != fieldNames.first) ...[
                ', '.indent(4),
            ],
            'this.$fieldName'.indent(4),
        ],
        ')'.indent(2),
        ';'.indent(2),
    ].join('\n');
    builder.declareInType(DeclarationCode.fromString(code));
}
``````dart
void _buildHashCode(
    MemberDeclarationBuilder builder,
    List<String> fieldNames,
) {
    final code = [
        '@переприсваивание'.indent(2),
        'int get hashCode {'.indent(2),
        'return Object.hash('.indent(4),
        'runtimeType,'.indent(6),
        for (final fieldName in fieldNames) ...[
            '$fieldName,'.indent(6),
        ],
        ');'.indent(4),
        '}'.indent(2),
    ].join('\n');
    builder.declareInType(DeclarationCode.fromString(code));
}
}
``````markdown
расширение на String {
    String indent(int length) {
        final space = StringBuffer();
        for (var i = 0; i < length; i++) {
            space.write(' ');
        }
        return '$space$this';
    }
}

Макросы на данном этапе находятся в экспериментальной стадии, поэтому API все еще находится в процессе доработки. Это также объясняет необходимость указания версии Dart в приведенном выше примере. В планах для макросов есть некоторые требования:

  • Все конструкторы макросов должны быть помечены как const.
  • Каждый макрос должен реализовать хотя бы один из интерфейсов Macro.
  • Макросы не могут быть абстрактными объектами.
  • Класс макроса не может быть создан другими макросами.
  • Класс макроса не может содержать параметры типа шаблонов.
  • Каждый интерфейс макроса должен декларировать методы, которые должны быть реализованы, такие как ClassDeclarationsMacro и его метод buildDeclarationsForClass.

В будущих планах API макросов возможно будет представлено как пакет Pub с помощью библиотеки dart:_macros, но окончательное решение будет зависеть от решения команды Dart при официальном выпуске.

Общая картина такова, что это значительное событие для Dart и Flutter. Хотя программирование с использованием макросов не является новым понятием, теперь Dart может элегантно реализовать сериализацию JSON, используя сам язык Dart. Для разработчиков Flutter это лучший подарок на Новый год.


Опубликовать ( 0 )

Вы можете оставить комментарий после Вход в систему

1
https://api.gitlife.ru/oschina-mirror/CarGuo-GSYFlutterBook.git
git@api.gitlife.ru:oschina-mirror/CarGuo-GSYFlutterBook.git
oschina-mirror
CarGuo-GSYFlutterBook
CarGuo-GSYFlutterBook
master