Enhance Your Macros: Pass Type to Macro Classes

Tsuyoshi ChujoTsuyoshi Chujo
4 min read

In the previous article, I introduced a step-by-step guide to making macro classes.

https://chooyan.hashnode.dev/make-your-macros-step-by-step-guide-for-flutter-macros

Yet, the macro lacks some important functionality for MaterialPageRoute. That is <T>.

MaterialPageRoute, or its superclass Route, gets <T> as their type parameter to specify the return type of .push() method like below.

final int? result = await Navigator.of(context).push(
  MaterialPageRoute<int>((context) => const NextPage()),
);

Doing so, NextPage can return int value using the argument of .pop() method like below.

Navigator.of(context).pop(42); // return 42 to the previous page

As our RouteMacro, made in the previous article, doesn't have the functionality to specify <T> now, let's add the feature in this article investigating how to pass types to macro classes.

Options to pass types to macro classes

Considering how to pass type to macro classes, 2 options come to our mind at first.

/// pass [int] as type parameter
@RouteMacro<int>()
class NextPage extends StatelessWidget {}
/// pass [int] as an argument with type [Type]
@RouteMacro(int)
class NextPage extends StatelessWidget {}

But unfortunately, neither of them doesn't work, at least with the current version of Channel master, 3.24.0-1.0.pre.69. I'll discuss the reasons one by one.

Strategy 1: Type parameter

According to the specification document's limitations/requirements section, "Macro classes cannot contain generic type parameters".

  • Macro classes cannot contain generic type parameters.

    • It is possible that in the future we could allow some restricted form of generic type parameters for macro classes, but it gets tricky because the types in the user code instantiating the macro are not necessarily present in the macros own transitive imports.

https://github.com/dart-lang/language/blob/main/working/macros/feature-specification.md#macro-limitationsrequirements

Besides this issue, knowing "where the type comes from" for macro classes seems to be a difficult concern. That type potentially has a complex hierarchy, is made by another macro class, or has a complicated definition using records or generics.

Anyways, we can't take this option at least right now, so let's discuss the next one.

Strategy 2: Argument

The previous specification document says we can pass Type as an argument of the macro's constructor, and the compiler implicitly converts it to TypeAnnotation object to be available with macro-related APIs.

  • If the parameter type is TypeAnnotation then a literal type must be passed, and it will be converted to a corresponding TypeAnnotation instance.

See the Macro Arguments section linked below for more details.

https://github.com/dart-lang/language/blob/main/working/macros/feature-specification.md#macro-arguments

TypeAnnotation preserves not only the name of the type but also Code, Identifier, and more stuff for code generation and code introspection operations, meaning we can generate MaterialPageRoute<int> by receiving this as an argument.

Unfortunately, however, this feature has not been implemented yet in the current version according to the comment on GitHub issue.

The intention is to pass these as regular arguments, and have them coerced into a TypeAnnotation implicitly, but that isn't implemented yet.

Thus, we have to wait until this feature is introduced and need another option.

Strategy 3: Implements interface

Here comes the third option; making NextPage implement some interface class with a generic type <T>.

/// Implement [RouteInterface<T>] with the type <int>
/// @RouteMacro()
class NextPage extends StatelessWidget implements RouteInterface<int> {}

This is tricky but we can workaround with this trick even with the current version.

As the definition of the augmented class's interfaces can be retrieved by ClassDeclaration object, which is given as an argument of buildDeclarationsForClass() method, with clazz.interfaces, we can refer to the interface RouteInterface and its type argument <int> like below.

macro class RouteMacro implements ClassDeclarationsMacro {
  const RouteMacro();

  @override
  Future<void> buildDeclarationsForClass(
    ClassDeclaration clazz,
    MemberDeclarationBuilder builder,
  ) async {
    /// retrieve interfaces
    final Iterable<NamedTypeAnnotation> interfaces = clazz.interfaces;

    /// retrieve type argument
    final TypeArgument typeArgument = interfaces.first.typeArguments.first;
    );
  }
}

Note that we omit checking the case that users don't implement RouteInterface for the simplicity of the code above. We may need to check the relevant interface like below.

final typeArgument = interfaces.firstWhereOrNull((interface) {
  return interface.identifier.name == 'RouteInterface' 
    && interface.typeArguments.isNotEmpty;
});

if (typeArgument == null) {
  // generate code with MaterialPageRoute
} else {
  // generate code with MaterialPageRoute<T>
}

Generate code with return type <T>

Once we deal with retrieving typeArgument whatever strategies, what we do next is just apply it to our generated code. Our builder.declareInType() method introduced in the previous article is updated below.

    builder.declareInType(DeclarationCode.fromParts([
      '''
static Route<''',
      typeArgument.code,
      '''> route() {
  return MaterialPageRoute<''',
      typeArgument.code,
      '''>(
    builder: (context) => const ''',
      widget,
      '''(),
  );
}
''',

Now, our macro will generate the augmentation code below.

augment class NextPage {
  static Route<prefix0.int> route() {
    return MaterialPageRoute<prefix0.int>(
      builder: (context) => const prefix1.NextPage(),
    );
  }
}

We can see int, with prefix prefix0, is now successfully in our generated code!

And the return type of push() is now also Future<int?>.

Conclusion

I believe a lot of macros need type for their generating code, so I also believe understanding how users can pass type to our macros will be crucial.

Though some features are still in development, it's valuable to know other workaround options and also the limitations/restrictions of macros.

Make sure to read the specification document and issues discussing "why" if you need detailed and precise information about macros.

https://github.com/dart-lang/language/blob/main/working/macros/feature-specification.md

https://github.com/dart-lang/language/issues?q=is%3Aissue+macros

10
Subscribe to my newsletter

Read articles from Tsuyoshi Chujo directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Tsuyoshi Chujo
Tsuyoshi Chujo

I am a freelance mobile app developer, mainly working on Flutter. I'm also developing a Flutter package named "crop_your_image", which enables Flutter app developers to build cropping images functionality in their app with their own designed UI. https://pub.dev/packages/crop_your_image