Речь о макросах программирования может показаться знакомой многим, но для разработчиков 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()`.

На следующем рисунке показана реализация макроса `@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
:
./dart --version
для проверки версии.```- Перейдите в папку 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 )