[pigeon] Adds StructuredGenerator class and subclasses (#3037)

* Rename generator class to Adapter

* create new generator class and dart subclass

* cpp and dart test gen

* added files

* Adds Generator class to all generators

* adds swift

* Updates tests to use new Adapter naming scheme

* Dart generate methods

* convert all generate functions to use new method

* chagngelog

* remove Generator class fields

* move paths to options

* remove dartTestOptions

* Moves write header to generator class method

* Updates tests to use new generator class

* source -> header

* correct options

* header -> source

* header -> prefix, source -> header

* remove headers from generateTestDart

* changelog

* Nits and combines source and header generators

* renames Adapter to GeneratorAdapter

* Update version number for breaking changes

* nits

* more personal nits

* update tests to match new merged generators

* cleaner header methods

* Fixes dart header bug

* add gen files for clarity

* better field naming

* better field naming

* removed unneeded dart test generator

* Add filetype to generator

* Adds filetype as field to generatorAdapters

* merge

* analyze

* add import method

* re-remove DartTestGenerator

* Moves imports to new method

* adds writeEnum method to generator class

* nits

* assert

* objc enum

* fix code order issues

* add writeDataClass method

* remove writeMainClass from java

* java + kotlin

* remove dead code

* swift

* fix dart test error

* cpp + objc

* objc + cpp

* Move all migrated methods into class

* Creates writeHeader method on Generator classes

* private unique methods and reorder

* changelog

* changelog

* changelog

* changelog

* prologue

* gen

* dart

* java

* kotlin

* swift

* cpp

* objc

* remove unneeded java method

* analyze

* single file gens

* objc

* Cpp

* analyze

* vocab

* vocab

* typo

* less comma

* wrap write methods

* move code from writeGeneralUtilities

* update changelog

* adds open and close namespace and fixes nits

* makes writeEnum optional

* remove unneeded sink

* remove unneeded namespace from guardname

* [indent]

* clean up
diff --git a/packages/pigeon/CHANGELOG.md b/packages/pigeon/CHANGELOG.md
index 1c47219..2bf8a72 100644
--- a/packages/pigeon/CHANGELOG.md
+++ b/packages/pigeon/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 6.0.0
+
+* Creates StructuredGenerator class and implements it on all platforms.
+
 ## 5.0.1
 
 * [c++] Fixes undefined behavior in `@async` methods.
diff --git a/packages/pigeon/lib/cpp_generator.dart b/packages/pigeon/lib/cpp_generator.dart
index ae5f912..105f6bd 100644
--- a/packages/pigeon/lib/cpp_generator.dart
+++ b/packages/pigeon/lib/cpp_generator.dart
@@ -72,105 +72,348 @@
 
 /// Class that manages all Cpp code generation.
 class CppGenerator extends Generator<OutputFileOptions<CppOptions>> {
-  /// Instantiates a Cpp Generator.
-  CppGenerator();
+  /// Constructor.
+  const CppGenerator();
 
-  /// Generates Cpp files with specified [OutputFileOptions<CppOptions>]
+  /// Generates C++ file of type specified in [generatorOptions]
   @override
-  void generate(OutputFileOptions<CppOptions> languageOptions, Root root,
+  void generate(OutputFileOptions<CppOptions> generatorOptions, Root root,
       StringSink sink) {
-    final FileType fileType = languageOptions.fileType;
-    assert(fileType == FileType.header || fileType == FileType.source);
-    if (fileType == FileType.header) {
-      generateCppHeader(languageOptions.languageOptions, root, sink);
-    } else {
-      generateCppSource(languageOptions.languageOptions, root, sink);
+    assert(generatorOptions.fileType == FileType.header ||
+        generatorOptions.fileType == FileType.source);
+    if (generatorOptions.fileType == FileType.header) {
+      const CppHeaderGenerator()
+          .generate(generatorOptions.languageOptions, root, sink);
+    } else if (generatorOptions.fileType == FileType.source) {
+      const CppSourceGenerator()
+          .generate(generatorOptions.languageOptions, root, sink);
     }
   }
 }
 
-String _getCodecSerializerName(Api api) => '${api.name}CodecSerializer';
+/// Writes C++ header (.h) file to sink.
+class CppHeaderGenerator extends StructuredGenerator<CppOptions> {
+  /// Constructor.
+  const CppHeaderGenerator();
 
-const String _pointerPrefix = 'pointer';
-const String _encodablePrefix = 'encodable';
+  @override
+  void writeFilePrologue(
+      CppOptions generatorOptions, Root root, Indent indent) {
+    if (generatorOptions.copyrightHeader != null) {
+      addLines(indent, generatorOptions.copyrightHeader!, linePrefix: '// ');
+    }
+    indent.writeln('$_commentPrefix $generatedCodeWarning');
+    indent.writeln('$_commentPrefix $seeAlsoWarning');
+    indent.addln('');
+  }
 
-void _writeCodecHeader(Indent indent, Api api, Root root) {
-  assert(getCodecClasses(api, root).isNotEmpty);
-  final String codeSerializerName = _getCodecSerializerName(api);
-  indent.write('class $codeSerializerName : public $_defaultCodecSerializer ');
-  indent.scoped('{', '};', () {
-    indent.scoped(' public:', '', () {
-      indent.writeln('');
-      indent.format('''
+  @override
+  void writeFileImports(CppOptions generatorOptions, Root root, Indent indent) {
+    final String guardName = _getGuardName(generatorOptions.headerIncludePath);
+    indent.writeln('#ifndef $guardName');
+    indent.writeln('#define $guardName');
+
+    _writeSystemHeaderIncludeBlock(indent, <String>[
+      'flutter/basic_message_channel.h',
+      'flutter/binary_messenger.h',
+      'flutter/encodable_value.h',
+      'flutter/standard_message_codec.h',
+    ]);
+    indent.addln('');
+    _writeSystemHeaderIncludeBlock(indent, <String>[
+      'map',
+      'string',
+      'optional',
+    ]);
+    indent.addln('');
+    if (generatorOptions.namespace != null) {
+      indent.writeln('namespace ${generatorOptions.namespace} {');
+    }
+    indent.addln('');
+    if (generatorOptions.namespace?.endsWith('_pigeontest') ?? false) {
+      final String testFixtureClass =
+          '${_pascalCaseFromSnakeCase(generatorOptions.namespace!.replaceAll('_pigeontest', ''))}Test';
+      indent.writeln('class $testFixtureClass;');
+    }
+    indent.addln('');
+    indent.writeln('$_commentPrefix Generated class from Pigeon.');
+  }
+
+  @override
+  void writeEnum(
+      CppOptions generatorOptions, Root root, Indent indent, Enum anEnum) {
+    indent.writeln('');
+    addDocumentationComments(
+        indent, anEnum.documentationComments, _docCommentSpec);
+    indent.write('enum class ${anEnum.name} ');
+    indent.scoped('{', '};', () {
+      enumerate(anEnum.members, (int index, final EnumMember member) {
+        addDocumentationComments(
+            indent, member.documentationComments, _docCommentSpec);
+        indent.writeln(
+            '${member.name} = $index${index == anEnum.members.length - 1 ? '' : ','}');
+      });
+    });
+  }
+
+  @override
+  void writeGeneralUtilities(
+      CppOptions generatorOptions, Root root, Indent indent) {
+    _writeErrorOr(indent, friends: root.apis.map((Api api) => api.name));
+  }
+
+  @override
+  void writeDataClass(
+      CppOptions generatorOptions, Root root, Indent indent, Class klass) {
+    // When generating for a Pigeon unit test, add a test fixture friend class to
+    // allow unit testing private methods, since testing serialization via public
+    // methods is essentially an end-to-end test.
+    String? testFixtureClass;
+    if (generatorOptions.namespace?.endsWith('_pigeontest') ?? false) {
+      testFixtureClass =
+          '${_pascalCaseFromSnakeCase(generatorOptions.namespace!.replaceAll('_pigeontest', ''))}Test';
+    }
+    indent.addln('');
+
+    const List<String> generatedMessages = <String>[
+      ' Generated class from Pigeon that represents data sent in messages.'
+    ];
+
+    addDocumentationComments(
+        indent, klass.documentationComments, _docCommentSpec,
+        generatorComments: generatedMessages);
+
+    indent.write('class ${klass.name} ');
+    indent.scoped('{', '};', () {
+      indent.scoped(' public:', '', () {
+        indent.writeln('${klass.name}();');
+        for (final NamedType field in getFieldsInSerializationOrder(klass)) {
+          addDocumentationComments(
+              indent, field.documentationComments, _docCommentSpec);
+          final HostDatatype baseDatatype = getFieldHostDatatype(
+              field,
+              root.classes,
+              root.enums,
+              (TypeDeclaration x) => _baseCppTypeForBuiltinDartType(x));
+          indent.writeln(
+              '${_getterReturnType(baseDatatype)} ${_makeGetterName(field)}() const;');
+          indent.writeln(
+              'void ${_makeSetterName(field)}(${_unownedArgumentType(baseDatatype)} value_arg);');
+          if (field.type.isNullable) {
+            // Add a second setter that takes the non-nullable version of the
+            // argument for convenience, since setting literal values with the
+            // pointer version is non-trivial.
+            final HostDatatype nonNullType = _nonNullableType(baseDatatype);
+            indent.writeln(
+                'void ${_makeSetterName(field)}(${_unownedArgumentType(nonNullType)} value_arg);');
+          }
+          indent.addln('');
+        }
+      });
+
+      indent.scoped(' private:', '', () {
+        indent.writeln('${klass.name}(const flutter::EncodableList& list);');
+        indent.writeln('flutter::EncodableList ToEncodableList() const;');
+        for (final Class friend in root.classes) {
+          if (friend != klass &&
+              friend.fields.any(
+                  (NamedType element) => element.type.baseName == klass.name)) {
+            indent.writeln('friend class ${friend.name};');
+          }
+        }
+        for (final Api api in root.apis) {
+          // TODO(gaaclarke): Find a way to be more precise with our
+          // friendships.
+          indent.writeln('friend class ${api.name};');
+          indent.writeln('friend class ${_getCodecSerializerName(api)};');
+        }
+        if (testFixtureClass != null) {
+          indent.writeln('friend class $testFixtureClass;');
+        }
+
+        for (final NamedType field in getFieldsInSerializationOrder(klass)) {
+          final HostDatatype hostDatatype = getFieldHostDatatype(
+              field,
+              root.classes,
+              root.enums,
+              (TypeDeclaration x) => _baseCppTypeForBuiltinDartType(x));
+          indent.writeln(
+              '${_valueType(hostDatatype)} ${_makeInstanceVariableName(field)};');
+        }
+      });
+    }, nestCount: 0);
+    indent.writeln('');
+  }
+
+  @override
+  void writeFlutterApi(
+    CppOptions generatorOptions,
+    Root root,
+    Indent indent,
+    Api api,
+  ) {
+    assert(api.location == ApiLocation.flutter);
+    if (getCodecClasses(api, root).isNotEmpty) {
+      _writeCodec(generatorOptions, root, indent, api);
+    }
+    const List<String> generatedMessages = <String>[
+      ' Generated class from Pigeon that represents Flutter messages that can be called from C++.'
+    ];
+    addDocumentationComments(indent, api.documentationComments, _docCommentSpec,
+        generatorComments: generatedMessages);
+    indent.write('class ${api.name} ');
+    indent.scoped('{', '};', () {
+      indent.scoped(' private:', '', () {
+        indent.writeln('flutter::BinaryMessenger* binary_messenger_;');
+      });
+      indent.scoped(' public:', '', () {
+        indent
+            .write('${api.name}(flutter::BinaryMessenger* binary_messenger);');
+        indent.writeln('');
+        indent
+            .writeln('static const flutter::StandardMessageCodec& GetCodec();');
+        for (final Method func in api.methods) {
+          final String returnType = func.returnType.isVoid
+              ? 'void'
+              : _nullSafeCppTypeForDartType(func.returnType);
+          final String callback = 'std::function<void($returnType)>&& callback';
+          addDocumentationComments(
+              indent, func.documentationComments, _docCommentSpec);
+          if (func.arguments.isEmpty) {
+            indent.writeln('void ${func.name}($callback);');
+          } else {
+            final Iterable<String> argTypes = func.arguments
+                .map((NamedType e) => _nullSafeCppTypeForDartType(e.type));
+            final Iterable<String> argNames =
+                indexMap(func.arguments, _getSafeArgumentName);
+            final String argsSignature =
+                map2(argTypes, argNames, (String x, String y) => '$x $y')
+                    .join(', ');
+            indent.writeln('void ${func.name}($argsSignature, $callback);');
+          }
+        }
+      });
+    }, nestCount: 0);
+    indent.writeln('');
+  }
+
+  @override
+  void writeHostApi(
+    CppOptions generatorOptions,
+    Root root,
+    Indent indent,
+    Api api,
+  ) {
+    assert(api.location == ApiLocation.host);
+    if (getCodecClasses(api, root).isNotEmpty) {
+      _writeCodec(generatorOptions, root, indent, api);
+    }
+    const List<String> generatedMessages = <String>[
+      ' Generated interface from Pigeon that represents a handler of messages from Flutter.'
+    ];
+    addDocumentationComments(indent, api.documentationComments, _docCommentSpec,
+        generatorComments: generatedMessages);
+    indent.write('class ${api.name} ');
+    indent.scoped('{', '};', () {
+      indent.scoped(' public:', '', () {
+        indent.writeln('${api.name}(const ${api.name}&) = delete;');
+        indent.writeln('${api.name}& operator=(const ${api.name}&) = delete;');
+        indent.writeln('virtual ~${api.name}() { };');
+        for (final Method method in api.methods) {
+          final HostDatatype returnType = getHostDatatype(
+              method.returnType,
+              root.classes,
+              root.enums,
+              (TypeDeclaration x) => _baseCppTypeForBuiltinDartType(x));
+          final String returnTypeName = _apiReturnType(returnType);
+
+          final List<String> argSignature = <String>[];
+          if (method.arguments.isNotEmpty) {
+            final Iterable<String> argTypes =
+                method.arguments.map((NamedType arg) {
+              final HostDatatype hostType = getFieldHostDatatype(
+                  arg,
+                  root.classes,
+                  root.enums,
+                  (TypeDeclaration x) => _baseCppTypeForBuiltinDartType(x));
+              return _hostApiArgumentType(hostType);
+            });
+            final Iterable<String> argNames =
+                method.arguments.map((NamedType e) => _makeVariableName(e));
+            argSignature.addAll(
+                map2(argTypes, argNames, (String argType, String argName) {
+              return '$argType $argName';
+            }));
+          }
+
+          addDocumentationComments(
+              indent, method.documentationComments, _docCommentSpec);
+
+          if (method.isAsynchronous) {
+            argSignature
+                .add('std::function<void($returnTypeName reply)> result');
+            indent.writeln(
+                'virtual void ${_makeMethodName(method)}(${argSignature.join(', ')}) = 0;');
+          } else {
+            indent.writeln(
+                'virtual $returnTypeName ${_makeMethodName(method)}(${argSignature.join(', ')}) = 0;');
+          }
+        }
+        indent.addln('');
+        indent.writeln('$_commentPrefix The codec used by ${api.name}.');
+        indent
+            .writeln('static const flutter::StandardMessageCodec& GetCodec();');
+        indent.writeln(
+            '$_commentPrefix Sets up an instance of `${api.name}` to handle messages through the `binary_messenger`.');
+        indent.writeln(
+            'static void SetUp(flutter::BinaryMessenger* binary_messenger, ${api.name}* api);');
+        indent.writeln(
+            'static flutter::EncodableValue WrapError(std::string_view error_message);');
+        indent.writeln(
+            'static flutter::EncodableValue WrapError(const FlutterError& error);');
+      });
+      indent.scoped(' protected:', '', () {
+        indent.writeln('${api.name}() = default;');
+      });
+    }, nestCount: 0);
+  }
+
+  void _writeCodec(
+      CppOptions generatorOptions, Root root, Indent indent, Api api) {
+    assert(getCodecClasses(api, root).isNotEmpty);
+    final String codeSerializerName = _getCodecSerializerName(api);
+    indent
+        .write('class $codeSerializerName : public $_defaultCodecSerializer ');
+    indent.scoped('{', '};', () {
+      indent.scoped(' public:', '', () {
+        indent.writeln('');
+        indent.format('''
 inline static $codeSerializerName& GetInstance() {
 \tstatic $codeSerializerName sInstance;
 \treturn sInstance;
 }
 ''');
-      indent.writeln('$codeSerializerName();');
-    });
-    indent.writeScoped(' public:', '', () {
-      indent.writeln(
-          'void WriteValue(const flutter::EncodableValue& value, flutter::ByteStreamWriter* stream) const override;');
-    });
-    indent.writeScoped(' protected:', '', () {
-      indent.writeln(
-          'flutter::EncodableValue ReadValueOfType(uint8_t type, flutter::ByteStreamReader* stream) const override;');
-    });
-  }, nestCount: 0);
-}
-
-void _writeCodecSource(Indent indent, Api api, Root root) {
-  assert(getCodecClasses(api, root).isNotEmpty);
-  final String codeSerializerName = _getCodecSerializerName(api);
-  indent.writeln('$codeSerializerName::$codeSerializerName() {}');
-  indent.write(
-      'flutter::EncodableValue $codeSerializerName::ReadValueOfType(uint8_t type, flutter::ByteStreamReader* stream) const ');
-  indent.scoped('{', '}', () {
-    indent.write('switch (type) ');
-    indent.scoped('{', '}', () {
-      for (final EnumeratedClass customClass in getCodecClasses(api, root)) {
-        indent.write('case ${customClass.enumeration}:');
-        indent.writeScoped('', '', () {
-          indent.writeln(
-              'return flutter::CustomEncodableValue(${customClass.name}(std::get<flutter::EncodableList>(ReadValue(stream))));');
-        });
-      }
-      indent.write('default:');
-      indent.writeScoped('', '', () {
+        indent.writeln('$codeSerializerName();');
+      });
+      indent.writeScoped(' public:', '', () {
         indent.writeln(
-            'return $_defaultCodecSerializer::ReadValueOfType(type, stream);');
-      }, addTrailingNewline: false);
-    });
-  });
-  indent.writeln('');
-  indent.write(
-      'void $codeSerializerName::WriteValue(const flutter::EncodableValue& value, flutter::ByteStreamWriter* stream) const ');
-  indent.writeScoped('{', '}', () {
-    indent.write(
-        'if (const flutter::CustomEncodableValue* custom_value = std::get_if<flutter::CustomEncodableValue>(&value)) ');
-    indent.scoped('{', '}', () {
-      for (final EnumeratedClass customClass in getCodecClasses(api, root)) {
-        indent
-            .write('if (custom_value->type() == typeid(${customClass.name})) ');
-        indent.scoped('{', '}', () {
-          indent.writeln('stream->WriteByte(${customClass.enumeration});');
-          indent.writeln(
-              'WriteValue(flutter::EncodableValue(std::any_cast<${customClass.name}>(*custom_value).ToEncodableList()), stream);');
-          indent.writeln('return;');
-        });
-      }
-    });
-    indent.writeln('$_defaultCodecSerializer::WriteValue(value, stream);');
-  });
-}
+            'void WriteValue(const flutter::EncodableValue& value, flutter::ByteStreamWriter* stream) const override;');
+      });
+      indent.writeScoped(' protected:', '', () {
+        indent.writeln(
+            'flutter::EncodableValue ReadValueOfType(uint8_t type, flutter::ByteStreamReader* stream) const override;');
+      });
+    }, nestCount: 0);
+    indent.addln('');
+  }
 
-void _writeErrorOr(Indent indent,
-    {Iterable<String> friends = const <String>[]}) {
-  final String friendLines = friends
-      .map((String className) => '\tfriend class $className;')
-      .join('\n');
-  indent.format('''
+  void _writeErrorOr(Indent indent,
+      {Iterable<String> friends = const <String>[]}) {
+    final String friendLines = friends
+        .map((String className) => '\tfriend class $className;')
+        .join('\n');
+    indent.format('''
+
 class FlutterError {
  public:
 \texplicit FlutterError(const std::string& code)
@@ -211,101 +454,563 @@
 \tstd::variant<T, FlutterError> v_;
 };
 ''');
+  }
+
+  @override
+  void writeCloseNamespace(
+      CppOptions generatorOptions, Root root, Indent indent) {
+    if (generatorOptions.namespace != null) {
+      indent.writeln('}  // namespace ${generatorOptions.namespace}');
+    }
+    final String guardName = _getGuardName(generatorOptions.headerIncludePath);
+    indent.writeln('#endif  // $guardName');
+  }
 }
 
-/// Writes the declaration for the custom class [klass].
-///
-/// See [_writeDataClassImplementation] for the corresponding declaration.
-/// This is intended to be added to the header.
-void _writeDataClassDeclaration(Indent indent, Class klass, Root root,
-    {String? testFriend}) {
-  indent.addln('');
+/// Writes C++ source (.cpp) file to sink.
+class CppSourceGenerator extends StructuredGenerator<CppOptions> {
+  /// Constructor.
+  const CppSourceGenerator();
 
-  const List<String> generatedMessages = <String>[
-    ' Generated class from Pigeon that represents data sent in messages.'
-  ];
+  @override
+  void writeFilePrologue(
+      CppOptions generatorOptions, Root root, Indent indent) {
+    if (generatorOptions.copyrightHeader != null) {
+      addLines(indent, generatorOptions.copyrightHeader!, linePrefix: '// ');
+    }
+    indent.writeln('$_commentPrefix $generatedCodeWarning');
+    indent.writeln('$_commentPrefix $seeAlsoWarning');
+    indent.addln('');
+    indent.addln('#undef _HAS_EXCEPTIONS');
+    indent.addln('');
+  }
 
-  addDocumentationComments(indent, klass.documentationComments, _docCommentSpec,
-      generatorComments: generatedMessages);
+  @override
+  void writeFileImports(CppOptions generatorOptions, Root root, Indent indent) {
+    indent.writeln('#include "${generatorOptions.headerIncludePath}"');
+    indent.addln('');
+    _writeSystemHeaderIncludeBlock(indent, <String>[
+      'flutter/basic_message_channel.h',
+      'flutter/binary_messenger.h',
+      'flutter/encodable_value.h',
+      'flutter/standard_message_codec.h',
+    ]);
+    indent.addln('');
+    _writeSystemHeaderIncludeBlock(indent, <String>[
+      'map',
+      'string',
+      'optional',
+    ]);
+    indent.addln('');
+  }
 
-  indent.write('class ${klass.name} ');
-  indent.scoped('{', '};', () {
-    indent.scoped(' public:', '', () {
-      indent.writeln('${klass.name}();');
-      for (final NamedType field in getFieldsInSerializationOrder(klass)) {
-        addDocumentationComments(
-            indent, field.documentationComments, _docCommentSpec);
-        final HostDatatype baseDatatype = getFieldHostDatatype(
-            field,
-            root.classes,
-            root.enums,
-            (TypeDeclaration x) => _baseCppTypeForBuiltinDartType(x));
-        indent.writeln(
-            '${_getterReturnType(baseDatatype)} ${_makeGetterName(field)}() const;');
-        indent.writeln(
-            'void ${_makeSetterName(field)}(${_unownedArgumentType(baseDatatype)} value_arg);');
-        if (field.type.isNullable) {
-          // Add a second setter that takes the non-nullable version of the
-          // argument for convenience, since setting literal values with the
-          // pointer version is non-trivial.
-          final HostDatatype nonNullType = _nonNullableType(baseDatatype);
+  @override
+  void writeOpenNamespace(
+      CppOptions generatorOptions, Root root, Indent indent) {
+    if (generatorOptions.namespace != null) {
+      indent.writeln('namespace ${generatorOptions.namespace} {');
+    }
+  }
+
+  @override
+  void writeDataClass(
+      CppOptions generatorOptions, Root root, Indent indent, Class klass) {
+    final Set<String> customClassNames =
+        root.classes.map((Class x) => x.name).toSet();
+    final Set<String> customEnumNames =
+        root.enums.map((Enum x) => x.name).toSet();
+
+    indent.addln('');
+    indent.writeln('$_commentPrefix ${klass.name}');
+    indent.addln('');
+
+    // Getters and setters.
+    for (final NamedType field in getFieldsInSerializationOrder(klass)) {
+      _writeCppSourceClassField(generatorOptions, root, indent, klass, field);
+    }
+
+    // Serialization.
+    writeClassEncode(generatorOptions, root, indent, klass, customClassNames,
+        customEnumNames);
+
+    // Default constructor.
+    indent.writeln('${klass.name}::${klass.name}() {}');
+    indent.addln('');
+
+    // Deserialization.
+    writeClassDecode(generatorOptions, root, indent, klass, customClassNames,
+        customEnumNames);
+  }
+
+  @override
+  void writeClassEncode(
+    CppOptions generatorOptions,
+    Root root,
+    Indent indent,
+    Class klass,
+    Set<String> customClassNames,
+    Set<String> customEnumNames,
+  ) {
+    indent.write(
+        'flutter::EncodableList ${klass.name}::ToEncodableList() const ');
+    indent.scoped('{', '}', () {
+      indent.scoped('return flutter::EncodableList{', '};', () {
+        for (final NamedType field in getFieldsInSerializationOrder(klass)) {
+          final HostDatatype hostDatatype = getFieldHostDatatype(
+              field,
+              root.classes,
+              root.enums,
+              (TypeDeclaration x) => _baseCppTypeForBuiltinDartType(x));
+
+          final String instanceVariable = _makeInstanceVariableName(field);
+
+          String encodableValue = '';
+          if (!hostDatatype.isBuiltin &&
+              customClassNames.contains(field.type.baseName)) {
+            final String operator = field.type.isNullable ? '->' : '.';
+            encodableValue =
+                'flutter::EncodableValue($instanceVariable${operator}ToEncodableList())';
+          } else if (!hostDatatype.isBuiltin &&
+              customEnumNames.contains(field.type.baseName)) {
+            final String nonNullValue = field.type.isNullable
+                ? '(*$instanceVariable)'
+                : instanceVariable;
+            encodableValue = 'flutter::EncodableValue((int)$nonNullValue)';
+          } else {
+            final String operator = field.type.isNullable ? '*' : '';
+            encodableValue =
+                'flutter::EncodableValue($operator$instanceVariable)';
+          }
+
+          if (field.type.isNullable) {
+            encodableValue =
+                '$instanceVariable ? $encodableValue : flutter::EncodableValue()';
+          }
+
+          indent.writeln('$encodableValue,');
+        }
+      });
+    });
+    indent.addln('');
+  }
+
+  @override
+  void writeClassDecode(
+    CppOptions generatorOptions,
+    Root root,
+    Indent indent,
+    Class klass,
+    Set<String> customClassNames,
+    Set<String> customEnumNames,
+  ) {
+    indent.write(
+        '${klass.name}::${klass.name}(const flutter::EncodableList& list) ');
+    indent.scoped('{', '}', () {
+      enumerate(getFieldsInSerializationOrder(klass),
+          (int index, final NamedType field) {
+        final String instanceVariableName = _makeInstanceVariableName(field);
+        final String pointerFieldName =
+            '${_pointerPrefix}_${_makeVariableName(field)}';
+        final String encodableFieldName =
+            '${_encodablePrefix}_${_makeVariableName(field)}';
+        indent.writeln('auto& $encodableFieldName = list[$index];');
+        if (customEnumNames.contains(field.type.baseName)) {
           indent.writeln(
-              'void ${_makeSetterName(field)}(${_unownedArgumentType(nonNullType)} value_arg);');
+              'if (const int32_t* $pointerFieldName = std::get_if<int32_t>(&$encodableFieldName))\t$instanceVariableName = (${field.type.baseName})*$pointerFieldName;');
+        } else {
+          final HostDatatype hostDatatype = getFieldHostDatatype(
+              field,
+              root.classes,
+              root.enums,
+              (TypeDeclaration x) => _baseCppTypeForBuiltinDartType(x));
+          if (field.type.baseName == 'int') {
+            indent.format('''
+if (const int32_t* $pointerFieldName = std::get_if<int32_t>(&$encodableFieldName))
+\t$instanceVariableName = *$pointerFieldName;
+else if (const int64_t* ${pointerFieldName}_64 = std::get_if<int64_t>(&$encodableFieldName))
+\t$instanceVariableName = *${pointerFieldName}_64;''');
+          } else if (!hostDatatype.isBuiltin &&
+              root.classes
+                  .map((Class x) => x.name)
+                  .contains(field.type.baseName)) {
+            indent.write(
+                'if (const flutter::EncodableList* $pointerFieldName = std::get_if<flutter::EncodableList>(&$encodableFieldName)) ');
+            indent.scoped('{', '}', () {
+              indent.writeln(
+                  '$instanceVariableName = ${hostDatatype.datatype}(*$pointerFieldName);');
+            });
+          } else {
+            indent.write(
+                'if (const ${hostDatatype.datatype}* $pointerFieldName = std::get_if<${hostDatatype.datatype}>(&$encodableFieldName)) ');
+            indent.scoped('{', '}', () {
+              indent.writeln('$instanceVariableName = *$pointerFieldName;');
+            });
+          }
         }
-        indent.addln('');
-      }
+      });
     });
+    indent.addln('');
+  }
 
-    indent.scoped(' private:', '', () {
-      indent.writeln('${klass.name}(const flutter::EncodableList& list);');
-      indent.writeln('flutter::EncodableList ToEncodableList() const;');
-      for (final Class friend in root.classes) {
-        if (friend != klass &&
-            friend.fields.any(
-                (NamedType element) => element.type.baseName == klass.name)) {
-          indent.writeln('friend class ${friend.name};');
-        }
-      }
-      for (final Api api in root.apis) {
-        // TODO(gaaclarke): Find a way to be more precise with our
-        // friendships.
-        indent.writeln('friend class ${api.name};');
-        indent.writeln('friend class ${_getCodecSerializerName(api)};');
-      }
-      if (testFriend != null) {
-        indent.writeln('friend class $testFriend;');
-      }
-
-      for (final NamedType field in getFieldsInSerializationOrder(klass)) {
-        final HostDatatype hostDatatype = getFieldHostDatatype(
-            field,
-            root.classes,
-            root.enums,
-            (TypeDeclaration x) => _baseCppTypeForBuiltinDartType(x));
-        indent.writeln(
-            '${_valueType(hostDatatype)} ${_makeInstanceVariableName(field)};');
-      }
+  @override
+  void writeFlutterApi(
+    CppOptions generatorOptions,
+    Root root,
+    Indent indent,
+    Api api,
+  ) {
+    assert(api.location == ApiLocation.flutter);
+    if (getCodecClasses(api, root).isNotEmpty) {
+      _writeCodec(generatorOptions, root, indent, api);
+    }
+    indent.writeln(
+        '$_commentPrefix Generated class from Pigeon that represents Flutter messages that can be called from C++.');
+    indent.write(
+        '${api.name}::${api.name}(flutter::BinaryMessenger* binary_messenger) ');
+    indent.scoped('{', '}', () {
+      indent.writeln('this->binary_messenger_ = binary_messenger;');
     });
-  }, nestCount: 0);
-  indent.writeln('');
+    indent.writeln('');
+    final String codeSerializerName = getCodecClasses(api, root).isNotEmpty
+        ? _getCodecSerializerName(api)
+        : _defaultCodecSerializer;
+    indent.format('''
+const flutter::StandardMessageCodec& ${api.name}::GetCodec() {
+\treturn flutter::StandardMessageCodec::GetInstance(&$codeSerializerName::GetInstance());
 }
+''');
+    for (final Method func in api.methods) {
+      final String channelName = makeChannelName(api, func);
+      final String returnType = func.returnType.isVoid
+          ? 'void'
+          : _nullSafeCppTypeForDartType(func.returnType);
+      String sendArgument;
+      final String callback = 'std::function<void($returnType)>&& callback';
+      if (func.arguments.isEmpty) {
+        indent.write('void ${api.name}::${func.name}($callback) ');
+        sendArgument = 'flutter::EncodableValue()';
+      } else {
+        final Iterable<String> argTypes = func.arguments
+            .map((NamedType e) => _nullSafeCppTypeForDartType(e.type));
+        final Iterable<String> argNames =
+            indexMap(func.arguments, _getSafeArgumentName);
+        sendArgument =
+            'flutter::EncodableList { ${argNames.map((String arg) => 'flutter::CustomEncodableValue($arg)').join(', ')} }';
+        final String argsSignature =
+            map2(argTypes, argNames, (String x, String y) => '$x $y')
+                .join(', ');
+        indent.write(
+            'void ${api.name}::${func.name}($argsSignature, $callback) ');
+      }
+      indent.scoped('{', '}', () {
+        const String channel = 'channel';
+        indent.writeln(
+            'auto channel = std::make_unique<flutter::BasicMessageChannel<flutter::EncodableValue>>(');
+        indent.inc();
+        indent.inc();
+        indent.writeln('binary_messenger_, "$channelName", &GetCodec());');
+        indent.dec();
+        indent.dec();
+        indent.write(
+            '$channel->Send($sendArgument, [callback](const uint8_t* reply, size_t reply_size) ');
+        indent.scoped('{', '});', () {
+          if (func.returnType.isVoid) {
+            indent.writeln('callback();');
+          } else {
+            indent.writeln(
+                'std::unique_ptr<flutter::EncodableValue> decoded_reply = GetCodec().DecodeMessage(reply, reply_size);');
+            indent.writeln(
+                'flutter::EncodableValue args = *(flutter::EncodableValue*)(decoded_reply.release());');
+            const String output = 'output';
 
-/// Writes the implementation for the custom class [klass].
-///
-/// See [_writeDataClassDeclaration] for the corresponding declaration.
-/// This is intended to be added to the implementation file.
-void _writeDataClassImplementation(Indent indent, Class klass, Root root) {
-  final Set<String> rootClassNameSet =
-      root.classes.map((Class x) => x.name).toSet();
-  final Set<String> rootEnumNameSet =
-      root.enums.map((Enum x) => x.name).toSet();
+            final bool isBuiltin =
+                _baseCppTypeForBuiltinDartType(func.returnType) != null;
+            final String returnTypeName =
+                _baseCppTypeForDartType(func.returnType);
+            if (func.returnType.isNullable) {
+              indent.writeln('$returnType $output{};');
+            } else {
+              indent.writeln('$returnTypeName $output{};');
+            }
+            const String pointerVariable = '${_pointerPrefix}_$output';
+            if (func.returnType.baseName == 'int') {
+              indent.format('''
+if (const int32_t* $pointerVariable = std::get_if<int32_t>(&args))
+\t$output = *$pointerVariable;
+else if (const int64_t* ${pointerVariable}_64 = std::get_if<int64_t>(&args))
+\t$output = *${pointerVariable}_64;''');
+            } else if (!isBuiltin) {
+              indent.write(
+                  'if (const flutter::EncodableList* $pointerVariable = std::get_if<flutter::EncodableList>(&args)) ');
+              indent.scoped('{', '}', () {
+                indent.writeln('$output = $returnTypeName(*$pointerVariable);');
+              });
+            } else {
+              indent.write(
+                  'if (const $returnTypeName* $pointerVariable = std::get_if<$returnTypeName>(&args)) ');
+              indent.scoped('{', '}', () {
+                indent.writeln('$output = *$pointerVariable;');
+              });
+            }
 
-  indent.addln('');
-  indent.writeln('$_commentPrefix ${klass.name}');
-  indent.addln('');
+            indent.writeln('callback($output);');
+          }
+        });
+      });
+    }
+  }
 
-  // Getters and setters.
-  for (final NamedType field in getFieldsInSerializationOrder(klass)) {
+  @override
+  void writeHostApi(
+      CppOptions generatorOptions, Root root, Indent indent, Api api) {
+    assert(api.location == ApiLocation.host);
+    if (getCodecClasses(api, root).isNotEmpty) {
+      _writeCodec(generatorOptions, root, indent, api);
+    }
+
+    final String codeSerializerName = getCodecClasses(api, root).isNotEmpty
+        ? _getCodecSerializerName(api)
+        : _defaultCodecSerializer;
+    indent.format('''
+/// The codec used by ${api.name}.
+const flutter::StandardMessageCodec& ${api.name}::GetCodec() {
+\treturn flutter::StandardMessageCodec::GetInstance(&$codeSerializerName::GetInstance());
+}
+''');
+    indent.writeln(
+        '$_commentPrefix Sets up an instance of `${api.name}` to handle messages through the `binary_messenger`.');
+    indent.write(
+        'void ${api.name}::SetUp(flutter::BinaryMessenger* binary_messenger, ${api.name}* api) ');
+    indent.scoped('{', '}', () {
+      for (final Method method in api.methods) {
+        final String channelName = makeChannelName(api, method);
+        indent.write('');
+        indent.scoped('{', '}', () {
+          indent.writeln(
+              'auto channel = std::make_unique<flutter::BasicMessageChannel<flutter::EncodableValue>>(');
+          indent.inc();
+          indent.inc();
+          indent.writeln('binary_messenger, "$channelName", &GetCodec());');
+          indent.dec();
+          indent.dec();
+          indent.write('if (api != nullptr) ');
+          indent.scoped('{', '} else {', () {
+            indent.write(
+                'channel->SetMessageHandler([api](const flutter::EncodableValue& message, const flutter::MessageReply<flutter::EncodableValue>& reply) ');
+            indent.scoped('{', '});', () {
+              indent.write('try ');
+              indent.scoped('{', '}', () {
+                final List<String> methodArgument = <String>[];
+                if (method.arguments.isNotEmpty) {
+                  indent.writeln(
+                      'const auto& args = std::get<flutter::EncodableList>(message);');
+
+                  // Writes the code to declare and populate a variable called
+                  // [argName] to use as a parameter to an API method call from
+                  // an existing EncodablValue variable called [encodableArgName]
+                  // which corresponds to [arg] in the API definition.
+                  void extractEncodedArgument(
+                      String argName,
+                      String encodableArgName,
+                      NamedType arg,
+                      HostDatatype hostType) {
+                    if (arg.type.isNullable) {
+                      // Nullable arguments are always pointers, with nullptr
+                      // corresponding to null.
+                      if (hostType.datatype == 'int64_t') {
+                        // The EncodableValue will either be an int32_t or an
+                        // int64_t depending on the value, but the generated API
+                        // requires an int64_t so that it can handle any case.
+                        // Create a local variable for the 64-bit value...
+                        final String valueVarName = '${argName}_value';
+                        indent.writeln(
+                            'const int64_t $valueVarName = $encodableArgName.IsNull() ? 0 : $encodableArgName.LongValue();');
+                        // ... then declare the arg as a reference to that local.
+                        indent.writeln(
+                            'const auto* $argName = $encodableArgName.IsNull() ? nullptr : &$valueVarName;');
+                      } else if (hostType.datatype ==
+                          'flutter::EncodableValue') {
+                        // Generic objects just pass the EncodableValue through
+                        // directly.
+                        indent.writeln(
+                            'const auto* $argName = &$encodableArgName;');
+                      } else if (hostType.isBuiltin) {
+                        indent.writeln(
+                            'const auto* $argName = std::get_if<${hostType.datatype}>(&$encodableArgName);');
+                      } else {
+                        indent.writeln(
+                            'const auto* $argName = &(std::any_cast<const ${hostType.datatype}&>(std::get<flutter::CustomEncodableValue>($encodableArgName)));');
+                      }
+                    } else {
+                      // Non-nullable arguments are either passed by value or
+                      // reference, but the extraction doesn't need to distinguish
+                      // since those are the same at the call site.
+                      if (hostType.datatype == 'int64_t') {
+                        // The EncodableValue will either be an int32_t or an
+                        // int64_t depending on the value, but the generated API
+                        // requires an int64_t so that it can handle any case.
+                        indent.writeln(
+                            'const int64_t $argName = $encodableArgName.LongValue();');
+                      } else if (hostType.datatype ==
+                          'flutter::EncodableValue') {
+                        // Generic objects just pass the EncodableValue through
+                        // directly. This creates an alias just to avoid having to
+                        // special-case the argName/encodableArgName distinction
+                        // at a higher level.
+                        indent.writeln(
+                            'const auto& $argName = $encodableArgName;');
+                      } else if (hostType.isBuiltin) {
+                        indent.writeln(
+                            'const auto& $argName = std::get<${hostType.datatype}>($encodableArgName);');
+                      } else {
+                        indent.writeln(
+                            'const auto& $argName = std::any_cast<const ${hostType.datatype}&>(std::get<flutter::CustomEncodableValue>($encodableArgName));');
+                      }
+                    }
+                  }
+
+                  enumerate(method.arguments, (int index, NamedType arg) {
+                    final HostDatatype hostType = getHostDatatype(
+                        arg.type,
+                        root.classes,
+                        root.enums,
+                        (TypeDeclaration x) =>
+                            _baseCppTypeForBuiltinDartType(x));
+                    final String argName = _getSafeArgumentName(index, arg);
+
+                    final String encodableArgName =
+                        '${_encodablePrefix}_$argName';
+                    indent.writeln(
+                        'const auto& $encodableArgName = args.at($index);');
+                    if (!arg.type.isNullable) {
+                      indent.write('if ($encodableArgName.IsNull()) ');
+                      indent.scoped('{', '}', () {
+                        indent.writeln(
+                            'reply(WrapError("$argName unexpectedly null."));');
+                        indent.writeln('return;');
+                      });
+                    }
+                    extractEncodedArgument(
+                        argName, encodableArgName, arg, hostType);
+                    methodArgument.add(argName);
+                  });
+                }
+
+                final HostDatatype returnType = getHostDatatype(
+                    method.returnType,
+                    root.classes,
+                    root.enums,
+                    (TypeDeclaration x) => _baseCppTypeForBuiltinDartType(x));
+                final String returnTypeName = _apiReturnType(returnType);
+                if (method.isAsynchronous) {
+                  methodArgument.add(
+                    '[reply]($returnTypeName&& output) {${indent.newline}'
+                    '${_wrapResponse(indent, root, method.returnType, prefix: '\t')}${indent.newline}'
+                    '}',
+                  );
+                }
+                final String call =
+                    'api->${_makeMethodName(method)}(${methodArgument.join(', ')})';
+                if (method.isAsynchronous) {
+                  indent.format('$call;');
+                } else {
+                  indent.writeln('$returnTypeName output = $call;');
+                  indent.format(_wrapResponse(indent, root, method.returnType));
+                }
+              });
+              indent.write('catch (const std::exception& exception) ');
+              indent.scoped('{', '}', () {
+                // There is a potential here for `reply` to be called twice, which
+                // is a violation of the API contract, because there's no way of
+                // knowing whether or not the plugin code called `reply` before
+                // throwing. Since use of `@async` suggests that the reply is
+                // probably not sent within the scope of the stack, err on the
+                // side of potential double-call rather than no call (which is
+                // also an API violation) so that unexpected errors have a better
+                // chance of being caught and handled in a useful way.
+                indent.writeln('reply(WrapError(exception.what()));');
+              });
+            });
+          });
+          indent.scoped(null, '}', () {
+            indent.writeln('channel->SetMessageHandler(nullptr);');
+          });
+        });
+      }
+    });
+
+    indent.addln('');
+    indent.format('''
+flutter::EncodableValue ${api.name}::WrapError(std::string_view error_message) {
+\treturn flutter::EncodableValue(flutter::EncodableList{
+\t\tflutter::EncodableValue(std::string(error_message)),
+\t\tflutter::EncodableValue("Error"),
+\t\tflutter::EncodableValue()
+\t});
+}
+flutter::EncodableValue ${api.name}::WrapError(const FlutterError& error) {
+\treturn flutter::EncodableValue(flutter::EncodableList{
+\t\tflutter::EncodableValue(error.message()),
+\t\tflutter::EncodableValue(error.code()),
+\t\terror.details()
+\t});
+}''');
+    indent.addln('');
+  }
+
+  void _writeCodec(
+    CppOptions generatorOptions,
+    Root root,
+    Indent indent,
+    Api api,
+  ) {
+    assert(getCodecClasses(api, root).isNotEmpty);
+    final String codeSerializerName = _getCodecSerializerName(api);
+    indent.writeln('$codeSerializerName::$codeSerializerName() {}');
+    indent.write(
+        'flutter::EncodableValue $codeSerializerName::ReadValueOfType(uint8_t type, flutter::ByteStreamReader* stream) const ');
+    indent.scoped('{', '}', () {
+      indent.write('switch (type) ');
+      indent.scoped('{', '}', () {
+        for (final EnumeratedClass customClass in getCodecClasses(api, root)) {
+          indent.write('case ${customClass.enumeration}:');
+          indent.writeScoped('', '', () {
+            indent.writeln(
+                'return flutter::CustomEncodableValue(${customClass.name}(std::get<flutter::EncodableList>(ReadValue(stream))));');
+          });
+        }
+        indent.write('default:');
+        indent.writeScoped('', '', () {
+          indent.writeln(
+              'return $_defaultCodecSerializer::ReadValueOfType(type, stream);');
+        }, addTrailingNewline: false);
+      });
+    });
+    indent.writeln('');
+    indent.write(
+        'void $codeSerializerName::WriteValue(const flutter::EncodableValue& value, flutter::ByteStreamWriter* stream) const ');
+    indent.writeScoped('{', '}', () {
+      indent.write(
+          'if (const flutter::CustomEncodableValue* custom_value = std::get_if<flutter::CustomEncodableValue>(&value)) ');
+      indent.scoped('{', '}', () {
+        for (final EnumeratedClass customClass in getCodecClasses(api, root)) {
+          indent.write(
+              'if (custom_value->type() == typeid(${customClass.name})) ');
+          indent.scoped('{', '}', () {
+            indent.writeln('stream->WriteByte(${customClass.enumeration});');
+            indent.writeln(
+                'WriteValue(flutter::EncodableValue(std::any_cast<${customClass.name}>(*custom_value).ToEncodableList()), stream);');
+            indent.writeln('return;');
+          });
+        }
+      });
+      indent.writeln('$_defaultCodecSerializer::WriteValue(value, stream);');
+    });
+    indent.writeln('');
+  }
+
+  void _writeCppSourceClassField(CppOptions generatorOptions, Root root,
+      Indent indent, Class klass, NamedType field) {
     final HostDatatype hostDatatype = getFieldHostDatatype(field, root.classes,
         root.enums, (TypeDeclaration x) => _baseCppTypeForBuiltinDartType(x));
     final String instanceVariableName = _makeInstanceVariableName(field);
@@ -333,7 +1038,7 @@
         '{ return $returnExpression; }');
     indent.writeln(makeSetter(hostDatatype));
     if (hostDatatype.isNullable) {
-      // Write the non-nullable variant; see _writeDataClassDeclaration.
+      // Write the non-nullable variant; see _writeCppHeaderDataClass.
       final HostDatatype nonNullType = _nonNullableType(hostDatatype);
       indent.writeln(makeSetter(nonNullType));
     }
@@ -341,344 +1046,46 @@
     indent.addln('');
   }
 
-  // Serialization.
-  indent
-      .write('flutter::EncodableList ${klass.name}::ToEncodableList() const ');
-  indent.scoped('{', '}', () {
-    indent.scoped('return flutter::EncodableList{', '};', () {
-      for (final NamedType field in getFieldsInSerializationOrder(klass)) {
-        final HostDatatype hostDatatype = getFieldHostDatatype(
-            field,
-            root.classes,
-            root.enums,
-            (TypeDeclaration x) => _baseCppTypeForBuiltinDartType(x));
+  String _wrapResponse(Indent indent, Root root, TypeDeclaration returnType,
+      {String prefix = ''}) {
+    final String nonErrorPath;
+    final String errorCondition;
+    final String errorGetter;
 
-        final String instanceVariable = _makeInstanceVariableName(field);
-
-        String encodableValue = '';
-        if (!hostDatatype.isBuiltin &&
-            rootClassNameSet.contains(field.type.baseName)) {
-          final String operator = field.type.isNullable ? '->' : '.';
-          encodableValue =
-              'flutter::EncodableValue($instanceVariable${operator}ToEncodableList())';
-        } else if (!hostDatatype.isBuiltin &&
-            rootEnumNameSet.contains(field.type.baseName)) {
-          final String nonNullValue =
-              field.type.isNullable ? '(*$instanceVariable)' : instanceVariable;
-          encodableValue = 'flutter::EncodableValue((int)$nonNullValue)';
-        } else {
-          final String operator = field.type.isNullable ? '*' : '';
-          encodableValue =
-              'flutter::EncodableValue($operator$instanceVariable)';
-        }
-
-        if (field.type.isNullable) {
-          encodableValue =
-              '$instanceVariable ? $encodableValue : flutter::EncodableValue()';
-        }
-
-        indent.writeln('$encodableValue,');
-      }
-    });
-  });
-  indent.addln('');
-
-  // Default constructor.
-  indent.writeln('${klass.name}::${klass.name}() {}');
-  indent.addln('');
-
-  // Deserialization.
-  indent.write(
-      '${klass.name}::${klass.name}(const flutter::EncodableList& list) ');
-  indent.scoped('{', '}', () {
-    enumerate(getFieldsInSerializationOrder(klass),
-        (int index, final NamedType field) {
-      final String instanceVariableName = _makeInstanceVariableName(field);
-      final String pointerFieldName =
-          '${_pointerPrefix}_${_makeVariableName(field)}';
-      final String encodableFieldName =
-          '${_encodablePrefix}_${_makeVariableName(field)}';
-      indent.writeln('auto& $encodableFieldName = list[$index];');
-      if (rootEnumNameSet.contains(field.type.baseName)) {
-        indent.writeln(
-            'if (const int32_t* $pointerFieldName = std::get_if<int32_t>(&$encodableFieldName))\t$instanceVariableName = (${field.type.baseName})*$pointerFieldName;');
-      } else {
-        final HostDatatype hostDatatype = getFieldHostDatatype(
-            field,
-            root.classes,
-            root.enums,
-            (TypeDeclaration x) => _baseCppTypeForBuiltinDartType(x));
-        if (field.type.baseName == 'int') {
-          indent.format('''
-if (const int32_t* $pointerFieldName = std::get_if<int32_t>(&$encodableFieldName))
-\t$instanceVariableName = *$pointerFieldName;
-else if (const int64_t* ${pointerFieldName}_64 = std::get_if<int64_t>(&$encodableFieldName))
-\t$instanceVariableName = *${pointerFieldName}_64;''');
-        } else if (!hostDatatype.isBuiltin &&
-            root.classes
-                .map((Class x) => x.name)
-                .contains(field.type.baseName)) {
-          indent.write(
-              'if (const flutter::EncodableList* $pointerFieldName = std::get_if<flutter::EncodableList>(&$encodableFieldName)) ');
-          indent.scoped('{', '}', () {
-            indent.writeln(
-                '$instanceVariableName = ${hostDatatype.datatype}(*$pointerFieldName);');
-          });
-        } else {
-          indent.write(
-              'if (const ${hostDatatype.datatype}* $pointerFieldName = std::get_if<${hostDatatype.datatype}>(&$encodableFieldName)) ');
-          indent.scoped('{', '}', () {
-            indent.writeln('$instanceVariableName = *$pointerFieldName;');
-          });
-        }
-      }
-    });
-  });
-  indent.addln('');
-}
-
-void _writeHostApiHeader(Indent indent, Api api, Root root) {
-  assert(api.location == ApiLocation.host);
-
-  const List<String> generatedMessages = <String>[
-    ' Generated interface from Pigeon that represents a handler of messages from Flutter.'
-  ];
-  addDocumentationComments(indent, api.documentationComments, _docCommentSpec,
-      generatorComments: generatedMessages);
-  indent.write('class ${api.name} ');
-  indent.scoped('{', '};', () {
-    indent.scoped(' public:', '', () {
-      indent.writeln('${api.name}(const ${api.name}&) = delete;');
-      indent.writeln('${api.name}& operator=(const ${api.name}&) = delete;');
-      indent.writeln('virtual ~${api.name}() { };');
-      for (final Method method in api.methods) {
-        final HostDatatype returnType = getHostDatatype(
-            method.returnType,
-            root.classes,
-            root.enums,
-            (TypeDeclaration x) => _baseCppTypeForBuiltinDartType(x));
-        final String returnTypeName = _apiReturnType(returnType);
-
-        final List<String> argSignature = <String>[];
-        if (method.arguments.isNotEmpty) {
-          final Iterable<String> argTypes =
-              method.arguments.map((NamedType arg) {
-            final HostDatatype hostType = getFieldHostDatatype(
-                arg,
-                root.classes,
-                root.enums,
-                (TypeDeclaration x) => _baseCppTypeForBuiltinDartType(x));
-            return _hostApiArgumentType(hostType);
-          });
-          final Iterable<String> argNames =
-              method.arguments.map((NamedType e) => _makeVariableName(e));
-          argSignature.addAll(
-              map2(argTypes, argNames, (String argType, String argName) {
-            return '$argType $argName';
-          }));
-        }
-
-        addDocumentationComments(
-            indent, method.documentationComments, _docCommentSpec);
-
-        if (method.isAsynchronous) {
-          argSignature.add('std::function<void($returnTypeName reply)> result');
-          indent.writeln(
-              'virtual void ${_makeMethodName(method)}(${argSignature.join(', ')}) = 0;');
-        } else {
-          indent.writeln(
-              'virtual $returnTypeName ${_makeMethodName(method)}(${argSignature.join(', ')}) = 0;');
-        }
-      }
-      indent.addln('');
-      indent.writeln('$_commentPrefix The codec used by ${api.name}.');
-      indent.writeln('static const flutter::StandardMessageCodec& GetCodec();');
-      indent.writeln(
-          '$_commentPrefix Sets up an instance of `${api.name}` to handle messages through the `binary_messenger`.');
-      indent.writeln(
-          'static void SetUp(flutter::BinaryMessenger* binary_messenger, ${api.name}* api);');
-      indent.writeln(
-          'static flutter::EncodableValue WrapError(std::string_view error_message);');
-      indent.writeln(
-          'static flutter::EncodableValue WrapError(const FlutterError& error);');
-    });
-    indent.scoped(' protected:', '', () {
-      indent.writeln('${api.name}() = default;');
-    });
-  }, nestCount: 0);
-}
-
-void _writeHostApiSource(Indent indent, Api api, Root root) {
-  assert(api.location == ApiLocation.host);
-
-  final String codeSerializerName = getCodecClasses(api, root).isNotEmpty
-      ? _getCodecSerializerName(api)
-      : _defaultCodecSerializer;
-  indent.format('''
-/// The codec used by ${api.name}.
-const flutter::StandardMessageCodec& ${api.name}::GetCodec() {
-\treturn flutter::StandardMessageCodec::GetInstance(&$codeSerializerName::GetInstance());
-}
-''');
-  indent.writeln(
-      '$_commentPrefix Sets up an instance of `${api.name}` to handle messages through the `binary_messenger`.');
-  indent.write(
-      'void ${api.name}::SetUp(flutter::BinaryMessenger* binary_messenger, ${api.name}* api) ');
-  indent.scoped('{', '}', () {
-    for (final Method method in api.methods) {
-      final String channelName = makeChannelName(api, method);
-      indent.write('');
-      indent.scoped('{', '}', () {
-        indent.writeln(
-            'auto channel = std::make_unique<flutter::BasicMessageChannel<flutter::EncodableValue>>(');
-        indent.inc();
-        indent.inc();
-        indent.writeln('binary_messenger, "$channelName", &GetCodec());');
-        indent.dec();
-        indent.dec();
-        indent.write('if (api != nullptr) ');
-        indent.scoped('{', '} else {', () {
-          indent.write(
-              'channel->SetMessageHandler([api](const flutter::EncodableValue& message, const flutter::MessageReply<flutter::EncodableValue>& reply) ');
-          indent.scoped('{', '});', () {
-            indent.write('try ');
-            indent.scoped('{', '}', () {
-              final List<String> methodArgument = <String>[];
-              if (method.arguments.isNotEmpty) {
-                indent.writeln(
-                    'const auto& args = std::get<flutter::EncodableList>(message);');
-
-                // Writes the code to declare and populate a variable called
-                // [argName] to use as a parameter to an API method call from
-                // an existing EncodablValue variable called [encodableArgName]
-                // which corresponds to [arg] in the API definition.
-                void extractEncodedArgument(
-                    String argName,
-                    String encodableArgName,
-                    NamedType arg,
-                    HostDatatype hostType) {
-                  if (arg.type.isNullable) {
-                    // Nullable arguments are always pointers, with nullptr
-                    // corresponding to null.
-                    if (hostType.datatype == 'int64_t') {
-                      // The EncodableValue will either be an int32_t or an
-                      // int64_t depending on the value, but the generated API
-                      // requires an int64_t so that it can handle any case.
-                      // Create a local variable for the 64-bit value...
-                      final String valueVarName = '${argName}_value';
-                      indent.writeln(
-                          'const int64_t $valueVarName = $encodableArgName.IsNull() ? 0 : $encodableArgName.LongValue();');
-                      // ... then declare the arg as a reference to that local.
-                      indent.writeln(
-                          'const auto* $argName = $encodableArgName.IsNull() ? nullptr : &$valueVarName;');
-                    } else if (hostType.datatype == 'flutter::EncodableValue') {
-                      // Generic objects just pass the EncodableValue through
-                      // directly.
-                      indent.writeln(
-                          'const auto* $argName = &$encodableArgName;');
-                    } else if (hostType.isBuiltin) {
-                      indent.writeln(
-                          'const auto* $argName = std::get_if<${hostType.datatype}>(&$encodableArgName);');
-                    } else {
-                      indent.writeln(
-                          'const auto* $argName = &(std::any_cast<const ${hostType.datatype}&>(std::get<flutter::CustomEncodableValue>($encodableArgName)));');
-                    }
-                  } else {
-                    // Non-nullable arguments are either passed by value or
-                    // reference, but the extraction doesn't need to distinguish
-                    // since those are the same at the call site.
-                    if (hostType.datatype == 'int64_t') {
-                      // The EncodableValue will either be an int32_t or an
-                      // int64_t depending on the value, but the generated API
-                      // requires an int64_t so that it can handle any case.
-                      indent.writeln(
-                          'const int64_t $argName = $encodableArgName.LongValue();');
-                    } else if (hostType.datatype == 'flutter::EncodableValue') {
-                      // Generic objects just pass the EncodableValue through
-                      // directly. This creates an alias just to avoid having to
-                      // special-case the argName/encodableArgName distinction
-                      // at a higher level.
-                      indent
-                          .writeln('const auto& $argName = $encodableArgName;');
-                    } else if (hostType.isBuiltin) {
-                      indent.writeln(
-                          'const auto& $argName = std::get<${hostType.datatype}>($encodableArgName);');
-                    } else {
-                      indent.writeln(
-                          'const auto& $argName = std::any_cast<const ${hostType.datatype}&>(std::get<flutter::CustomEncodableValue>($encodableArgName));');
-                    }
-                  }
-                }
-
-                enumerate(method.arguments, (int index, NamedType arg) {
-                  final HostDatatype hostType = getHostDatatype(
-                      arg.type,
-                      root.classes,
-                      root.enums,
-                      (TypeDeclaration x) => _baseCppTypeForBuiltinDartType(x));
-                  final String argName = _getSafeArgumentName(index, arg);
-
-                  final String encodableArgName =
-                      '${_encodablePrefix}_$argName';
-                  indent.writeln(
-                      'const auto& $encodableArgName = args.at($index);');
-                  if (!arg.type.isNullable) {
-                    indent.write('if ($encodableArgName.IsNull()) ');
-                    indent.scoped('{', '}', () {
-                      indent.writeln(
-                          'reply(WrapError("$argName unexpectedly null."));');
-                      indent.writeln('return;');
-                    });
-                  }
-                  extractEncodedArgument(
-                      argName, encodableArgName, arg, hostType);
-                  methodArgument.add(argName);
-                });
-              }
-
-              String wrapResponse(TypeDeclaration returnType,
-                  {String prefix = ''}) {
-                final String nonErrorPath;
-                final String errorCondition;
-                final String errorGetter;
-                const String nullValue = 'flutter::EncodableValue()';
-
-                if (returnType.isVoid) {
-                  nonErrorPath = '${prefix}wrapped.push_back($nullValue);';
-                  errorCondition = 'output.has_value()';
-                  errorGetter = 'value';
-                } else {
-                  final HostDatatype hostType = getHostDatatype(
-                      returnType,
-                      root.classes,
-                      root.enums,
-                      (TypeDeclaration x) => _baseCppTypeForBuiltinDartType(x));
-                  const String extractedValue = 'std::move(output).TakeValue()';
-                  final String wrapperType = hostType.isBuiltin
-                      ? 'flutter::EncodableValue'
-                      : 'flutter::CustomEncodableValue';
-                  if (returnType.isNullable) {
-                    // The value is a std::optional, so needs an extra layer of
-                    // handling.
-                    nonErrorPath = '''
+    const String nullValue = 'flutter::EncodableValue()';
+    if (returnType.isVoid) {
+      nonErrorPath = '${prefix}wrapped.push_back($nullValue);';
+      errorCondition = 'output.has_value()';
+      errorGetter = 'value';
+    } else {
+      final HostDatatype hostType = getHostDatatype(returnType, root.classes,
+          root.enums, (TypeDeclaration x) => _baseCppTypeForBuiltinDartType(x));
+      const String extractedValue = 'std::move(output).TakeValue()';
+      final String wrapperType = hostType.isBuiltin
+          ? 'flutter::EncodableValue'
+          : 'flutter::CustomEncodableValue';
+      if (returnType.isNullable) {
+        // The value is a std::optional, so needs an extra layer of
+        // handling.
+        nonErrorPath = '''
 ${prefix}auto output_optional = $extractedValue;
 ${prefix}if (output_optional) {
 $prefix\twrapped.push_back($wrapperType(std::move(output_optional).value()));
 $prefix} else {
 $prefix\twrapped.push_back($nullValue);
 $prefix}''';
-                  } else {
-                    nonErrorPath =
-                        '${prefix}wrapped.push_back($wrapperType($extractedValue));';
-                  }
-                  errorCondition = 'output.has_error()';
-                  errorGetter = 'error';
-                }
-                // Ideally this code would use an initializer list to create
-                // an EncodableList inline, which would be less code. However,
-                // that would always copy the element, so the slightly more
-                // verbose create-and-push approach is used instead.
-                return '''
+      } else {
+        nonErrorPath =
+            '${prefix}wrapped.push_back($wrapperType($extractedValue));';
+      }
+      errorCondition = 'output.has_error()';
+      errorGetter = 'error';
+    }
+    // Ideally this code would use an initializer list to create
+    // an EncodableList inline, which would be less code. However,
+    // that would always copy the element, so the slightly more
+    // verbose create-and-push approach is used instead.
+    return '''
 ${prefix}if ($errorCondition) {
 $prefix\treply(WrapError(output.$errorGetter()));
 $prefix\treturn;
@@ -686,52 +1093,22 @@
 ${prefix}flutter::EncodableList wrapped;
 $nonErrorPath
 ${prefix}reply(flutter::EncodableValue(std::move(wrapped)));''';
-              }
+  }
 
-              final HostDatatype returnType = getHostDatatype(
-                  method.returnType,
-                  root.classes,
-                  root.enums,
-                  (TypeDeclaration x) => _baseCppTypeForBuiltinDartType(x));
-              final String returnTypeName = _apiReturnType(returnType);
-              if (method.isAsynchronous) {
-                methodArgument.add(
-                  '[reply]($returnTypeName&& output) {${indent.newline}'
-                  '${wrapResponse(method.returnType, prefix: '\t')}${indent.newline}'
-                  '}',
-                );
-              }
-              final String call =
-                  'api->${_makeMethodName(method)}(${methodArgument.join(', ')})';
-              if (method.isAsynchronous) {
-                indent.format('$call;');
-              } else {
-                indent.writeln('$returnTypeName output = $call;');
-                indent.format(wrapResponse(method.returnType));
-              }
-            });
-            indent.write('catch (const std::exception& exception) ');
-            indent.scoped('{', '}', () {
-              // There is a potential here for `reply` to be called twice, which
-              // is a violation of the API contract, because there's no way of
-              // knowing whether or not the plugin code called `reply` before
-              // throwing. Since use of `@async` suggests that the reply is
-              // probably not sent within the scope of the stack, err on the
-              // side of potential double-call rather than no call (which is
-              // also an API violation) so that unexpected errors have a better
-              // chance of being caught and handled in a useful way.
-              indent.writeln('reply(WrapError(exception.what()));');
-            });
-          });
-        });
-        indent.scoped(null, '}', () {
-          indent.writeln('channel->SetMessageHandler(nullptr);');
-        });
-      });
+  @override
+  void writeCloseNamespace(
+      CppOptions generatorOptions, Root root, Indent indent) {
+    if (generatorOptions.namespace != null) {
+      indent.writeln('}  // namespace ${generatorOptions.namespace}');
     }
-  });
+  }
 }
 
+String _getCodecSerializerName(Api api) => '${api.name}CodecSerializer';
+
+const String _pointerPrefix = 'pointer';
+const String _encodablePrefix = 'encodable';
+
 String _getArgumentName(int count, NamedType argument) =>
     argument.name.isEmpty ? 'arg$count' : _makeVariableName(argument);
 
@@ -739,145 +1116,6 @@
 String _getSafeArgumentName(int count, NamedType argument) =>
     '${_getArgumentName(count, argument)}_arg';
 
-void _writeFlutterApiHeader(Indent indent, Api api) {
-  assert(api.location == ApiLocation.flutter);
-
-  const List<String> generatedMessages = <String>[
-    ' Generated class from Pigeon that represents Flutter messages that can be called from C++.'
-  ];
-  addDocumentationComments(indent, api.documentationComments, _docCommentSpec,
-      generatorComments: generatedMessages);
-  indent.write('class ${api.name} ');
-  indent.scoped('{', '};', () {
-    indent.scoped(' private:', '', () {
-      indent.writeln('flutter::BinaryMessenger* binary_messenger_;');
-    });
-    indent.scoped(' public:', '', () {
-      indent.write('${api.name}(flutter::BinaryMessenger* binary_messenger);');
-      indent.writeln('');
-      indent.writeln('static const flutter::StandardMessageCodec& GetCodec();');
-      for (final Method func in api.methods) {
-        final String returnType = func.returnType.isVoid
-            ? 'void'
-            : _nullSafeCppTypeForDartType(func.returnType);
-        final String callback = 'std::function<void($returnType)>&& callback';
-        addDocumentationComments(
-            indent, func.documentationComments, _docCommentSpec);
-        if (func.arguments.isEmpty) {
-          indent.writeln('void ${func.name}($callback);');
-        } else {
-          final Iterable<String> argTypes = func.arguments
-              .map((NamedType e) => _nullSafeCppTypeForDartType(e.type));
-          final Iterable<String> argNames =
-              indexMap(func.arguments, _getSafeArgumentName);
-          final String argsSignature =
-              map2(argTypes, argNames, (String x, String y) => '$x $y')
-                  .join(', ');
-          indent.writeln('void ${func.name}($argsSignature, $callback);');
-        }
-      }
-    });
-  }, nestCount: 0);
-}
-
-void _writeFlutterApiSource(Indent indent, Api api, Root root) {
-  assert(api.location == ApiLocation.flutter);
-  indent.writeln(
-      '$_commentPrefix Generated class from Pigeon that represents Flutter messages that can be called from C++.');
-  indent.write(
-      '${api.name}::${api.name}(flutter::BinaryMessenger* binary_messenger) ');
-  indent.scoped('{', '}', () {
-    indent.writeln('this->binary_messenger_ = binary_messenger;');
-  });
-  indent.writeln('');
-  final String codeSerializerName = getCodecClasses(api, root).isNotEmpty
-      ? _getCodecSerializerName(api)
-      : _defaultCodecSerializer;
-  indent.format('''
-const flutter::StandardMessageCodec& ${api.name}::GetCodec() {
-\treturn flutter::StandardMessageCodec::GetInstance(&$codeSerializerName::GetInstance());
-}
-''');
-  for (final Method func in api.methods) {
-    final String channelName = makeChannelName(api, func);
-    final String returnType = func.returnType.isVoid
-        ? 'void'
-        : _nullSafeCppTypeForDartType(func.returnType);
-    String sendArgument;
-    final String callback = 'std::function<void($returnType)>&& callback';
-    if (func.arguments.isEmpty) {
-      indent.write('void ${api.name}::${func.name}($callback) ');
-      sendArgument = 'flutter::EncodableValue()';
-    } else {
-      final Iterable<String> argTypes = func.arguments
-          .map((NamedType e) => _nullSafeCppTypeForDartType(e.type));
-      final Iterable<String> argNames =
-          indexMap(func.arguments, _getSafeArgumentName);
-      sendArgument =
-          'flutter::EncodableList { ${argNames.map((String arg) => 'flutter::CustomEncodableValue($arg)').join(', ')} }';
-      final String argsSignature =
-          map2(argTypes, argNames, (String x, String y) => '$x $y').join(', ');
-      indent
-          .write('void ${api.name}::${func.name}($argsSignature, $callback) ');
-    }
-    indent.scoped('{', '}', () {
-      const String channel = 'channel';
-      indent.writeln(
-          'auto channel = std::make_unique<flutter::BasicMessageChannel<flutter::EncodableValue>>(');
-      indent.inc();
-      indent.inc();
-      indent.writeln('binary_messenger_, "$channelName", &GetCodec());');
-      indent.dec();
-      indent.dec();
-      indent.write(
-          '$channel->Send($sendArgument, [callback](const uint8_t* reply, size_t reply_size) ');
-      indent.scoped('{', '});', () {
-        if (func.returnType.isVoid) {
-          indent.writeln('callback();');
-        } else {
-          indent.writeln(
-              'std::unique_ptr<flutter::EncodableValue> decoded_reply = GetCodec().DecodeMessage(reply, reply_size);');
-          indent.writeln(
-              'flutter::EncodableValue args = *(flutter::EncodableValue*)(decoded_reply.release());');
-          const String output = 'output';
-
-          final bool isBuiltin =
-              _baseCppTypeForBuiltinDartType(func.returnType) != null;
-          final String returnTypeName =
-              _baseCppTypeForDartType(func.returnType);
-          if (func.returnType.isNullable) {
-            indent.writeln('$returnType $output{};');
-          } else {
-            indent.writeln('$returnTypeName $output{};');
-          }
-          const String pointerVariable = '${_pointerPrefix}_$output';
-          if (func.returnType.baseName == 'int') {
-            indent.format('''
-if (const int32_t* $pointerVariable = std::get_if<int32_t>(&args))
-\t$output = *$pointerVariable;
-else if (const int64_t* ${pointerVariable}_64 = std::get_if<int64_t>(&args))
-\t$output = *${pointerVariable}_64;''');
-          } else if (!isBuiltin) {
-            indent.write(
-                'if (const flutter::EncodableList* $pointerVariable = std::get_if<flutter::EncodableList>(&args)) ');
-            indent.scoped('{', '}', () {
-              indent.writeln('$output = $returnTypeName(*$pointerVariable);');
-            });
-          } else {
-            indent.write(
-                'if (const $returnTypeName* $pointerVariable = std::get_if<$returnTypeName>(&args)) ');
-            indent.scoped('{', '}', () {
-              indent.writeln('$output = *$pointerVariable;');
-            });
-          }
-
-          indent.writeln('callback($output);');
-        }
-      });
-    });
-  }
-}
-
 /// Returns a non-nullable variant of [type].
 HostDatatype _nonNullableType(HostDatatype type) {
   return HostDatatype(
@@ -1035,14 +1273,11 @@
   }
 }
 
-String _getGuardName(String? headerFileName, String? namespace) {
+String _getGuardName(String? headerFileName) {
   String guardName = 'PIGEON_';
   if (headerFileName != null) {
     guardName += '${headerFileName.replaceAll('.', '_').toUpperCase()}_';
   }
-  if (namespace != null) {
-    guardName += '${namespace.toUpperCase()}_';
-  }
   return '${guardName}H_';
 }
 
@@ -1053,169 +1288,6 @@
   }
 }
 
-/// Generates the ".h" file for the AST represented by [root] to [sink] with the
-/// provided [options] and [headerFileName].
-void generateCppHeader(CppOptions options, Root root, StringSink sink) {
-  final String? headerFileName = options.headerOutPath;
-  final Indent indent = Indent(sink);
-  if (options.copyrightHeader != null) {
-    addLines(indent, options.copyrightHeader!, linePrefix: '// ');
-  }
-  indent.writeln('$_commentPrefix $generatedCodeWarning');
-  indent.writeln('$_commentPrefix $seeAlsoWarning');
-  indent.addln('');
-  final String guardName = _getGuardName(headerFileName, options.namespace);
-  indent.writeln('#ifndef $guardName');
-  indent.writeln('#define $guardName');
-
-  _writeSystemHeaderIncludeBlock(indent, <String>[
-    'flutter/basic_message_channel.h',
-    'flutter/binary_messenger.h',
-    'flutter/encodable_value.h',
-    'flutter/standard_message_codec.h',
-  ]);
-  indent.addln('');
-  _writeSystemHeaderIncludeBlock(indent, <String>[
-    'map',
-    'string',
-    'optional',
-  ]);
-  indent.addln('');
-
-  if (options.namespace != null) {
-    indent.writeln('namespace ${options.namespace} {');
-  }
-
-  // When generating for a Pigeon unit test, add a test fixture friend class to
-  // allow unit testing private methods, since testing serialization via public
-  // methods is essentially an end-to-end test.
-  String? testFixtureClass;
-  if (options.namespace?.endsWith('_pigeontest') ?? false) {
-    testFixtureClass =
-        '${_pascalCaseFromSnakeCase(options.namespace!.replaceAll('_pigeontest', ''))}Test';
-    indent.writeln('class $testFixtureClass;');
-  }
-
-  indent.addln('');
-  indent.writeln('$_commentPrefix Generated class from Pigeon.');
-
-  for (final Enum anEnum in root.enums) {
-    indent.writeln('');
-    addDocumentationComments(
-        indent, anEnum.documentationComments, _docCommentSpec);
-    indent.write('enum class ${anEnum.name} ');
-    indent.scoped('{', '};', () {
-      enumerate(anEnum.members, (int index, final EnumMember member) {
-        addDocumentationComments(
-            indent, member.documentationComments, _docCommentSpec);
-        indent.writeln(
-            '${member.name} = $index${index == anEnum.members.length - 1 ? '' : ','}');
-      });
-    });
-  }
-
-  indent.addln('');
-
-  _writeErrorOr(indent, friends: root.apis.map((Api api) => api.name));
-
-  for (final Class klass in root.classes) {
-    _writeDataClassDeclaration(indent, klass, root,
-        // Add a hook for unit testing data classes when using the namespace
-        // used by pigeon tests.
-        testFriend: testFixtureClass);
-  }
-
-  for (final Api api in root.apis) {
-    if (getCodecClasses(api, root).isNotEmpty) {
-      _writeCodecHeader(indent, api, root);
-    }
-    indent.addln('');
-    if (api.location == ApiLocation.host) {
-      _writeHostApiHeader(indent, api, root);
-    } else if (api.location == ApiLocation.flutter) {
-      _writeFlutterApiHeader(indent, api);
-    }
-  }
-
-  if (options.namespace != null) {
-    indent.writeln('}  // namespace ${options.namespace}');
-  }
-
-  indent.writeln('#endif  // $guardName');
-}
-
-/// Generates the ".cpp" file for the AST represented by [root] to [sink] with the
-/// provided [options].
-void generateCppSource(CppOptions options, Root root, StringSink sink) {
-  final Indent indent = Indent(sink);
-  if (options.copyrightHeader != null) {
-    addLines(indent, options.copyrightHeader!, linePrefix: '// ');
-  }
-  indent.writeln('$_commentPrefix $generatedCodeWarning');
-  indent.writeln('$_commentPrefix $seeAlsoWarning');
-  indent.addln('');
-  indent.addln('#undef _HAS_EXCEPTIONS');
-  indent.addln('');
-
-  indent.writeln('#include "${options.headerIncludePath}"');
-  indent.addln('');
-  _writeSystemHeaderIncludeBlock(indent, <String>[
-    'flutter/basic_message_channel.h',
-    'flutter/binary_messenger.h',
-    'flutter/encodable_value.h',
-    'flutter/standard_message_codec.h',
-  ]);
-  indent.addln('');
-  _writeSystemHeaderIncludeBlock(indent, <String>[
-    'map',
-    'string',
-    'optional',
-  ]);
-  indent.addln('');
-
-  if (options.namespace != null) {
-    indent.writeln('namespace ${options.namespace} {');
-  }
-
-  for (final Class klass in root.classes) {
-    _writeDataClassImplementation(indent, klass, root);
-  }
-
-  for (final Api api in root.apis) {
-    if (getCodecClasses(api, root).isNotEmpty) {
-      _writeCodecSource(indent, api, root);
-      indent.addln('');
-    }
-    if (api.location == ApiLocation.host) {
-      _writeHostApiSource(indent, api, root);
-
-      indent.addln('');
-      indent.format('''
-flutter::EncodableValue ${api.name}::WrapError(std::string_view error_message) {
-\treturn flutter::EncodableValue(flutter::EncodableList{
-\t\tflutter::EncodableValue(std::string(error_message)),
-\t\tflutter::EncodableValue("Error"),
-\t\tflutter::EncodableValue()
-\t});
-}
-flutter::EncodableValue ${api.name}::WrapError(const FlutterError& error) {
-\treturn flutter::EncodableValue(flutter::EncodableList{
-\t\tflutter::EncodableValue(error.message()),
-\t\tflutter::EncodableValue(error.code()),
-\t\terror.details()
-\t});
-}''');
-      indent.addln('');
-    } else if (api.location == ApiLocation.flutter) {
-      _writeFlutterApiSource(indent, api, root);
-    }
-  }
-
-  if (options.namespace != null) {
-    indent.writeln('}  // namespace ${options.namespace}');
-  }
-}
-
 /// Validates an AST to make sure the cpp generator supports everything.
 List<Error> validateCpp(CppOptions options, Root root) {
   final List<Error> result = <Error>[];
diff --git a/packages/pigeon/lib/dart_generator.dart b/packages/pigeon/lib/dart_generator.dart
index e6f6850..3b62aff 100644
--- a/packages/pigeon/lib/dart_generator.dart
+++ b/packages/pigeon/lib/dart_generator.dart
@@ -71,29 +71,557 @@
 }
 
 /// Class that manages all Dart code generation.
-class DartGenerator extends Generator<DartOptions> {
+class DartGenerator extends StructuredGenerator<DartOptions> {
   /// Instantiates a Dart Generator.
-  DartGenerator();
+  const DartGenerator();
 
-  /// Generates Dart files with specified [DartOptions]
   @override
-  void generate(DartOptions languageOptions, Root root, StringSink sink,
-      {FileType fileType = FileType.na}) {
-    assert(fileType == FileType.na);
-    generateDart(languageOptions, root, sink);
+  void writeFilePrologue(
+      DartOptions generatorOptions, Root root, Indent indent) {
+    if (generatorOptions.copyrightHeader != null) {
+      addLines(indent, generatorOptions.copyrightHeader!, linePrefix: '// ');
+    }
+    indent.writeln('// $generatedCodeWarning');
+    indent.writeln('// $seeAlsoWarning');
+    indent.writeln(
+      '// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import',
+    );
+    indent.addln('');
   }
 
-  /// Generates Dart files for testing with specified [DartOptions]
-  void generateTest(DartOptions languageOptions, Root root, StringSink sink) {
-    final String sourceOutPath = languageOptions.sourceOutPath ?? '';
-    final String testOutPath = languageOptions.testOutPath ?? '';
-    generateTestDart(
-      languageOptions,
-      root,
-      sink,
-      sourceOutPath: sourceOutPath,
-      testOutPath: testOutPath,
+  @override
+  void writeFileImports(
+      DartOptions generatorOptions, Root root, Indent indent) {
+    indent.writeln("import 'dart:async';");
+    indent.writeln(
+      "import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;",
     );
+    indent.addln('');
+    indent.writeln(
+        "import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;");
+    indent.writeln("import 'package:flutter/services.dart';");
+  }
+
+  @override
+  void writeEnum(
+      DartOptions generatorOptions, Root root, Indent indent, Enum anEnum) {
+    indent.writeln('');
+    addDocumentationComments(
+        indent, anEnum.documentationComments, _docCommentSpec);
+    indent.write('enum ${anEnum.name} ');
+    indent.scoped('{', '}', () {
+      for (final EnumMember member in anEnum.members) {
+        addDocumentationComments(
+            indent, member.documentationComments, _docCommentSpec);
+        indent.writeln('${member.name},');
+      }
+    });
+  }
+
+  @override
+  void writeDataClass(
+      DartOptions generatorOptions, Root root, Indent indent, Class klass) {
+    final Set<String> customClassNames =
+        root.classes.map((Class x) => x.name).toSet();
+    final Set<String> customEnumNames =
+        root.enums.map((Enum x) => x.name).toSet();
+
+    indent.writeln('');
+    addDocumentationComments(
+        indent, klass.documentationComments, _docCommentSpec);
+
+    indent.write('class ${klass.name} ');
+    indent.scoped('{', '}', () {
+      _writeConstructor(indent, klass);
+      indent.addln('');
+      for (final NamedType field in getFieldsInSerializationOrder(klass)) {
+        addDocumentationComments(
+            indent, field.documentationComments, _docCommentSpec);
+
+        final String datatype = _addGenericTypesNullable(field.type);
+        indent.writeln('$datatype ${field.name};');
+        indent.writeln('');
+      }
+      writeClassEncode(generatorOptions, root, indent, klass, customClassNames,
+          customEnumNames);
+      indent.writeln('');
+      writeClassDecode(generatorOptions, root, indent, klass, customClassNames,
+          customEnumNames);
+    });
+  }
+
+  void _writeConstructor(Indent indent, Class klass) {
+    indent.write(klass.name);
+    indent.scoped('({', '});', () {
+      for (final NamedType field in getFieldsInSerializationOrder(klass)) {
+        final String required = field.type.isNullable ? '' : 'required ';
+        indent.writeln('${required}this.${field.name},');
+      }
+    });
+  }
+
+  @override
+  void writeClassEncode(
+    DartOptions generatorOptions,
+    Root root,
+    Indent indent,
+    Class klass,
+    Set<String> customClassNames,
+    Set<String> customEnumNames,
+  ) {
+    indent.write('Object encode() ');
+    indent.scoped('{', '}', () {
+      indent.write(
+        'return <Object?>',
+      );
+      indent.scoped('[', '];', () {
+        for (final NamedType field in getFieldsInSerializationOrder(klass)) {
+          final String conditional = field.type.isNullable ? '?' : '';
+          if (customClassNames.contains(field.type.baseName)) {
+            indent.writeln(
+              '${field.name}$conditional.encode(),',
+            );
+          } else if (customEnumNames.contains(field.type.baseName)) {
+            indent.writeln(
+              '${field.name}$conditional.index,',
+            );
+          } else {
+            indent.writeln('${field.name},');
+          }
+        }
+      });
+    });
+  }
+
+  @override
+  void writeClassDecode(
+    DartOptions generatorOptions,
+    Root root,
+    Indent indent,
+    Class klass,
+    Set<String> customClassNames,
+    Set<String> customEnumNames,
+  ) {
+    void writeValueDecode(NamedType field, int index) {
+      final String resultAt = 'result[$index]';
+      if (customClassNames.contains(field.type.baseName)) {
+        final String nonNullValue =
+            '${field.type.baseName}.decode($resultAt! as List<Object?>)';
+        indent.format(
+            field.type.isNullable
+                ? '''
+$resultAt != null
+\t\t? $nonNullValue
+\t\t: null'''
+                : nonNullValue,
+            leadingSpace: false,
+            trailingNewline: false);
+      } else if (customEnumNames.contains(field.type.baseName)) {
+        final String nonNullValue =
+            '${field.type.baseName}.values[$resultAt! as int]';
+        indent.format(
+            field.type.isNullable
+                ? '''
+$resultAt != null
+\t\t? $nonNullValue
+\t\t: null'''
+                : nonNullValue,
+            leadingSpace: false,
+            trailingNewline: false);
+      } else if (field.type.typeArguments.isNotEmpty) {
+        final String genericType = _makeGenericTypeArguments(field.type);
+        final String castCall = _makeGenericCastCall(field.type);
+        final String castCallPrefix = field.type.isNullable ? '?' : '!';
+        indent.add(
+          '($resultAt as $genericType?)$castCallPrefix$castCall',
+        );
+      } else {
+        final String genericdType = _addGenericTypesNullable(field.type);
+        if (field.type.isNullable) {
+          indent.add(
+            '$resultAt as $genericdType',
+          );
+        } else {
+          indent.add(
+            '$resultAt! as $genericdType',
+          );
+        }
+      }
+    }
+
+    indent.write(
+      'static ${klass.name} decode(Object result) ',
+    );
+    indent.scoped('{', '}', () {
+      indent.writeln('result as List<Object?>;');
+      indent.write('return ${klass.name}');
+      indent.scoped('(', ');', () {
+        enumerate(getFieldsInSerializationOrder(klass),
+            (int index, final NamedType field) {
+          indent.write('${field.name}: ');
+          writeValueDecode(field, index);
+          indent.addln(',');
+        });
+      });
+    });
+  }
+
+  /// Writes the code for host [Api], [api].
+  /// Example:
+  /// class FooCodec extends StandardMessageCodec {...}
+  ///
+  /// abstract class Foo {
+  ///   static const MessageCodec<Object?> codec = FooCodec();
+  ///   int add(int x, int y);
+  ///   static void setup(Foo api, {BinaryMessenger? binaryMessenger}) {...}
+  /// }
+  @override
+  void writeFlutterApi(
+    DartOptions generatorOptions,
+    Root root,
+    Indent indent,
+    Api api, {
+    String Function(Method)? channelNameFunc,
+    bool isMockHandler = false,
+  }) {
+    assert(api.location == ApiLocation.flutter);
+    final List<String> customEnumNames =
+        root.enums.map((Enum x) => x.name).toList();
+    String codecName = _standardMessageCodec;
+    if (getCodecClasses(api, root).isNotEmpty) {
+      codecName = _getCodecName(api);
+      _writeCodec(indent, codecName, api, root);
+    }
+    indent.addln('');
+    addDocumentationComments(
+        indent, api.documentationComments, _docCommentSpec);
+
+    indent.write('abstract class ${api.name} ');
+    indent.scoped('{', '}', () {
+      indent
+          .writeln('static const MessageCodec<Object?> codec = $codecName();');
+      indent.addln('');
+      for (final Method func in api.methods) {
+        addDocumentationComments(
+            indent, func.documentationComments, _docCommentSpec);
+
+        final bool isAsync = func.isAsynchronous;
+        final String returnType = isAsync
+            ? 'Future<${_addGenericTypesNullable(func.returnType)}>'
+            : _addGenericTypesNullable(func.returnType);
+        final String argSignature = _getMethodArgumentsSignature(
+          func,
+          _getArgumentName,
+        );
+        indent.writeln('$returnType ${func.name}($argSignature);');
+        indent.writeln('');
+      }
+      indent.write(
+          'static void setup(${api.name}? api, {BinaryMessenger? binaryMessenger}) ');
+      indent.scoped('{', '}', () {
+        for (final Method func in api.methods) {
+          indent.write('');
+          indent.scoped('{', '}', () {
+            indent.writeln(
+              'final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(',
+            );
+            final String channelName = channelNameFunc == null
+                ? makeChannelName(api, func)
+                : channelNameFunc(func);
+            indent.nest(2, () {
+              indent.writeln("'$channelName', codec,");
+              indent.writeln(
+                'binaryMessenger: binaryMessenger);',
+              );
+            });
+            final String messageHandlerSetter =
+                isMockHandler ? 'setMockMessageHandler' : 'setMessageHandler';
+            indent.write('if (api == null) ');
+            indent.scoped('{', '}', () {
+              indent.writeln('channel.$messageHandlerSetter(null);');
+            }, addTrailingNewline: false);
+            indent.add(' else ');
+            indent.scoped('{', '}', () {
+              indent.write(
+                'channel.$messageHandlerSetter((Object? message) async ',
+              );
+              indent.scoped('{', '});', () {
+                final String returnType =
+                    _addGenericTypesNullable(func.returnType);
+                final bool isAsync = func.isAsynchronous;
+                final String emptyReturnStatement = isMockHandler
+                    ? 'return <Object?>[];'
+                    : func.returnType.isVoid
+                        ? 'return;'
+                        : 'return null;';
+                String call;
+                if (func.arguments.isEmpty) {
+                  indent.writeln('// ignore message');
+                  call = 'api.${func.name}()';
+                } else {
+                  indent.writeln('assert(message != null,');
+                  indent.writeln("'Argument for $channelName was null.');");
+                  const String argsArray = 'args';
+                  indent.writeln(
+                      'final List<Object?> $argsArray = (message as List<Object?>?)!;');
+                  String argNameFunc(int index, NamedType type) =>
+                      _getSafeArgumentName(index, type);
+                  enumerate(func.arguments, (int count, NamedType arg) {
+                    final String argType = _addGenericTypes(arg.type);
+                    final String argName = argNameFunc(count, arg);
+                    final String genericArgType =
+                        _makeGenericTypeArguments(arg.type);
+                    final String castCall = _makeGenericCastCall(arg.type);
+
+                    final String leftHandSide = 'final $argType? $argName';
+                    if (customEnumNames.contains(arg.type.baseName)) {
+                      indent.writeln(
+                          '$leftHandSide = $argsArray[$count] == null ? null : $argType.values[$argsArray[$count] as int];');
+                    } else {
+                      indent.writeln(
+                          '$leftHandSide = ($argsArray[$count] as $genericArgType?)${castCall.isEmpty ? '' : '?$castCall'};');
+                    }
+                    if (!arg.type.isNullable) {
+                      indent.writeln(
+                          "assert($argName != null, 'Argument for $channelName was null, expected non-null $argType.');");
+                    }
+                  });
+                  final Iterable<String> argNames =
+                      indexMap(func.arguments, (int index, NamedType field) {
+                    final String name = _getSafeArgumentName(index, field);
+                    return '$name${field.type.isNullable ? '' : '!'}';
+                  });
+                  call = 'api.${func.name}(${argNames.join(', ')})';
+                }
+                if (func.returnType.isVoid) {
+                  if (isAsync) {
+                    indent.writeln('await $call;');
+                  } else {
+                    indent.writeln('$call;');
+                  }
+                  indent.writeln(emptyReturnStatement);
+                } else {
+                  if (isAsync) {
+                    indent.writeln('final $returnType output = await $call;');
+                  } else {
+                    indent.writeln('final $returnType output = $call;');
+                  }
+                  const String returnExpression = 'output';
+                  final String returnStatement = isMockHandler
+                      ? 'return <Object?>[$returnExpression];'
+                      : 'return $returnExpression;';
+                  indent.writeln(returnStatement);
+                }
+              });
+            });
+          });
+        }
+      });
+    });
+  }
+
+  /// Writes the code for host [Api], [api].
+  /// Example:
+  /// class FooCodec extends StandardMessageCodec {...}
+  ///
+  /// class Foo {
+  ///   Foo(BinaryMessenger? binaryMessenger) {}
+  ///   static const MessageCodec<Object?> codec = FooCodec();
+  ///   Future<int> add(int x, int y) async {...}
+  /// }
+  ///
+  /// Messages will be sent and received in a list.
+  ///
+  /// If the message recieved was succesful,
+  /// the result will be contained at the 0'th index.
+  ///
+  /// If the message was a failure, the list will contain 3 items:
+  /// a code, a message, and details in that order.
+  @override
+  void writeHostApi(
+      DartOptions generatorOptions, Root root, Indent indent, Api api) {
+    assert(api.location == ApiLocation.host);
+    String codecName = _standardMessageCodec;
+    if (getCodecClasses(api, root).isNotEmpty) {
+      codecName = _getCodecName(api);
+      _writeCodec(indent, codecName, api, root);
+    }
+    indent.addln('');
+    bool first = true;
+    addDocumentationComments(
+        indent, api.documentationComments, _docCommentSpec);
+    indent.write('class ${api.name} ');
+    indent.scoped('{', '}', () {
+      indent.format('''
+/// Constructor for [${api.name}].  The [binaryMessenger] named argument is
+/// available for dependency injection.  If it is left null, the default
+/// BinaryMessenger will be used which routes to the host platform.
+${api.name}({BinaryMessenger? binaryMessenger})
+\t\t: _binaryMessenger = binaryMessenger;
+final BinaryMessenger? _binaryMessenger;
+''');
+
+      indent
+          .writeln('static const MessageCodec<Object?> codec = $codecName();');
+      indent.addln('');
+      for (final Method func in api.methods) {
+        if (!first) {
+          indent.writeln('');
+        } else {
+          first = false;
+        }
+        addDocumentationComments(
+            indent, func.documentationComments, _docCommentSpec);
+        String argSignature = '';
+        String sendArgument = 'null';
+        if (func.arguments.isNotEmpty) {
+          String argNameFunc(int index, NamedType type) =>
+              _getSafeArgumentName(index, type);
+          final Iterable<String> argExpressions =
+              indexMap(func.arguments, (int index, NamedType type) {
+            final String name = argNameFunc(index, type);
+            if (root.enums
+                .map((Enum e) => e.name)
+                .contains(type.type.baseName)) {
+              return '$name${type.type.isNullable ? '?' : ''}.index';
+            } else {
+              return name;
+            }
+          });
+          sendArgument = '<Object?>[${argExpressions.join(', ')}]';
+          argSignature = _getMethodArgumentsSignature(func, argNameFunc);
+        }
+        indent.write(
+          'Future<${_addGenericTypesNullable(func.returnType)}> ${func.name}($argSignature) async ',
+        );
+        indent.scoped('{', '}', () {
+          final String channelName = makeChannelName(api, func);
+          indent.writeln(
+              'final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(');
+          indent.nest(2, () {
+            indent.writeln("'$channelName', codec,");
+            indent.writeln('binaryMessenger: _binaryMessenger);');
+          });
+          final String returnType = _makeGenericTypeArguments(func.returnType);
+          final String genericCastCall = _makeGenericCastCall(func.returnType);
+          const String accessor = 'replyList[0]';
+          // Avoid warnings from pointlessly casting to `Object?`.
+          final String nullablyTypedAccessor =
+              returnType == 'Object' ? accessor : '($accessor as $returnType?)';
+          final String nullHandler = func.returnType.isNullable
+              ? (genericCastCall.isEmpty ? '' : '?')
+              : '!';
+          final String returnStatement = func.returnType.isVoid
+              ? 'return;'
+              : 'return $nullablyTypedAccessor$nullHandler$genericCastCall;';
+          indent.format('''
+final List<Object?>? replyList =
+\t\tawait channel.send($sendArgument) as List<Object?>?;
+if (replyList == null) {
+\tthrow PlatformException(
+\t\tcode: 'channel-error',
+\t\tmessage: 'Unable to establish connection on channel.',
+\t);
+} else if (replyList.length > 1) {
+\tthrow PlatformException(
+\t\tcode: replyList[0]! as String,
+\t\tmessage: replyList[1] as String?,
+\t\tdetails: replyList[2],
+\t);''');
+          // On iOS we can return nil from functions to accommodate error
+          // handling.  Returning a nil value and not returning an error is an
+          // exception.
+          if (!func.returnType.isNullable && !func.returnType.isVoid) {
+            indent.format('''
+} else if (replyList[0] == null) {
+\tthrow PlatformException(
+\t\tcode: 'null-error',
+\t\tmessage: 'Host platform returned null value for non-null return value.',
+\t);''');
+          }
+          indent.format('''
+} else {
+\t$returnStatement
+}''');
+        });
+      }
+    });
+  }
+
+  /// Generates Dart source code for test support libraries based on the given AST
+  /// represented by [root], outputting the code to [sink]. [sourceOutPath] is the
+  /// path of the generated dart code to be tested. [testOutPath] is where the
+  /// test code will be generated.
+  void generateTest(DartOptions generatorOptions, Root root, StringSink sink) {
+    final Indent indent = Indent(sink);
+    final String sourceOutPath = generatorOptions.sourceOutPath ?? '';
+    final String testOutPath = generatorOptions.testOutPath ?? '';
+    _writeTestPrologue(generatorOptions, root, indent);
+    _writeTestImports(generatorOptions, root, indent);
+    final String relativeDartPath =
+        path.Context(style: path.Style.posix).relative(
+      _posixify(sourceOutPath),
+      from: _posixify(path.dirname(testOutPath)),
+    );
+    late final String? packageName = _deducePackageName(sourceOutPath);
+    if (!relativeDartPath.contains('/lib/') || packageName == null) {
+      // If we can't figure out the package name or the relative path doesn't
+      // include a 'lib' directory, try relative path import which only works in
+      // certain (older) versions of Dart.
+      // TODO(gaaclarke): We should add a command-line parameter to override this import.
+      indent.writeln(
+          "import '${_escapeForDartSingleQuotedString(relativeDartPath)}';");
+    } else {
+      final String path =
+          relativeDartPath.replaceFirst(RegExp(r'^.*/lib/'), '');
+      indent.writeln("import 'package:$packageName/$path';");
+    }
+    for (final Api api in root.apis) {
+      if (api.location == ApiLocation.host && api.dartHostTestHandler != null) {
+        final Api mockApi = Api(
+          name: api.dartHostTestHandler!,
+          methods: api.methods,
+          location: ApiLocation.flutter,
+          dartHostTestHandler: api.dartHostTestHandler,
+          documentationComments: api.documentationComments,
+        );
+        indent.writeln('');
+        writeFlutterApi(
+          generatorOptions,
+          root,
+          indent,
+          mockApi,
+          channelNameFunc: (Method func) => makeChannelName(api, func),
+          isMockHandler: true,
+        );
+      }
+    }
+  }
+
+  /// Writes file header to sink.
+  void _writeTestPrologue(DartOptions opt, Root root, Indent indent) {
+    if (opt.copyrightHeader != null) {
+      addLines(indent, opt.copyrightHeader!, linePrefix: '// ');
+    }
+    indent.writeln('// $generatedCodeWarning');
+    indent.writeln('// $seeAlsoWarning');
+    indent.writeln(
+      '// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, unnecessary_import',
+    );
+    indent.writeln('// ignore_for_file: avoid_relative_lib_imports');
+  }
+
+  /// Writes file imports to sink.
+  void _writeTestImports(DartOptions opt, Root root, Indent indent) {
+    indent.writeln("import 'dart:async';");
+    indent.writeln(
+      "import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;",
+    );
+    indent.writeln(
+        "import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;");
+    indent.writeln("import 'package:flutter/services.dart';");
+    indent.writeln("import 'package:flutter_test/flutter_test.dart';");
+    indent.writeln('');
   }
 }
 
@@ -195,279 +723,6 @@
         }).join(', ');
 }
 
-/// Writes the code for host [Api], [api].
-/// Example:
-/// class FooCodec extends StandardMessageCodec {...}
-///
-/// class Foo {
-///   Foo(BinaryMessenger? binaryMessenger) {}
-///   static const MessageCodec<Object?> codec = FooCodec();
-///   Future<int> add(int x, int y) async {...}
-/// }
-///
-/// Messages will be sent and received in a list.
-///
-/// If the message recieved was succesful,
-/// the result will be contained at the 0'th index.
-///
-/// If the message was a failure, the list will contain 3 items:
-/// a code, a message, and details in that order.
-void _writeHostApi(DartOptions opt, Indent indent, Api api, Root root) {
-  assert(api.location == ApiLocation.host);
-  String codecName = _standardMessageCodec;
-  if (getCodecClasses(api, root).isNotEmpty) {
-    codecName = _getCodecName(api);
-    _writeCodec(indent, codecName, api, root);
-  }
-  indent.addln('');
-  bool first = true;
-  addDocumentationComments(indent, api.documentationComments, _docCommentSpec);
-  indent.write('class ${api.name} ');
-  indent.scoped('{', '}', () {
-    indent.format('''
-/// Constructor for [${api.name}].  The [binaryMessenger] named argument is
-/// available for dependency injection.  If it is left null, the default
-/// BinaryMessenger will be used which routes to the host platform.
-${api.name}({BinaryMessenger? binaryMessenger})
-\t\t: _binaryMessenger = binaryMessenger;
-final BinaryMessenger? _binaryMessenger;
-''');
-
-    indent.writeln('static const MessageCodec<Object?> codec = $codecName();');
-    indent.addln('');
-    for (final Method func in api.methods) {
-      if (!first) {
-        indent.writeln('');
-      } else {
-        first = false;
-      }
-      addDocumentationComments(
-          indent, func.documentationComments, _docCommentSpec);
-      String argSignature = '';
-      String sendArgument = 'null';
-      if (func.arguments.isNotEmpty) {
-        String argNameFunc(int index, NamedType type) =>
-            _getSafeArgumentName(index, type);
-        final Iterable<String> argExpressions =
-            indexMap(func.arguments, (int index, NamedType type) {
-          final String name = argNameFunc(index, type);
-          if (root.enums.map((Enum e) => e.name).contains(type.type.baseName)) {
-            return '$name${type.type.isNullable ? '?' : ''}.index';
-          } else {
-            return name;
-          }
-        });
-        sendArgument = '<Object?>[${argExpressions.join(', ')}]';
-        argSignature = _getMethodArgumentsSignature(func, argNameFunc);
-      }
-      indent.write(
-        'Future<${_addGenericTypesNullable(func.returnType)}> ${func.name}($argSignature) async ',
-      );
-      indent.scoped('{', '}', () {
-        final String channelName = makeChannelName(api, func);
-        indent.writeln(
-            'final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(');
-        indent.nest(2, () {
-          indent.writeln("'$channelName', codec,");
-          indent.writeln('binaryMessenger: _binaryMessenger);');
-        });
-        final String returnType = _makeGenericTypeArguments(func.returnType);
-        final String genericCastCall = _makeGenericCastCall(func.returnType);
-        const String accessor = 'replyList[0]';
-        // Avoid warnings from pointlessly casting to `Object?`.
-        final String nullablyTypedAccessor =
-            returnType == 'Object' ? accessor : '($accessor as $returnType?)';
-        final String nullHandler = func.returnType.isNullable
-            ? (genericCastCall.isEmpty ? '' : '?')
-            : '!';
-        final String returnStatement = func.returnType.isVoid
-            ? 'return;'
-            : 'return $nullablyTypedAccessor$nullHandler$genericCastCall;';
-        indent.format('''
-final List<Object?>? replyList =
-\t\tawait channel.send($sendArgument) as List<Object?>?;
-if (replyList == null) {
-\tthrow PlatformException(
-\t\tcode: 'channel-error',
-\t\tmessage: 'Unable to establish connection on channel.',
-\t);
-} else if (replyList.length > 1) {
-\tthrow PlatformException(
-\t\tcode: replyList[0]! as String,
-\t\tmessage: replyList[1] as String?,
-\t\tdetails: replyList[2],
-\t);''');
-        // On iOS we can return nil from functions to accommodate error
-        // handling.  Returning a nil value and not returning an error is an
-        // exception.
-        if (!func.returnType.isNullable && !func.returnType.isVoid) {
-          indent.format('''
-} else if (replyList[0] == null) {
-\tthrow PlatformException(
-\t\tcode: 'null-error',
-\t\tmessage: 'Host platform returned null value for non-null return value.',
-\t);''');
-        }
-        indent.format('''
-} else {
-\t$returnStatement
-}''');
-      });
-    }
-  });
-}
-
-/// Writes the code for host [Api], [api].
-/// Example:
-/// class FooCodec extends StandardMessageCodec {...}
-///
-/// abstract class Foo {
-///   static const MessageCodec<Object?> codec = FooCodec();
-///   int add(int x, int y);
-///   static void setup(Foo api, {BinaryMessenger? binaryMessenger}) {...}
-/// }
-void _writeFlutterApi(
-  DartOptions opt,
-  Indent indent,
-  Api api,
-  Root root, {
-  String Function(Method)? channelNameFunc,
-  bool isMockHandler = false,
-}) {
-  assert(api.location == ApiLocation.flutter);
-  final List<String> customEnumNames =
-      root.enums.map((Enum x) => x.name).toList();
-  String codecName = _standardMessageCodec;
-  if (getCodecClasses(api, root).isNotEmpty) {
-    codecName = _getCodecName(api);
-    _writeCodec(indent, codecName, api, root);
-  }
-  indent.addln('');
-  addDocumentationComments(indent, api.documentationComments, _docCommentSpec);
-
-  indent.write('abstract class ${api.name} ');
-  indent.scoped('{', '}', () {
-    indent.writeln('static const MessageCodec<Object?> codec = $codecName();');
-    indent.addln('');
-    for (final Method func in api.methods) {
-      addDocumentationComments(
-          indent, func.documentationComments, _docCommentSpec);
-
-      final bool isAsync = func.isAsynchronous;
-      final String returnType = isAsync
-          ? 'Future<${_addGenericTypesNullable(func.returnType)}>'
-          : _addGenericTypesNullable(func.returnType);
-      final String argSignature = _getMethodArgumentsSignature(
-        func,
-        _getArgumentName,
-      );
-      indent.writeln('$returnType ${func.name}($argSignature);');
-      indent.writeln('');
-    }
-    indent.write(
-        'static void setup(${api.name}? api, {BinaryMessenger? binaryMessenger}) ');
-    indent.scoped('{', '}', () {
-      for (final Method func in api.methods) {
-        indent.write('');
-        indent.scoped('{', '}', () {
-          indent.writeln(
-            'final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(',
-          );
-          final String channelName = channelNameFunc == null
-              ? makeChannelName(api, func)
-              : channelNameFunc(func);
-          indent.nest(2, () {
-            indent.writeln("'$channelName', codec,");
-            indent.writeln(
-              'binaryMessenger: binaryMessenger);',
-            );
-          });
-          final String messageHandlerSetter =
-              isMockHandler ? 'setMockMessageHandler' : 'setMessageHandler';
-          indent.write('if (api == null) ');
-          indent.scoped('{', '}', () {
-            indent.writeln('channel.$messageHandlerSetter(null);');
-          }, addTrailingNewline: false);
-          indent.add(' else ');
-          indent.scoped('{', '}', () {
-            indent.write(
-              'channel.$messageHandlerSetter((Object? message) async ',
-            );
-            indent.scoped('{', '});', () {
-              final String returnType =
-                  _addGenericTypesNullable(func.returnType);
-              final bool isAsync = func.isAsynchronous;
-              final String emptyReturnStatement = isMockHandler
-                  ? 'return <Object?>[];'
-                  : func.returnType.isVoid
-                      ? 'return;'
-                      : 'return null;';
-              String call;
-              if (func.arguments.isEmpty) {
-                indent.writeln('// ignore message');
-                call = 'api.${func.name}()';
-              } else {
-                indent.writeln('assert(message != null,');
-                indent.writeln("'Argument for $channelName was null.');");
-                const String argsArray = 'args';
-                indent.writeln(
-                    'final List<Object?> $argsArray = (message as List<Object?>?)!;');
-                String argNameFunc(int index, NamedType type) =>
-                    _getSafeArgumentName(index, type);
-                enumerate(func.arguments, (int count, NamedType arg) {
-                  final String argType = _addGenericTypes(arg.type);
-                  final String argName = argNameFunc(count, arg);
-                  final String genericArgType =
-                      _makeGenericTypeArguments(arg.type);
-                  final String castCall = _makeGenericCastCall(arg.type);
-
-                  final String leftHandSide = 'final $argType? $argName';
-                  if (customEnumNames.contains(arg.type.baseName)) {
-                    indent.writeln(
-                        '$leftHandSide = $argsArray[$count] == null ? null : $argType.values[$argsArray[$count] as int];');
-                  } else {
-                    indent.writeln(
-                        '$leftHandSide = ($argsArray[$count] as $genericArgType?)${castCall.isEmpty ? '' : '?$castCall'};');
-                  }
-                  if (!arg.type.isNullable) {
-                    indent.writeln(
-                        "assert($argName != null, 'Argument for $channelName was null, expected non-null $argType.');");
-                  }
-                });
-                final Iterable<String> argNames =
-                    indexMap(func.arguments, (int index, NamedType field) {
-                  final String name = _getSafeArgumentName(index, field);
-                  return '$name${field.type.isNullable ? '' : '!'}';
-                });
-                call = 'api.${func.name}(${argNames.join(', ')})';
-              }
-              if (func.returnType.isVoid) {
-                if (isAsync) {
-                  indent.writeln('await $call;');
-                } else {
-                  indent.writeln('$call;');
-                }
-                indent.writeln(emptyReturnStatement);
-              } else {
-                if (isAsync) {
-                  indent.writeln('final $returnType output = await $call;');
-                } else {
-                  indent.writeln('final $returnType output = $call;');
-                }
-                const String returnExpression = 'output';
-                final String returnStatement = isMockHandler
-                    ? 'return <Object?>[$returnExpression];'
-                    : 'return $returnExpression;';
-                indent.writeln(returnStatement);
-              }
-            });
-          });
-        });
-      }
-    });
-  });
-}
-
 /// Converts a [List] of [TypeDeclaration]s to a comma separated [String] to be
 /// used in Dart code.
 String _flattenTypeArguments(List<TypeDeclaration> args) {
@@ -501,196 +756,6 @@
   return type.isNullable ? '$genericdType?' : genericdType;
 }
 
-/// Generates Dart source code for the given AST represented by [root],
-/// outputting the code to [sink].
-void generateDart(DartOptions opt, Root root, StringSink sink) {
-  final List<String> customClassNames =
-      root.classes.map((Class x) => x.name).toList();
-  final List<String> customEnumNames =
-      root.enums.map((Enum x) => x.name).toList();
-  final Indent indent = Indent(sink);
-
-  void writeHeader() {
-    if (opt.copyrightHeader != null) {
-      addLines(indent, opt.copyrightHeader!, linePrefix: '// ');
-    }
-    indent.writeln('// $generatedCodeWarning');
-    indent.writeln('// $seeAlsoWarning');
-    indent.writeln(
-      '// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import',
-    );
-  }
-
-  void writeEnums() {
-    for (final Enum anEnum in root.enums) {
-      indent.writeln('');
-      addDocumentationComments(
-          indent, anEnum.documentationComments, _docCommentSpec);
-      indent.write('enum ${anEnum.name} ');
-      indent.scoped('{', '}', () {
-        for (final EnumMember member in anEnum.members) {
-          addDocumentationComments(
-              indent, member.documentationComments, _docCommentSpec);
-          indent.writeln('${member.name},');
-        }
-      });
-    }
-  }
-
-  void writeImports() {
-    indent.writeln("import 'dart:async';");
-    indent.writeln(
-      "import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;",
-    );
-    indent.addln('');
-    indent.writeln(
-        "import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;");
-    indent.writeln("import 'package:flutter/services.dart';");
-  }
-
-  void writeDataClass(Class klass) {
-    void writeConstructor() {
-      indent.write(klass.name);
-      indent.scoped('({', '});', () {
-        for (final NamedType field in getFieldsInSerializationOrder(klass)) {
-          final String required = field.type.isNullable ? '' : 'required ';
-          indent.writeln('${required}this.${field.name},');
-        }
-      });
-    }
-
-    void writeEncode() {
-      indent.write('Object encode() ');
-      indent.scoped('{', '}', () {
-        indent.write(
-          'return <Object?>',
-        );
-        indent.scoped('[', '];', () {
-          for (final NamedType field in getFieldsInSerializationOrder(klass)) {
-            final String conditional = field.type.isNullable ? '?' : '';
-            if (customClassNames.contains(field.type.baseName)) {
-              indent.writeln(
-                '${field.name}$conditional.encode(),',
-              );
-            } else if (customEnumNames.contains(field.type.baseName)) {
-              indent.writeln(
-                '${field.name}$conditional.index,',
-              );
-            } else {
-              indent.writeln('${field.name},');
-            }
-          }
-        });
-      });
-    }
-
-    void writeDecode() {
-      void writeValueDecode(NamedType field, int index) {
-        final String resultAt = 'result[$index]';
-        if (customClassNames.contains(field.type.baseName)) {
-          final String nonNullValue =
-              '${field.type.baseName}.decode($resultAt! as List<Object?>)';
-          indent.format(
-              field.type.isNullable
-                  ? '''
-$resultAt != null
-\t\t? $nonNullValue
-\t\t: null'''
-                  : nonNullValue,
-              leadingSpace: false,
-              trailingNewline: false);
-        } else if (customEnumNames.contains(field.type.baseName)) {
-          final String nonNullValue =
-              '${field.type.baseName}.values[$resultAt! as int]';
-          indent.format(
-              field.type.isNullable
-                  ? '''
-$resultAt != null
-\t\t? $nonNullValue
-\t\t: null'''
-                  : nonNullValue,
-              leadingSpace: false,
-              trailingNewline: false);
-        } else if (field.type.typeArguments.isNotEmpty) {
-          final String genericType = _makeGenericTypeArguments(field.type);
-          final String castCall = _makeGenericCastCall(field.type);
-          final String castCallPrefix = field.type.isNullable ? '?' : '!';
-          indent.add(
-            '($resultAt as $genericType?)$castCallPrefix$castCall',
-          );
-        } else {
-          final String genericdType = _addGenericTypesNullable(field.type);
-          if (field.type.isNullable) {
-            indent.add(
-              '$resultAt as $genericdType',
-            );
-          } else {
-            indent.add(
-              '$resultAt! as $genericdType',
-            );
-          }
-        }
-      }
-
-      indent.write(
-        'static ${klass.name} decode(Object result) ',
-      );
-      indent.scoped('{', '}', () {
-        indent.writeln('result as List<Object?>;');
-        indent.write('return ${klass.name}');
-        indent.scoped('(', ');', () {
-          enumerate(getFieldsInSerializationOrder(klass),
-              (int index, final NamedType field) {
-            indent.write('${field.name}: ');
-            writeValueDecode(field, index);
-            indent.addln(',');
-          });
-        });
-      });
-    }
-
-    addDocumentationComments(
-        indent, klass.documentationComments, _docCommentSpec);
-
-    indent.write('class ${klass.name} ');
-    indent.scoped('{', '}', () {
-      writeConstructor();
-      indent.addln('');
-      for (final NamedType field in getFieldsInSerializationOrder(klass)) {
-        addDocumentationComments(
-            indent, field.documentationComments, _docCommentSpec);
-
-        final String datatype = _addGenericTypesNullable(field.type);
-        indent.writeln('$datatype ${field.name};');
-        indent.writeln('');
-      }
-      writeEncode();
-      indent.writeln('');
-      writeDecode();
-    });
-  }
-
-  void writeApi(Api api) {
-    if (api.location == ApiLocation.host) {
-      _writeHostApi(opt, indent, api, root);
-    } else if (api.location == ApiLocation.flutter) {
-      _writeFlutterApi(opt, indent, api, root);
-    }
-  }
-
-  writeHeader();
-  writeImports();
-  writeEnums();
-  for (final Class klass in root.classes) {
-    indent.writeln('');
-    writeDataClass(klass);
-  }
-  for (final Api api in root.apis) {
-    indent.writeln('');
-    writeApi(api);
-  }
-}
-
 /// Crawls up the path of [dartFilePath] until it finds a pubspec.yaml in a
 /// parent directory and returns its path.
 String? _findPubspecPath(String dartFilePath) {
@@ -739,72 +804,3 @@
   final path.Context context = path.Context(style: path.Style.posix);
   return context.fromUri(path.toUri(path.absolute(inputPath)));
 }
-
-/// Generates Dart source code for test support libraries based on the given AST
-/// represented by [root], outputting the code to [sink]. [sourceOutPath] is the
-/// path of the generated dart code to be tested. [testOutPath] is where the
-/// test code will be generated.
-void generateTestDart(
-  DartOptions opt,
-  Root root,
-  StringSink sink, {
-  required String sourceOutPath,
-  required String testOutPath,
-}) {
-  final Indent indent = Indent(sink);
-  if (opt.copyrightHeader != null) {
-    addLines(indent, opt.copyrightHeader!, linePrefix: '// ');
-  }
-  indent.writeln('// $generatedCodeWarning');
-  indent.writeln('// $seeAlsoWarning');
-  indent.writeln(
-    '// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, unnecessary_import',
-  );
-  indent.writeln('// ignore_for_file: avoid_relative_lib_imports');
-  indent.writeln("import 'dart:async';");
-  indent.writeln(
-    "import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;",
-  );
-  indent.writeln(
-      "import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;");
-  indent.writeln("import 'package:flutter/services.dart';");
-  indent.writeln("import 'package:flutter_test/flutter_test.dart';");
-  indent.writeln('');
-  final String relativeDartPath =
-      path.Context(style: path.Style.posix).relative(
-    _posixify(sourceOutPath),
-    from: _posixify(path.dirname(testOutPath)),
-  );
-  late final String? packageName = _deducePackageName(sourceOutPath);
-  if (!relativeDartPath.contains('/lib/') || packageName == null) {
-    // If we can't figure out the package name or the relative path doesn't
-    // include a 'lib' directory, try relative path import which only works in
-    // certain (older) versions of Dart.
-    // TODO(gaaclarke): We should add a command-line parameter to override this import.
-    indent.writeln(
-        "import '${_escapeForDartSingleQuotedString(relativeDartPath)}';");
-  } else {
-    final String path = relativeDartPath.replaceFirst(RegExp(r'^.*/lib/'), '');
-    indent.writeln("import 'package:$packageName/$path';");
-  }
-  for (final Api api in root.apis) {
-    if (api.location == ApiLocation.host && api.dartHostTestHandler != null) {
-      final Api mockApi = Api(
-        name: api.dartHostTestHandler!,
-        methods: api.methods,
-        location: ApiLocation.flutter,
-        dartHostTestHandler: api.dartHostTestHandler,
-        documentationComments: api.documentationComments,
-      );
-      indent.writeln('');
-      _writeFlutterApi(
-        opt,
-        indent,
-        mockApi,
-        root,
-        channelNameFunc: (Method func) => makeChannelName(api, func),
-        isMockHandler: true,
-      );
-    }
-  }
-}
diff --git a/packages/pigeon/lib/generator.dart b/packages/pigeon/lib/generator.dart
index 02667e4..c730f92 100644
--- a/packages/pigeon/lib/generator.dart
+++ b/packages/pigeon/lib/generator.dart
@@ -3,11 +3,119 @@
 // found in the LICENSE file.
 
 import 'ast.dart';
+import 'generator_tools.dart';
 
-/// A superclass of generator classes.
+/// An abstract base class of generators.
 ///
 /// This provides the structure that is common across generators for different languages.
 abstract class Generator<T> {
-  /// Generates files for specified language with specified [languageOptions]
-  void generate(T languageOptions, Root root, StringSink sink);
+  /// Constructor.
+  const Generator();
+
+  /// Generates files for specified language with specified [generatorOptions]
+  void generate(T generatorOptions, Root root, StringSink sink);
+}
+
+/// An abstract base class that enforces code generation across platforms.
+abstract class StructuredGenerator<T> extends Generator<T> {
+  /// Constructor.
+  const StructuredGenerator();
+
+  @override
+  void generate(
+    T generatorOptions,
+    Root root,
+    StringSink sink,
+  ) {
+    final Indent indent = Indent(sink);
+
+    writeFilePrologue(generatorOptions, root, indent);
+
+    writeFileImports(generatorOptions, root, indent);
+
+    writeOpenNamespace(generatorOptions, root, indent);
+
+    writeGeneralUtilities(generatorOptions, root, indent);
+
+    writeEnums(generatorOptions, root, indent);
+
+    writeDataClasses(generatorOptions, root, indent);
+
+    writeApis(generatorOptions, root, indent);
+
+    writeCloseNamespace(generatorOptions, root, indent);
+  }
+
+  /// Adds specified headers to [indent].
+  void writeFilePrologue(T generatorOptions, Root root, Indent indent);
+
+  /// Writes specified imports to [indent].
+  void writeFileImports(T generatorOptions, Root root, Indent indent);
+
+  /// Writes code to [indent] that opens file namespace if needed.
+  ///
+  /// This method is not required, and does not need to be overridden.
+  void writeOpenNamespace(T generatorOptions, Root root, Indent indent) {}
+
+  /// Writes code to [indent] that closes file namespace if needed.
+  ///
+  /// This method is not required, and does not need to be overridden.
+  void writeCloseNamespace(T generatorOptions, Root root, Indent indent) {}
+
+  /// Writes any necessary helper utilities to [indent] if needed.
+  ///
+  /// This method is not required, and does not need to be overridden.
+  void writeGeneralUtilities(T generatorOptions, Root root, Indent indent) {}
+
+  /// Writes all enums to [indent].
+  ///
+  /// Can be overridden to add extra code before/after enums.
+  void writeEnums(T generatorOptions, Root root, Indent indent) {
+    for (final Enum anEnum in root.enums) {
+      writeEnum(generatorOptions, root, indent, anEnum);
+    }
+  }
+
+  /// Writes a single Enum to [indent]. This is needed in most generators.
+  void writeEnum(T generatorOptions, Root root, Indent indent, Enum anEnum) {}
+
+  /// Writes all data classes to [indent].
+  ///
+  /// Can be overridden to add extra code before/after apis.
+  void writeDataClasses(T generatorOptions, Root root, Indent indent) {
+    for (final Class klass in root.classes) {
+      writeDataClass(generatorOptions, root, indent, klass);
+    }
+  }
+
+  /// Writes a single data class to [indent].
+  void writeDataClass(
+      T generatorOptions, Root root, Indent indent, Class klass);
+
+  /// Writes a single class encode method to [indent].
+  void writeClassEncode(T generatorOptions, Root root, Indent indent,
+      Class klass, Set<String> customClassNames, Set<String> customEnumNames) {}
+
+  /// Writes a single class decode method to [indent].
+  void writeClassDecode(T generatorOptions, Root root, Indent indent,
+      Class klass, Set<String> customClassNames, Set<String> customEnumNames) {}
+
+  /// Writes all apis to [indent].
+  ///
+  /// Can be overridden to add extra code before/after classes.
+  void writeApis(T generatorOptions, Root root, Indent indent) {
+    for (final Api api in root.apis) {
+      if (api.location == ApiLocation.host) {
+        writeHostApi(generatorOptions, root, indent, api);
+      } else if (api.location == ApiLocation.flutter) {
+        writeFlutterApi(generatorOptions, root, indent, api);
+      }
+    }
+  }
+
+  /// Writes a single Flutter Api to [indent].
+  void writeFlutterApi(T generatorOptions, Root root, Indent indent, Api api);
+
+  /// Writes a single Host Api to [indent].
+  void writeHostApi(T generatorOptions, Root root, Indent indent, Api api);
 }
diff --git a/packages/pigeon/lib/generator_tools.dart b/packages/pigeon/lib/generator_tools.dart
index 4355096..3dc1df4 100644
--- a/packages/pigeon/lib/generator_tools.dart
+++ b/packages/pigeon/lib/generator_tools.dart
@@ -9,7 +9,7 @@
 import 'ast.dart';
 
 /// The current version of pigeon. This must match the version in pubspec.yaml.
-const String pigeonVersion = '5.0.1';
+const String pigeonVersion = '6.0.0';
 
 /// Read all the content from [stdin] to a String.
 String readStdin() {
@@ -510,7 +510,7 @@
   na,
 }
 
-/// Options for [Generator]'s that have multiple output file types.
+/// Options for [Generator]s that have multiple output file types.
 ///
 /// Specifies which file to write as well as wraps all language options.
 class OutputFileOptions<T> {
diff --git a/packages/pigeon/lib/java_generator.dart b/packages/pigeon/lib/java_generator.dart
index daca638..e6e7f8c 100644
--- a/packages/pigeon/lib/java_generator.dart
+++ b/packages/pigeon/lib/java_generator.dart
@@ -86,94 +86,433 @@
 }
 
 /// Class that manages all Java code generation.
-class JavaGenerator extends Generator<JavaOptions> {
+class JavaGenerator extends StructuredGenerator<JavaOptions> {
   /// Instantiates a Java Generator.
-  JavaGenerator();
+  const JavaGenerator();
 
-  /// Generates Java files with specified [JavaOptions]
   @override
-  void generate(JavaOptions languageOptions, Root root, StringSink sink,
-      {FileType fileType = FileType.na}) {
-    assert(fileType == FileType.na);
-    generateJava(languageOptions, root, sink);
+  void writeFilePrologue(
+      JavaOptions generatorOptions, Root root, Indent indent) {
+    if (generatorOptions.copyrightHeader != null) {
+      addLines(indent, generatorOptions.copyrightHeader!, linePrefix: '// ');
+    }
+    indent.writeln('// $generatedCodeWarning');
+    indent.writeln('// $seeAlsoWarning');
+    indent.addln('');
   }
-}
 
-/// Calculates the name of the codec that will be generated for [api].
-String _getCodecName(Api api) => '${api.name}Codec';
+  @override
+  void writeFileImports(
+      JavaOptions generatorOptions, Root root, Indent indent) {
+    if (generatorOptions.package != null) {
+      indent.writeln('package ${generatorOptions.package};');
+    }
+    indent.writeln('import android.util.Log;');
+    indent.writeln('import androidx.annotation.NonNull;');
+    indent.writeln('import androidx.annotation.Nullable;');
+    indent.writeln('import io.flutter.plugin.common.BasicMessageChannel;');
+    indent.writeln('import io.flutter.plugin.common.BinaryMessenger;');
+    indent.writeln('import io.flutter.plugin.common.MessageCodec;');
+    indent.writeln('import io.flutter.plugin.common.StandardMessageCodec;');
+    indent.writeln('import java.io.ByteArrayOutputStream;');
+    indent.writeln('import java.nio.ByteBuffer;');
+    indent.writeln('import java.util.Arrays;');
+    indent.writeln('import java.util.ArrayList;');
+    indent.writeln('import java.util.Collections;');
+    indent.writeln('import java.util.List;');
+    indent.writeln('import java.util.Map;');
+    indent.writeln('import java.util.HashMap;');
+    indent.addln('');
+  }
 
-/// Converts an expression that evaluates to an nullable int to an expression
-/// that evaluates to a nullable enum.
-String _intToEnum(String expression, String enumName) =>
-    '$expression == null ? null : $enumName.values()[(int)$expression]';
+  @override
+  void writeOpenNamespace(
+      JavaOptions generatorOptions, Root root, Indent indent) {
+    indent.writeln(
+        '$_docCommentPrefix Generated class from Pigeon.$_docCommentSuffix');
+    indent.writeln(
+        '@SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression"})');
+    if (generatorOptions.useGeneratedAnnotation ?? false) {
+      indent.writeln('@javax.annotation.Generated("dev.flutter.pigeon")');
+    }
+    indent.writeln('public class ${generatorOptions.className!} {');
+    indent.inc();
+  }
 
-/// Writes the codec class that will be used by [api].
-/// Example:
-/// private static class FooCodec extends StandardMessageCodec {...}
-void _writeCodec(Indent indent, Api api, Root root) {
-  assert(getCodecClasses(api, root).isNotEmpty);
-  final Iterable<EnumeratedClass> codecClasses = getCodecClasses(api, root);
-  final String codecName = _getCodecName(api);
-  indent
-      .write('private static class $codecName extends $_standardMessageCodec ');
-  indent.scoped('{', '}', () {
-    indent
-        .writeln('public static final $codecName INSTANCE = new $codecName();');
-    indent.writeln('private $codecName() {}');
-    indent.writeln('@Override');
-    indent.write(
-        'protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) ');
+  @override
+  void writeEnum(
+      JavaOptions generatorOptions, Root root, Indent indent, Enum anEnum) {
+    String camelToSnake(String camelCase) {
+      final RegExp regex = RegExp('([a-z])([A-Z]+)');
+      return camelCase
+          .replaceAllMapped(regex, (Match m) => '${m[1]}_${m[2]}')
+          .toUpperCase();
+    }
+
+    indent.writeln('');
+    addDocumentationComments(
+        indent, anEnum.documentationComments, _docCommentSpec);
+
+    indent.write('public enum ${anEnum.name} ');
     indent.scoped('{', '}', () {
-      indent.write('switch (type) ');
+      enumerate(anEnum.members, (int index, final EnumMember member) {
+        addDocumentationComments(
+            indent, member.documentationComments, _docCommentSpec);
+        indent.writeln(
+            '${camelToSnake(member.name)}($index)${index == anEnum.members.length - 1 ? ';' : ','}');
+      });
+      indent.writeln('');
+      indent.writeln('private final int index;');
+      indent.write('private ${anEnum.name}(final int index) ');
       indent.scoped('{', '}', () {
-        for (final EnumeratedClass customClass in codecClasses) {
-          indent.write('case (byte)${customClass.enumeration}: ');
-          indent.writeScoped('', '', () {
-            indent.writeln(
-                'return ${customClass.name}.fromList((ArrayList<Object>) readValue(buffer));');
-          });
-        }
-        indent.write('default:');
-        indent.writeScoped('', '', () {
-          indent.writeln('return super.readValueOfType(type, buffer);');
-        });
+        indent.writeln('this.index = index;');
       });
     });
-    indent.writeln('@Override');
-    indent.write(
-        'protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) ');
-    indent.writeScoped('{', '}', () {
-      for (final EnumeratedClass customClass in codecClasses) {
-        indent.write('if (value instanceof ${customClass.name}) ');
-        indent.scoped('{', '} else ', () {
-          indent.writeln('stream.write(${customClass.enumeration});');
+  }
+
+  @override
+  void writeDataClass(
+      JavaOptions generatorOptions, Root root, Indent indent, Class klass) {
+    final Set<String> customClassNames =
+        root.classes.map((Class x) => x.name).toSet();
+    final Set<String> customEnumNames =
+        root.enums.map((Enum x) => x.name).toSet();
+
+    const List<String> generatedMessages = <String>[
+      ' Generated class from Pigeon that represents data sent in messages.'
+    ];
+    indent.addln('');
+    addDocumentationComments(
+        indent, klass.documentationComments, _docCommentSpec,
+        generatorComments: generatedMessages);
+
+    indent.write('public static class ${klass.name} ');
+    indent.scoped('{', '}', () {
+      for (final NamedType field in getFieldsInSerializationOrder(klass)) {
+        _writeClassField(generatorOptions, root, indent, field);
+        indent.addln('');
+      }
+
+      if (getFieldsInSerializationOrder(klass)
+          .map((NamedType e) => !e.type.isNullable)
+          .any((bool e) => e)) {
+        indent.writeln(
+            '${_docCommentPrefix}Constructor is private to enforce null safety; use Builder.$_docCommentSuffix');
+        indent.writeln('private ${klass.name}() {}');
+      }
+
+      _writeClassBuilder(generatorOptions, root, indent, klass);
+      writeClassEncode(generatorOptions, root, indent, klass, customClassNames,
+          customEnumNames);
+      writeClassDecode(generatorOptions, root, indent, klass, customClassNames,
+          customEnumNames);
+    });
+  }
+
+  void _writeClassField(
+      JavaOptions generatorOptions, Root root, Indent indent, NamedType field) {
+    final HostDatatype hostDatatype = getFieldHostDatatype(field, root.classes,
+        root.enums, (TypeDeclaration x) => _javaTypeForBuiltinDartType(x));
+    final String nullability = field.type.isNullable ? '@Nullable' : '@NonNull';
+    addDocumentationComments(
+        indent, field.documentationComments, _docCommentSpec);
+
+    indent.writeln(
+        'private $nullability ${hostDatatype.datatype} ${field.name};');
+    indent.writeln(
+        'public $nullability ${hostDatatype.datatype} ${_makeGetter(field)}() { return ${field.name}; }');
+    indent.writeScoped(
+        'public void ${_makeSetter(field)}($nullability ${hostDatatype.datatype} setterArg) {',
+        '}', () {
+      if (!field.type.isNullable) {
+        indent.writeScoped('if (setterArg == null) {', '}', () {
           indent.writeln(
-              'writeValue(stream, ((${customClass.name}) value).toList());');
+              'throw new IllegalStateException("Nonnull field \\"${field.name}\\" is null.");');
         });
       }
+      indent.writeln('this.${field.name} = setterArg;');
+    });
+  }
+
+  void _writeClassBuilder(
+    JavaOptions generatorOptions,
+    Root root,
+    Indent indent,
+    Class klass,
+  ) {
+    indent.write('public static final class Builder ');
+    indent.scoped('{', '}', () {
+      for (final NamedType field in getFieldsInSerializationOrder(klass)) {
+        final HostDatatype hostDatatype = getFieldHostDatatype(
+            field,
+            root.classes,
+            root.enums,
+            (TypeDeclaration x) => _javaTypeForBuiltinDartType(x));
+        final String nullability =
+            field.type.isNullable ? '@Nullable' : '@NonNull';
+        indent.writeln(
+            'private @Nullable ${hostDatatype.datatype} ${field.name};');
+        indent.writeScoped(
+            'public @NonNull Builder ${_makeSetter(field)}($nullability ${hostDatatype.datatype} setterArg) {',
+            '}', () {
+          indent.writeln('this.${field.name} = setterArg;');
+          indent.writeln('return this;');
+        });
+      }
+      indent.write('public @NonNull ${klass.name} build() ');
       indent.scoped('{', '}', () {
-        indent.writeln('super.writeValue(stream, value);');
+        const String returnVal = 'pigeonReturn';
+        indent.writeln('${klass.name} $returnVal = new ${klass.name}();');
+        for (final NamedType field in getFieldsInSerializationOrder(klass)) {
+          indent.writeln('$returnVal.${_makeSetter(field)}(${field.name});');
+        }
+        indent.writeln('return $returnVal;');
       });
     });
-  });
-}
+  }
 
-/// Write the java code that represents a host [Api], [api].
-/// Example:
-/// public interface Foo {
-///   int add(int x, int y);
-///   static void setup(BinaryMessenger binaryMessenger, Foo api) {...}
-/// }
-void _writeHostApi(Indent indent, Api api, Root root) {
-  assert(api.location == ApiLocation.host);
+  @override
+  void writeClassEncode(
+    JavaOptions generatorOptions,
+    Root root,
+    Indent indent,
+    Class klass,
+    Set<String> customClassNames,
+    Set<String> customEnumNames,
+  ) {
+    indent.write('@NonNull ArrayList<Object> toList() ');
+    indent.scoped('{', '}', () {
+      indent.writeln(
+          'ArrayList<Object> toListResult = new ArrayList<Object>(${klass.fields.length});');
+      for (final NamedType field in getFieldsInSerializationOrder(klass)) {
+        final HostDatatype hostDatatype = getFieldHostDatatype(
+            field,
+            root.classes,
+            root.enums,
+            (TypeDeclaration x) => _javaTypeForBuiltinDartType(x));
+        String toWriteValue = '';
+        final String fieldName = field.name;
+        if (!hostDatatype.isBuiltin &&
+            customClassNames.contains(field.type.baseName)) {
+          toWriteValue = '($fieldName == null) ? null : $fieldName.toList()';
+        } else if (!hostDatatype.isBuiltin &&
+            customEnumNames.contains(field.type.baseName)) {
+          toWriteValue = '$fieldName == null ? null : $fieldName.index';
+        } else {
+          toWriteValue = field.name;
+        }
+        indent.writeln('toListResult.add($toWriteValue);');
+      }
+      indent.writeln('return toListResult;');
+    });
+  }
 
-  bool isEnum(TypeDeclaration type) =>
-      root.enums.map((Enum e) => e.name).contains(type.baseName);
+  @override
+  void writeClassDecode(
+    JavaOptions generatorOptions,
+    Root root,
+    Indent indent,
+    Class klass,
+    Set<String> customClassNames,
+    Set<String> customEnumNames,
+  ) {
+    indent.write(
+        'static @NonNull ${klass.name} fromList(@NonNull ArrayList<Object> list) ');
+    indent.scoped('{', '}', () {
+      const String result = 'pigeonResult';
+      indent.writeln('${klass.name} $result = new ${klass.name}();');
+      enumerate(getFieldsInSerializationOrder(klass),
+          (int index, final NamedType field) {
+        final String fieldVariable = field.name;
+        final String setter = _makeSetter(field);
+        indent.writeln('Object $fieldVariable = list.get($index);');
+        if (customEnumNames.contains(field.type.baseName)) {
+          indent.writeln(
+              '$result.$setter(${_intToEnum(fieldVariable, field.type.baseName)});');
+        } else {
+          indent.writeln(
+              '$result.$setter(${_castObject(field, root.classes, root.enums, fieldVariable)});');
+        }
+      });
+      indent.writeln('return $result;');
+    });
+  }
+
+  /// Writes the code for a flutter [Api], [api].
+  /// Example:
+  /// public static class Foo {
+  ///   public Foo(BinaryMessenger argBinaryMessenger) {...}
+  ///   public interface Reply<T> {
+  ///     void reply(T reply);
+  ///   }
+  ///   public int add(int x, int y, Reply<int> callback) {...}
+  /// }
+  @override
+  void writeFlutterApi(
+    JavaOptions generatorOptions,
+    Root root,
+    Indent indent,
+    Api api,
+  ) {
+    assert(api.location == ApiLocation.flutter);
+    if (getCodecClasses(api, root).isNotEmpty) {
+      _writeCodec(indent, api, root);
+    }
+    const List<String> generatedMessages = <String>[
+      ' Generated class from Pigeon that represents Flutter messages that can be called from Java.'
+    ];
+    addDocumentationComments(indent, api.documentationComments, _docCommentSpec,
+        generatorComments: generatedMessages);
+
+    indent.write('public static class ${api.name} ');
+    indent.scoped('{', '}', () {
+      indent.writeln('private final BinaryMessenger binaryMessenger;');
+      indent.write('public ${api.name}(BinaryMessenger argBinaryMessenger)');
+      indent.scoped('{', '}', () {
+        indent.writeln('this.binaryMessenger = argBinaryMessenger;');
+      });
+      indent.write('public interface Reply<T> ');
+      indent.scoped('{', '}', () {
+        indent.writeln('void reply(T reply);');
+      });
+      final String codecName = _getCodecName(api);
+      indent.writeln('/** The codec used by ${api.name}. */');
+      indent.write('static MessageCodec<Object> getCodec() ');
+      indent.scoped('{', '}', () {
+        indent.write('return ');
+        if (getCodecClasses(api, root).isNotEmpty) {
+          indent.writeln('$codecName.INSTANCE;');
+        } else {
+          indent.writeln('new $_standardMessageCodec();');
+        }
+      });
+
+      for (final Method func in api.methods) {
+        final String channelName = makeChannelName(api, func);
+        final String returnType = func.returnType.isVoid
+            ? 'Void'
+            : _javaTypeForDartType(func.returnType);
+        String sendArgument;
+        addDocumentationComments(
+            indent, func.documentationComments, _docCommentSpec);
+        if (func.arguments.isEmpty) {
+          indent
+              .write('public void ${func.name}(Reply<$returnType> callback) ');
+          sendArgument = 'null';
+        } else {
+          final Iterable<String> argTypes = func.arguments
+              .map((NamedType e) => _nullsafeJavaTypeForDartType(e.type));
+          final Iterable<String> argNames =
+              indexMap(func.arguments, _getSafeArgumentName);
+          if (func.arguments.length == 1) {
+            sendArgument =
+                'new ArrayList<Object>(Collections.singletonList(${argNames.first}))';
+          } else {
+            sendArgument =
+                'new ArrayList<Object>(Arrays.asList(${argNames.join(', ')}))';
+          }
+          final String argsSignature =
+              map2(argTypes, argNames, (String x, String y) => '$x $y')
+                  .join(', ');
+          indent.write(
+              'public void ${func.name}($argsSignature, Reply<$returnType> callback) ');
+        }
+        indent.scoped('{', '}', () {
+          const String channel = 'channel';
+          indent.writeln('BasicMessageChannel<Object> $channel =');
+          indent.inc();
+          indent.inc();
+          indent.writeln(
+              'new BasicMessageChannel<>(binaryMessenger, "$channelName", getCodec());');
+          indent.dec();
+          indent.dec();
+          indent.write('$channel.send($sendArgument, channelReply -> ');
+          indent.scoped('{', '});', () {
+            if (func.returnType.isVoid) {
+              indent.writeln('callback.reply(null);');
+            } else {
+              const String output = 'output';
+              indent.writeln('@SuppressWarnings("ConstantConditions")');
+              if (func.returnType.baseName == 'int') {
+                indent.writeln(
+                    '$returnType $output = channelReply == null ? null : ((Number)channelReply).longValue();');
+              } else {
+                indent.writeln(
+                    '$returnType $output = ($returnType)channelReply;');
+              }
+              indent.writeln('callback.reply($output);');
+            }
+          });
+        });
+      }
+    });
+  }
+
+  @override
+  void writeApis(JavaOptions generatorOptions, Root root, Indent indent) {
+    if (root.apis.any((Api api) =>
+        api.location == ApiLocation.host &&
+        api.methods.any((Method it) => it.isAsynchronous))) {
+      indent.addln('');
+      _writeResultInterface(indent);
+    }
+    super.writeApis(generatorOptions, root, indent);
+  }
+
+  /// Write the java code that represents a host [Api], [api].
+  /// Example:
+  /// public interface Foo {
+  ///   int add(int x, int y);
+  ///   static void setup(BinaryMessenger binaryMessenger, Foo api) {...}
+  /// }
+  @override
+  void writeHostApi(
+      JavaOptions generatorOptions, Root root, Indent indent, Api api) {
+    assert(api.location == ApiLocation.host);
+    if (getCodecClasses(api, root).isNotEmpty) {
+      _writeCodec(indent, api, root);
+    }
+    const List<String> generatedMessages = <String>[
+      ' Generated interface from Pigeon that represents a handler of messages from Flutter.'
+    ];
+    addDocumentationComments(indent, api.documentationComments, _docCommentSpec,
+        generatorComments: generatedMessages);
+
+    indent.write('public interface ${api.name} ');
+    indent.scoped('{', '}', () {
+      for (final Method method in api.methods) {
+        _writeInterfaceMethod(generatorOptions, root, indent, api, method);
+      }
+      indent.addln('');
+      final String codecName = _getCodecName(api);
+      indent.writeln('/** The codec used by ${api.name}. */');
+      indent.write('static MessageCodec<Object> getCodec() ');
+      indent.scoped('{', '}', () {
+        indent.write('return ');
+        if (getCodecClasses(api, root).isNotEmpty) {
+          indent.write('$codecName.INSTANCE;');
+        } else {
+          indent.write('new $_standardMessageCodec();');
+        }
+      });
+
+      indent.writeln(
+          '${_docCommentPrefix}Sets up an instance of `${api.name}` to handle messages through the `binaryMessenger`.$_docCommentSuffix');
+      indent.write(
+          'static void setup(BinaryMessenger binaryMessenger, ${api.name} api) ');
+      indent.scoped('{', '}', () {
+        for (final Method method in api.methods) {
+          _writeMethodSetup(generatorOptions, root, indent, api, method);
+        }
+      });
+    });
+  }
 
   /// Write a method in the interface.
   /// Example:
   ///   int add(int x, int y);
-  void writeInterfaceMethod(final Method method) {
+  void _writeInterfaceMethod(JavaOptions generatorOptions, Root root,
+      Indent indent, Api api, final Method method) {
     final String returnType = method.isAsynchronous
         ? 'void'
         : _nullsafeJavaTypeForDartType(method.returnType);
@@ -203,7 +542,8 @@
   /// Write a static setup function in the interface.
   /// Example:
   ///   static void setup(BinaryMessenger binaryMessenger, Foo api) {...}
-  void writeMethodSetup(final Method method) {
+  void _writeMethodSetup(JavaOptions generatorOptions, Root root, Indent indent,
+      Api api, final Method method) {
     final String channelName = makeChannelName(api, method);
     indent.write('');
     indent.scoped('{', '}', () {
@@ -252,7 +592,7 @@
                     ? '($argName == null) ? null : $argName.longValue()'
                     : argName;
                 String accessor = 'args.get($index)';
-                if (isEnum(arg.type)) {
+                if (isEnum(root, arg.type)) {
                   accessor = _intToEnum(accessor, arg.type.baseName);
                 } else if (argType != 'Object') {
                   accessor = '($argType)$accessor';
@@ -319,38 +659,99 @@
     });
   }
 
-  const List<String> generatedMessages = <String>[
-    ' Generated interface from Pigeon that represents a handler of messages from Flutter.'
-  ];
-  addDocumentationComments(indent, api.documentationComments, _docCommentSpec,
-      generatorComments: generatedMessages);
-
-  indent.write('public interface ${api.name} ');
-  indent.scoped('{', '}', () {
-    api.methods.forEach(writeInterfaceMethod);
-    indent.addln('');
+  /// Writes the codec class that will be used by [api].
+  /// Example:
+  /// private static class FooCodec extends StandardMessageCodec {...}
+  void _writeCodec(Indent indent, Api api, Root root) {
+    assert(getCodecClasses(api, root).isNotEmpty);
+    final Iterable<EnumeratedClass> codecClasses = getCodecClasses(api, root);
     final String codecName = _getCodecName(api);
-    indent.writeln('/** The codec used by ${api.name}. */');
-    indent.write('static MessageCodec<Object> getCodec() ');
-    indent.scoped('{', '}', () {
-      indent.write('return ');
-      if (getCodecClasses(api, root).isNotEmpty) {
-        indent.write('$codecName.INSTANCE;');
-      } else {
-        indent.write('new $_standardMessageCodec();');
-      }
-    });
-
-    indent.writeln(
-        '${_docCommentPrefix}Sets up an instance of `${api.name}` to handle messages through the `binaryMessenger`.$_docCommentSuffix');
     indent.write(
-        'static void setup(BinaryMessenger binaryMessenger, ${api.name} api) ');
+        'private static class $codecName extends $_standardMessageCodec ');
     indent.scoped('{', '}', () {
-      api.methods.forEach(writeMethodSetup);
+      indent.writeln(
+          'public static final $codecName INSTANCE = new $codecName();');
+      indent.writeln('private $codecName() {}');
+      indent.writeln('@Override');
+      indent.write(
+          'protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) ');
+      indent.scoped('{', '}', () {
+        indent.write('switch (type) ');
+        indent.scoped('{', '}', () {
+          for (final EnumeratedClass customClass in codecClasses) {
+            indent.write('case (byte)${customClass.enumeration}: ');
+            indent.writeScoped('', '', () {
+              indent.writeln(
+                  'return ${customClass.name}.fromList((ArrayList<Object>) readValue(buffer));');
+            });
+          }
+          indent.write('default:');
+          indent.writeScoped('', '', () {
+            indent.writeln('return super.readValueOfType(type, buffer);');
+          });
+        });
+      });
+      indent.writeln('@Override');
+      indent.write(
+          'protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) ');
+      indent.writeScoped('{', '}', () {
+        for (final EnumeratedClass customClass in codecClasses) {
+          indent.write('if (value instanceof ${customClass.name}) ');
+          indent.scoped('{', '} else ', () {
+            indent.writeln('stream.write(${customClass.enumeration});');
+            indent.writeln(
+                'writeValue(stream, ((${customClass.name}) value).toList());');
+          });
+        }
+        indent.scoped('{', '}', () {
+          indent.writeln('super.writeValue(stream, value);');
+        });
+      });
     });
-  });
+    indent.addln('');
+  }
+
+  void _writeResultInterface(Indent indent) {
+    indent.write('public interface Result<T> ');
+    indent.scoped('{', '}', () {
+      indent.writeln('void success(T result);');
+      indent.writeln('void error(Throwable error);');
+    });
+  }
+
+  void _writeWrapError(Indent indent) {
+    indent.format('''
+@NonNull private static ArrayList<Object> wrapError(@NonNull Throwable exception) {
+\tArrayList<Object> errorList = new ArrayList<>(3);
+\terrorList.add(exception.toString());
+\terrorList.add(exception.getClass().getSimpleName());
+\terrorList.add("Cause: " + exception.getCause() + ", Stacktrace: " + Log.getStackTraceString(exception));
+\treturn errorList;
+}''');
+  }
+
+  @override
+  void writeGeneralUtilities(
+      JavaOptions generatorOptions, Root root, Indent indent) {
+    _writeWrapError(indent);
+  }
+
+  @override
+  void writeCloseNamespace(
+      JavaOptions generatorOptions, Root root, Indent indent) {
+    indent.dec();
+    indent.addln('}');
+  }
 }
 
+/// Calculates the name of the codec that will be generated for [api].
+String _getCodecName(Api api) => '${api.name}Codec';
+
+/// Converts an expression that evaluates to an nullable int to an expression
+/// that evaluates to a nullable enum.
+String _intToEnum(String expression, String enumName) =>
+    '$expression == null ? null : $enumName.values()[(int)$expression]';
+
 String _getArgumentName(int count, NamedType argument) =>
     argument.name.isEmpty ? 'arg$count' : argument.name;
 
@@ -358,106 +759,6 @@
 String _getSafeArgumentName(int count, NamedType argument) =>
     '${_getArgumentName(count, argument)}Arg';
 
-/// Writes the code for a flutter [Api], [api].
-/// Example:
-/// public static class Foo {
-///   public Foo(BinaryMessenger argBinaryMessenger) {...}
-///   public interface Reply<T> {
-///     void reply(T reply);
-///   }
-///   public int add(int x, int y, Reply<int> callback) {...}
-/// }
-void _writeFlutterApi(Indent indent, Api api, Root root) {
-  assert(api.location == ApiLocation.flutter);
-  const List<String> generatedMessages = <String>[
-    ' Generated class from Pigeon that represents Flutter messages that can be called from Java.'
-  ];
-  addDocumentationComments(indent, api.documentationComments, _docCommentSpec,
-      generatorComments: generatedMessages);
-
-  indent.write('public static class ${api.name} ');
-  indent.scoped('{', '}', () {
-    indent.writeln('private final BinaryMessenger binaryMessenger;');
-    indent.write('public ${api.name}(BinaryMessenger argBinaryMessenger)');
-    indent.scoped('{', '}', () {
-      indent.writeln('this.binaryMessenger = argBinaryMessenger;');
-    });
-    indent.write('public interface Reply<T> ');
-    indent.scoped('{', '}', () {
-      indent.writeln('void reply(T reply);');
-    });
-    final String codecName = _getCodecName(api);
-    indent.writeln('/** The codec used by ${api.name}. */');
-    indent.write('static MessageCodec<Object> getCodec() ');
-    indent.scoped('{', '}', () {
-      indent.write('return ');
-      if (getCodecClasses(api, root).isNotEmpty) {
-        indent.writeln('$codecName.INSTANCE;');
-      } else {
-        indent.writeln('new $_standardMessageCodec();');
-      }
-    });
-
-    for (final Method func in api.methods) {
-      final String channelName = makeChannelName(api, func);
-      final String returnType = func.returnType.isVoid
-          ? 'Void'
-          : _javaTypeForDartType(func.returnType);
-      String sendArgument;
-      addDocumentationComments(
-          indent, func.documentationComments, _docCommentSpec);
-      if (func.arguments.isEmpty) {
-        indent.write('public void ${func.name}(Reply<$returnType> callback) ');
-        sendArgument = 'null';
-      } else {
-        final Iterable<String> argTypes = func.arguments
-            .map((NamedType e) => _nullsafeJavaTypeForDartType(e.type));
-        final Iterable<String> argNames =
-            indexMap(func.arguments, _getSafeArgumentName);
-        if (func.arguments.length == 1) {
-          sendArgument =
-              'new ArrayList<Object>(Collections.singletonList(${argNames.first}))';
-        } else {
-          sendArgument =
-              'new ArrayList<Object>(Arrays.asList(${argNames.join(', ')}))';
-        }
-        final String argsSignature =
-            map2(argTypes, argNames, (String x, String y) => '$x $y')
-                .join(', ');
-        indent.write(
-            'public void ${func.name}($argsSignature, Reply<$returnType> callback) ');
-      }
-      indent.scoped('{', '}', () {
-        const String channel = 'channel';
-        indent.writeln('BasicMessageChannel<Object> $channel =');
-        indent.inc();
-        indent.inc();
-        indent.writeln(
-            'new BasicMessageChannel<>(binaryMessenger, "$channelName", getCodec());');
-        indent.dec();
-        indent.dec();
-        indent.write('$channel.send($sendArgument, channelReply -> ');
-        indent.scoped('{', '});', () {
-          if (func.returnType.isVoid) {
-            indent.writeln('callback.reply(null);');
-          } else {
-            const String output = 'output';
-            indent.writeln('@SuppressWarnings("ConstantConditions")');
-            if (func.returnType.baseName == 'int') {
-              indent.writeln(
-                  '$returnType $output = channelReply == null ? null : ((Number)channelReply).longValue();');
-            } else {
-              indent
-                  .writeln('$returnType $output = ($returnType)channelReply;');
-            }
-            indent.writeln('callback.reply($output);');
-          }
-        });
-      });
-    }
-  });
-}
-
 String _makeGetter(NamedType field) {
   final String uppercased =
       field.name.substring(0, 1).toUpperCase() + field.name.substring(1);
@@ -535,279 +836,3 @@
     return '(${hostDatatype.datatype})$varName';
   }
 }
-
-/// Generates the ".java" file for the AST represented by [root] to [sink] with the
-/// provided [options].
-void generateJava(JavaOptions options, Root root, StringSink sink) {
-  final Set<String> rootClassNameSet =
-      root.classes.map((Class x) => x.name).toSet();
-  final Set<String> rootEnumNameSet =
-      root.enums.map((Enum x) => x.name).toSet();
-  final Indent indent = Indent(sink);
-
-  void writeHeader() {
-    if (options.copyrightHeader != null) {
-      addLines(indent, options.copyrightHeader!, linePrefix: '// ');
-    }
-    indent.writeln('// $generatedCodeWarning');
-    indent.writeln('// $seeAlsoWarning');
-  }
-
-  void writeImports() {
-    indent.writeln('import android.util.Log;');
-    indent.writeln('import androidx.annotation.NonNull;');
-    indent.writeln('import androidx.annotation.Nullable;');
-    indent.writeln('import io.flutter.plugin.common.BasicMessageChannel;');
-    indent.writeln('import io.flutter.plugin.common.BinaryMessenger;');
-    indent.writeln('import io.flutter.plugin.common.MessageCodec;');
-    indent.writeln('import io.flutter.plugin.common.StandardMessageCodec;');
-    indent.writeln('import java.io.ByteArrayOutputStream;');
-    indent.writeln('import java.nio.ByteBuffer;');
-    indent.writeln('import java.util.Arrays;');
-    indent.writeln('import java.util.ArrayList;');
-    indent.writeln('import java.util.Collections;');
-    indent.writeln('import java.util.List;');
-    indent.writeln('import java.util.Map;');
-    indent.writeln('import java.util.HashMap;');
-  }
-
-  String camelToSnake(String camelCase) {
-    final RegExp regex = RegExp('([a-z])([A-Z]+)');
-    return camelCase
-        .replaceAllMapped(regex, (Match m) => '${m[1]}_${m[2]}')
-        .toUpperCase();
-  }
-
-  void writeEnum(Enum anEnum) {
-    addDocumentationComments(
-        indent, anEnum.documentationComments, _docCommentSpec);
-
-    indent.write('public enum ${anEnum.name} ');
-    indent.scoped('{', '}', () {
-      enumerate(anEnum.members, (int index, final EnumMember member) {
-        addDocumentationComments(
-            indent, member.documentationComments, _docCommentSpec);
-        indent.writeln(
-            '${camelToSnake(member.name)}($index)${index == anEnum.members.length - 1 ? ';' : ','}');
-      });
-      indent.writeln('');
-      indent.writeln('private final int index;');
-      indent.write('private ${anEnum.name}(final int index) ');
-      indent.scoped('{', '}', () {
-        indent.writeln('this.index = index;');
-      });
-    });
-  }
-
-  void writeDataClass(Class klass) {
-    void writeField(NamedType field) {
-      final HostDatatype hostDatatype = getFieldHostDatatype(
-          field,
-          root.classes,
-          root.enums,
-          (TypeDeclaration x) => _javaTypeForBuiltinDartType(x));
-      final String nullability =
-          field.type.isNullable ? '@Nullable' : '@NonNull';
-      addDocumentationComments(
-          indent, field.documentationComments, _docCommentSpec);
-
-      indent.writeln(
-          'private $nullability ${hostDatatype.datatype} ${field.name};');
-      indent.writeln(
-          'public $nullability ${hostDatatype.datatype} ${_makeGetter(field)}() { return ${field.name}; }');
-      indent.writeScoped(
-          'public void ${_makeSetter(field)}($nullability ${hostDatatype.datatype} setterArg) {',
-          '}', () {
-        if (!field.type.isNullable) {
-          indent.writeScoped('if (setterArg == null) {', '}', () {
-            indent.writeln(
-                'throw new IllegalStateException("Nonnull field \\"${field.name}\\" is null.");');
-          });
-        }
-        indent.writeln('this.${field.name} = setterArg;');
-      });
-    }
-
-    void writeToList() {
-      indent.write('@NonNull ArrayList<Object> toList() ');
-      indent.scoped('{', '}', () {
-        indent.writeln(
-            'ArrayList<Object> toListResult = new ArrayList<Object>(${klass.fields.length});');
-        for (final NamedType field in getFieldsInSerializationOrder(klass)) {
-          final HostDatatype hostDatatype = getFieldHostDatatype(
-              field,
-              root.classes,
-              root.enums,
-              (TypeDeclaration x) => _javaTypeForBuiltinDartType(x));
-          String toWriteValue = '';
-          final String fieldName = field.name;
-          if (!hostDatatype.isBuiltin &&
-              rootClassNameSet.contains(field.type.baseName)) {
-            toWriteValue = '($fieldName == null) ? null : $fieldName.toList()';
-          } else if (!hostDatatype.isBuiltin &&
-              rootEnumNameSet.contains(field.type.baseName)) {
-            toWriteValue = '$fieldName == null ? null : $fieldName.index';
-          } else {
-            toWriteValue = field.name;
-          }
-          indent.writeln('toListResult.add($toWriteValue);');
-        }
-        indent.writeln('return toListResult;');
-      });
-    }
-
-    void writeFromList() {
-      indent.write(
-          'static @NonNull ${klass.name} fromList(@NonNull ArrayList<Object> list) ');
-      indent.scoped('{', '}', () {
-        const String result = 'pigeonResult';
-        indent.writeln('${klass.name} $result = new ${klass.name}();');
-        enumerate(getFieldsInSerializationOrder(klass),
-            (int index, final NamedType field) {
-          final String fieldVariable = field.name;
-          final String setter = _makeSetter(field);
-          indent.writeln('Object $fieldVariable = list.get($index);');
-          if (rootEnumNameSet.contains(field.type.baseName)) {
-            indent.writeln(
-                '$result.$setter(${_intToEnum(fieldVariable, field.type.baseName)});');
-          } else {
-            indent.writeln(
-                '$result.$setter(${_castObject(field, root.classes, root.enums, fieldVariable)});');
-          }
-        });
-        indent.writeln('return $result;');
-      });
-    }
-
-    void writeBuilder() {
-      indent.write('public static final class Builder ');
-      indent.scoped('{', '}', () {
-        for (final NamedType field in getFieldsInSerializationOrder(klass)) {
-          final HostDatatype hostDatatype = getFieldHostDatatype(
-              field,
-              root.classes,
-              root.enums,
-              (TypeDeclaration x) => _javaTypeForBuiltinDartType(x));
-          final String nullability =
-              field.type.isNullable ? '@Nullable' : '@NonNull';
-          indent.writeln(
-              'private @Nullable ${hostDatatype.datatype} ${field.name};');
-          indent.writeScoped(
-              'public @NonNull Builder ${_makeSetter(field)}($nullability ${hostDatatype.datatype} setterArg) {',
-              '}', () {
-            indent.writeln('this.${field.name} = setterArg;');
-            indent.writeln('return this;');
-          });
-        }
-        indent.write('public @NonNull ${klass.name} build() ');
-        indent.scoped('{', '}', () {
-          const String returnVal = 'pigeonReturn';
-          indent.writeln('${klass.name} $returnVal = new ${klass.name}();');
-          for (final NamedType field in getFieldsInSerializationOrder(klass)) {
-            indent.writeln('$returnVal.${_makeSetter(field)}(${field.name});');
-          }
-          indent.writeln('return $returnVal;');
-        });
-      });
-    }
-
-    const List<String> generatedMessages = <String>[
-      ' Generated class from Pigeon that represents data sent in messages.'
-    ];
-    addDocumentationComments(
-        indent, klass.documentationComments, _docCommentSpec,
-        generatorComments: generatedMessages);
-
-    indent.write('public static class ${klass.name} ');
-    indent.scoped('{', '}', () {
-      for (final NamedType field in getFieldsInSerializationOrder(klass)) {
-        writeField(field);
-        indent.addln('');
-      }
-
-      if (getFieldsInSerializationOrder(klass)
-          .map((NamedType e) => !e.type.isNullable)
-          .any((bool e) => e)) {
-        indent.writeln(
-            '${_docCommentPrefix}Constructor is private to enforce null safety; use Builder.$_docCommentSuffix');
-        indent.writeln('private ${klass.name}() {}');
-      }
-
-      writeBuilder();
-      writeToList();
-      writeFromList();
-    });
-  }
-
-  void writeResultInterface() {
-    indent.write('public interface Result<T> ');
-    indent.scoped('{', '}', () {
-      indent.writeln('void success(T result);');
-      indent.writeln('void error(Throwable error);');
-    });
-  }
-
-  void writeApi(Api api) {
-    if (api.location == ApiLocation.host) {
-      _writeHostApi(indent, api, root);
-    } else if (api.location == ApiLocation.flutter) {
-      _writeFlutterApi(indent, api, root);
-    }
-  }
-
-  void writeWrapError() {
-    indent.format('''
-@NonNull private static ArrayList<Object> wrapError(@NonNull Throwable exception) {
-\tArrayList<Object> errorList = new ArrayList<>(3);
-\terrorList.add(exception.toString());
-\terrorList.add(exception.getClass().getSimpleName());
-\terrorList.add("Cause: " + exception.getCause() + ", Stacktrace: " + Log.getStackTraceString(exception));
-\treturn errorList;
-}''');
-  }
-
-  writeHeader();
-  indent.addln('');
-  if (options.package != null) {
-    indent.writeln('package ${options.package};');
-  }
-  indent.addln('');
-  writeImports();
-  indent.addln('');
-  indent.writeln(
-      '$_docCommentPrefix Generated class from Pigeon.$_docCommentSuffix');
-  indent.writeln(
-      '@SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression"})');
-  if (options.useGeneratedAnnotation ?? false) {
-    indent.writeln('@javax.annotation.Generated("dev.flutter.pigeon")');
-  }
-  indent.write('public class ${options.className!} ');
-  indent.scoped('{', '}', () {
-    for (final Enum anEnum in root.enums) {
-      indent.writeln('');
-      writeEnum(anEnum);
-    }
-
-    for (final Class klass in root.classes) {
-      indent.addln('');
-      writeDataClass(klass);
-    }
-
-    if (root.apis.any((Api api) =>
-        api.location == ApiLocation.host &&
-        api.methods.any((Method it) => it.isAsynchronous))) {
-      indent.addln('');
-      writeResultInterface();
-    }
-
-    for (final Api api in root.apis) {
-      if (getCodecClasses(api, root).isNotEmpty) {
-        _writeCodec(indent, api, root);
-        indent.addln('');
-      }
-      writeApi(api);
-    }
-
-    writeWrapError();
-  });
-}
diff --git a/packages/pigeon/lib/kotlin_generator.dart b/packages/pigeon/lib/kotlin_generator.dart
index 18e08c7..76b270b 100644
--- a/packages/pigeon/lib/kotlin_generator.dart
+++ b/packages/pigeon/lib/kotlin_generator.dart
@@ -66,220 +66,574 @@
 }
 
 /// Class that manages all Kotlin code generation.
-class KotlinGenerator extends Generator<KotlinOptions> {
+class KotlinGenerator extends StructuredGenerator<KotlinOptions> {
   /// Instantiates a Kotlin Generator.
-  KotlinGenerator();
+  const KotlinGenerator();
 
-  /// Generates Kotlin files with specified [KotlinOptions]
   @override
-  void generate(KotlinOptions languageOptions, Root root, StringSink sink,
-      {FileType fileType = FileType.na}) {
-    assert(fileType == FileType.na);
-    generateKotlin(languageOptions, root, sink);
+  void writeFilePrologue(
+      KotlinOptions generatorOptions, Root root, Indent indent) {
+    if (generatorOptions.copyrightHeader != null) {
+      addLines(indent, generatorOptions.copyrightHeader!, linePrefix: '// ');
+    }
+    indent.writeln('// $generatedCodeWarning');
+    indent.writeln('// $seeAlsoWarning');
   }
+
+  @override
+  void writeFileImports(
+      KotlinOptions generatorOptions, Root root, Indent indent) {
+    indent.addln('');
+    if (generatorOptions.package != null) {
+      indent.writeln('package ${generatorOptions.package}');
+    }
+    indent.addln('');
+    indent.writeln('import android.util.Log');
+    indent.writeln('import io.flutter.plugin.common.BasicMessageChannel');
+    indent.writeln('import io.flutter.plugin.common.BinaryMessenger');
+    indent.writeln('import io.flutter.plugin.common.MessageCodec');
+    indent.writeln('import io.flutter.plugin.common.StandardMessageCodec');
+    indent.writeln('import java.io.ByteArrayOutputStream');
+    indent.writeln('import java.nio.ByteBuffer');
+  }
+
+  @override
+  void writeEnum(
+      KotlinOptions generatorOptions, Root root, Indent indent, Enum anEnum) {
+    indent.writeln('');
+    addDocumentationComments(
+        indent, anEnum.documentationComments, _docCommentSpec);
+    indent.write('enum class ${anEnum.name}(val raw: Int) ');
+    indent.scoped('{', '}', () {
+      enumerate(anEnum.members, (int index, final EnumMember member) {
+        addDocumentationComments(
+            indent, member.documentationComments, _docCommentSpec);
+        indent.write('${member.name.toUpperCase()}($index)');
+        if (index != anEnum.members.length - 1) {
+          indent.addln(',');
+        } else {
+          indent.addln(';');
+        }
+      });
+
+      indent.writeln('');
+      indent.write('companion object ');
+      indent.scoped('{', '}', () {
+        indent.write('fun ofRaw(raw: Int): ${anEnum.name}? ');
+        indent.scoped('{', '}', () {
+          indent.writeln('return values().firstOrNull { it.raw == raw }');
+        });
+      });
+    });
+  }
+
+  @override
+  void writeDataClass(
+      KotlinOptions generatorOptions, Root root, Indent indent, Class klass) {
+    final Set<String> customClassNames =
+        root.classes.map((Class x) => x.name).toSet();
+    final Set<String> customEnumNames =
+        root.enums.map((Enum x) => x.name).toSet();
+
+    const List<String> generatedMessages = <String>[
+      ' Generated class from Pigeon that represents data sent in messages.'
+    ];
+    indent.addln('');
+    addDocumentationComments(
+        indent, klass.documentationComments, _docCommentSpec,
+        generatorComments: generatedMessages);
+
+    indent.write('data class ${klass.name} ');
+    indent.scoped('(', '', () {
+      for (final NamedType element in getFieldsInSerializationOrder(klass)) {
+        _writeClassField(indent, element);
+        if (getFieldsInSerializationOrder(klass).last != element) {
+          indent.addln(',');
+        } else {
+          indent.addln('');
+        }
+      }
+    });
+
+    indent.scoped(') {', '}', () {
+      writeClassDecode(generatorOptions, root, indent, klass, customClassNames,
+          customEnumNames);
+      writeClassEncode(generatorOptions, root, indent, klass, customClassNames,
+          customEnumNames);
+    });
+  }
+
+  @override
+  void writeClassEncode(
+    KotlinOptions generatorOptions,
+    Root root,
+    Indent indent,
+    Class klass,
+    Set<String> customClassNames,
+    Set<String> customEnumNames,
+  ) {
+    indent.write('fun toList(): List<Any?> ');
+    indent.scoped('{', '}', () {
+      indent.write('return listOf<Any?>');
+      indent.scoped('(', ')', () {
+        for (final NamedType field in getFieldsInSerializationOrder(klass)) {
+          final HostDatatype hostDatatype = _getHostDatatype(root, field);
+          String toWriteValue = '';
+          final String fieldName = field.name;
+          if (!hostDatatype.isBuiltin &&
+              customClassNames.contains(field.type.baseName)) {
+            toWriteValue = '$fieldName?.toList()';
+          } else if (!hostDatatype.isBuiltin &&
+              customEnumNames.contains(field.type.baseName)) {
+            toWriteValue = '$fieldName?.raw';
+          } else {
+            toWriteValue = fieldName;
+          }
+          indent.writeln('$toWriteValue,');
+        }
+      });
+    });
+  }
+
+  @override
+  void writeClassDecode(
+    KotlinOptions generatorOptions,
+    Root root,
+    Indent indent,
+    Class klass,
+    Set<String> customClassNames,
+    Set<String> customEnumNames,
+  ) {
+    final String className = klass.name;
+
+    indent.write('companion object ');
+    indent.scoped('{', '}', () {
+      indent.writeln('@Suppress("UNCHECKED_CAST")');
+      indent.write('fun fromList(list: List<Any?>): $className ');
+
+      indent.scoped('{', '}', () {
+        enumerate(getFieldsInSerializationOrder(klass),
+            (int index, final NamedType field) {
+          final HostDatatype hostDatatype = _getHostDatatype(root, field);
+
+          // The StandardMessageCodec can give us [Integer, Long] for
+          // a Dart 'int'.  To keep things simple we just use 64bit
+          // longs in Pigeon with Kotlin.
+          final bool isInt = field.type.baseName == 'int';
+
+          final String listValue = 'list[$index]';
+          final String fieldType = _kotlinTypeForDartType(field.type);
+
+          if (field.type.isNullable) {
+            if (!hostDatatype.isBuiltin &&
+                customClassNames.contains(field.type.baseName)) {
+              indent.write('val ${field.name}: $fieldType? = ');
+              indent.add('($listValue as? List<Any?>)?.let ');
+              indent.scoped('{', '}', () {
+                indent.writeln('$fieldType.fromList(it)');
+              });
+            } else if (!hostDatatype.isBuiltin &&
+                customEnumNames.contains(field.type.baseName)) {
+              indent.write('val ${field.name}: $fieldType? = ');
+              indent.add('($listValue as? Int)?.let ');
+              indent.scoped('{', '}', () {
+                indent.writeln('$fieldType.ofRaw(it)');
+              });
+            } else if (isInt) {
+              indent.write('val ${field.name} = $listValue');
+              indent.addln(
+                  '.let { if (it is Int) it.toLong() else it as? Long }');
+            } else {
+              indent.writeln('val ${field.name} = $listValue as? $fieldType');
+            }
+          } else {
+            if (!hostDatatype.isBuiltin &&
+                customClassNames.contains(field.type.baseName)) {
+              indent.writeln(
+                  'val ${field.name} = $fieldType.fromList($listValue as List<Any?>)');
+            } else if (!hostDatatype.isBuiltin &&
+                customEnumNames.contains(field.type.baseName)) {
+              indent.write(
+                  'val ${field.name} = $fieldType.ofRaw($listValue as Int)!!');
+            } else {
+              indent.writeln('val ${field.name} = $listValue as $fieldType');
+            }
+          }
+        });
+
+        indent.writeln('');
+        indent.write('return $className(');
+        for (final NamedType field in getFieldsInSerializationOrder(klass)) {
+          final String comma =
+              getFieldsInSerializationOrder(klass).last == field ? '' : ', ';
+          indent.add('${field.name}$comma');
+        }
+        indent.addln(')');
+      });
+    });
+  }
+
+  void _writeClassField(Indent indent, NamedType field) {
+    addDocumentationComments(
+        indent, field.documentationComments, _docCommentSpec);
+    indent.write(
+        'val ${field.name}: ${_nullsafeKotlinTypeForDartType(field.type)}');
+    final String defaultNil = field.type.isNullable ? ' = null' : '';
+    indent.add(defaultNil);
+  }
+
+  @override
+  void writeApis(
+    KotlinOptions generatorOptions,
+    Root root,
+    Indent indent,
+  ) {
+    if (root.apis.any((Api api) =>
+        api.location == ApiLocation.host &&
+        api.methods.any((Method it) => it.isAsynchronous))) {
+      indent.addln('');
+    }
+    super.writeApis(generatorOptions, root, indent);
+  }
+
+  /// Writes the code for a flutter [Api], [api].
+  /// Example:
+  /// class Foo(private val binaryMessenger: BinaryMessenger) {
+  ///   fun add(x: Int, y: Int, callback: (Int?) -> Unit) {...}
+  /// }
+  @override
+  void writeFlutterApi(
+    KotlinOptions generatorOptions,
+    Root root,
+    Indent indent,
+    Api api,
+  ) {
+    assert(api.location == ApiLocation.flutter);
+    final bool isCustomCodec = getCodecClasses(api, root).isNotEmpty;
+    if (isCustomCodec) {
+      _writeCodec(indent, api, root);
+    }
+
+    const List<String> generatedMessages = <String>[
+      ' Generated class from Pigeon that represents Flutter messages that can be called from Kotlin.'
+    ];
+    addDocumentationComments(indent, api.documentationComments, _docCommentSpec,
+        generatorComments: generatedMessages);
+
+    final String apiName = api.name;
+    indent.writeln('@Suppress("UNCHECKED_CAST")');
+    indent
+        .write('class $apiName(private val binaryMessenger: BinaryMessenger) ');
+    indent.scoped('{', '}', () {
+      indent.write('companion object ');
+      indent.scoped('{', '}', () {
+        indent.writeln('/** The codec used by $apiName. */');
+        indent.write('val codec: MessageCodec<Any?> by lazy ');
+        indent.scoped('{', '}', () {
+          if (isCustomCodec) {
+            indent.writeln(_getCodecName(api));
+          } else {
+            indent.writeln('StandardMessageCodec()');
+          }
+        });
+      });
+
+      for (final Method func in api.methods) {
+        final String channelName = makeChannelName(api, func);
+        final String returnType = func.returnType.isVoid
+            ? ''
+            : _nullsafeKotlinTypeForDartType(func.returnType);
+        String sendArgument;
+
+        addDocumentationComments(
+            indent, func.documentationComments, _docCommentSpec);
+
+        if (func.arguments.isEmpty) {
+          indent.write('fun ${func.name}(callback: ($returnType) -> Unit) ');
+          sendArgument = 'null';
+        } else {
+          final Iterable<String> argTypes = func.arguments
+              .map((NamedType e) => _nullsafeKotlinTypeForDartType(e.type));
+          final Iterable<String> argNames =
+              indexMap(func.arguments, _getSafeArgumentName);
+          sendArgument = 'listOf(${argNames.join(', ')})';
+          final String argsSignature = map2(argTypes, argNames,
+              (String type, String name) => '$name: $type').join(', ');
+          if (func.returnType.isVoid) {
+            indent.write(
+                'fun ${func.name}($argsSignature, callback: () -> Unit) ');
+          } else {
+            indent.write(
+                'fun ${func.name}($argsSignature, callback: ($returnType) -> Unit) ');
+          }
+        }
+        indent.scoped('{', '}', () {
+          const String channel = 'channel';
+          indent.writeln(
+              'val $channel = BasicMessageChannel<Any?>(binaryMessenger, "$channelName", codec)');
+          indent.write('$channel.send($sendArgument) ');
+          if (func.returnType.isVoid) {
+            indent.scoped('{', '}', () {
+              indent.writeln('callback()');
+            });
+          } else {
+            final String forceUnwrap = func.returnType.isNullable ? '?' : '';
+            indent.scoped('{', '}', () {
+              indent.writeln('val result = it as$forceUnwrap $returnType');
+              indent.writeln('callback(result)');
+            });
+          }
+        });
+      }
+    });
+  }
+
+  /// Write the kotlin code that represents a host [Api], [api].
+  /// Example:
+  /// interface Foo {
+  ///   Int add(x: Int, y: Int);
+  ///   companion object {
+  ///     fun setUp(binaryMessenger: BinaryMessenger, api: Api) {...}
+  ///   }
+  /// }
+  @override
+  void writeHostApi(
+    KotlinOptions generatorOptions,
+    Root root,
+    Indent indent,
+    Api api,
+  ) {
+    assert(api.location == ApiLocation.host);
+
+    final String apiName = api.name;
+
+    final bool isCustomCodec = getCodecClasses(api, root).isNotEmpty;
+    if (isCustomCodec) {
+      _writeCodec(indent, api, root);
+    }
+
+    const List<String> generatedMessages = <String>[
+      ' Generated interface from Pigeon that represents a handler of messages from Flutter.'
+    ];
+    addDocumentationComments(indent, api.documentationComments, _docCommentSpec,
+        generatorComments: generatedMessages);
+
+    indent.write('interface $apiName ');
+    indent.scoped('{', '}', () {
+      for (final Method method in api.methods) {
+        final List<String> argSignature = <String>[];
+        if (method.arguments.isNotEmpty) {
+          final Iterable<String> argTypes = method.arguments
+              .map((NamedType e) => _nullsafeKotlinTypeForDartType(e.type));
+          final Iterable<String> argNames =
+              method.arguments.map((NamedType e) => e.name);
+          argSignature.addAll(
+              map2(argTypes, argNames, (String argType, String argName) {
+            return '$argName: $argType';
+          }));
+        }
+
+        final String returnType = method.returnType.isVoid
+            ? ''
+            : _nullsafeKotlinTypeForDartType(method.returnType);
+
+        addDocumentationComments(
+            indent, method.documentationComments, _docCommentSpec);
+
+        if (method.isAsynchronous) {
+          argSignature.add('callback: ($returnType) -> Unit');
+          indent.writeln('fun ${method.name}(${argSignature.join(', ')})');
+        } else if (method.returnType.isVoid) {
+          indent.writeln('fun ${method.name}(${argSignature.join(', ')})');
+        } else {
+          indent.writeln(
+              'fun ${method.name}(${argSignature.join(', ')}): $returnType');
+        }
+      }
+
+      indent.addln('');
+      indent.write('companion object ');
+      indent.scoped('{', '}', () {
+        indent.writeln('/** The codec used by $apiName. */');
+        indent.write('val codec: MessageCodec<Any?> by lazy ');
+        indent.scoped('{', '}', () {
+          if (isCustomCodec) {
+            indent.writeln(_getCodecName(api));
+          } else {
+            indent.writeln('StandardMessageCodec()');
+          }
+        });
+        indent.writeln(
+            '/** Sets up an instance of `$apiName` to handle messages through the `binaryMessenger`. */');
+        indent.writeln('@Suppress("UNCHECKED_CAST")');
+        indent.write(
+            'fun setUp(binaryMessenger: BinaryMessenger, api: $apiName?) ');
+        indent.scoped('{', '}', () {
+          for (final Method method in api.methods) {
+            indent.write('run ');
+            indent.scoped('{', '}', () {
+              String? taskQueue;
+              if (method.taskQueueType != TaskQueueType.serial) {
+                taskQueue = 'taskQueue';
+                indent.writeln(
+                    'val $taskQueue = binaryMessenger.makeBackgroundTaskQueue()');
+              }
+
+              final String channelName = makeChannelName(api, method);
+
+              indent.write(
+                  'val channel = BasicMessageChannel<Any?>(binaryMessenger, "$channelName", codec');
+
+              if (taskQueue != null) {
+                indent.addln(', $taskQueue)');
+              } else {
+                indent.addln(')');
+              }
+
+              indent.write('if (api != null) ');
+              indent.scoped('{', '}', () {
+                final String messageVarName =
+                    method.arguments.isNotEmpty ? 'message' : '_';
+
+                indent.write('channel.setMessageHandler ');
+                indent.scoped('{ $messageVarName, reply ->', '}', () {
+                  indent.writeln('var wrapped = listOf<Any?>()');
+                  indent.write('try ');
+                  indent.scoped('{', '}', () {
+                    final List<String> methodArgument = <String>[];
+                    if (method.arguments.isNotEmpty) {
+                      indent.writeln('val args = message as List<Any?>');
+                      enumerate(method.arguments, (int index, NamedType arg) {
+                        final String argName = _getSafeArgumentName(index, arg);
+                        final String argIndex = 'args[$index]';
+                        indent.writeln(
+                            'val $argName = ${_castForceUnwrap(argIndex, arg.type, root)}');
+                        methodArgument.add(argName);
+                      });
+                    }
+                    final String call =
+                        'api.${method.name}(${methodArgument.join(', ')})';
+                    if (method.isAsynchronous) {
+                      indent.write('$call ');
+                      final String resultValue =
+                          method.returnType.isVoid ? 'null' : 'it';
+                      indent.scoped('{', '}', () {
+                        indent.writeln('reply.reply(wrapResult($resultValue))');
+                      });
+                    } else if (method.returnType.isVoid) {
+                      indent.writeln(call);
+                      indent.writeln('wrapped = listOf<Any?>(null)');
+                    } else {
+                      indent.writeln('wrapped = listOf<Any?>($call)');
+                    }
+                  }, addTrailingNewline: false);
+                  indent.add(' catch (exception: Error) ');
+                  indent.scoped('{', '}', () {
+                    indent.writeln('wrapped = wrapError(exception)');
+                    if (method.isAsynchronous) {
+                      indent.writeln('reply.reply(wrapped)');
+                    }
+                  });
+                  if (!method.isAsynchronous) {
+                    indent.writeln('reply.reply(wrapped)');
+                  }
+                });
+              }, addTrailingNewline: false);
+              indent.scoped(' else {', '}', () {
+                indent.writeln('channel.setMessageHandler(null)');
+              });
+            });
+          }
+        });
+      });
+    });
+  }
+
+  /// Writes the codec class that will be used by [api].
+  /// Example:
+  /// private static class FooCodec extends StandardMessageCodec {...}
+  void _writeCodec(Indent indent, Api api, Root root) {
+    assert(getCodecClasses(api, root).isNotEmpty);
+    final Iterable<EnumeratedClass> codecClasses = getCodecClasses(api, root);
+    final String codecName = _getCodecName(api);
+    indent.writeln('@Suppress("UNCHECKED_CAST")');
+    indent.write('private object $codecName : StandardMessageCodec() ');
+    indent.scoped('{', '}', () {
+      indent.write(
+          'override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? ');
+      indent.scoped('{', '}', () {
+        indent.write('return when (type) ');
+        indent.scoped('{', '}', () {
+          for (final EnumeratedClass customClass in codecClasses) {
+            indent.write('${customClass.enumeration}.toByte() -> ');
+            indent.scoped('{', '}', () {
+              indent.write('return (readValue(buffer) as? List<Any?>)?.let ');
+              indent.scoped('{', '}', () {
+                indent.writeln('${customClass.name}.fromList(it)');
+              });
+            });
+          }
+          indent.writeln('else -> super.readValueOfType(type, buffer)');
+        });
+      });
+
+      indent.write(
+          'override fun writeValue(stream: ByteArrayOutputStream, value: Any?) ');
+      indent.writeScoped('{', '}', () {
+        indent.write('when (value) ');
+        indent.scoped('{', '}', () {
+          for (final EnumeratedClass customClass in codecClasses) {
+            indent.write('is ${customClass.name} -> ');
+            indent.scoped('{', '}', () {
+              indent.writeln('stream.write(${customClass.enumeration})');
+              indent.writeln('writeValue(stream, value.toList())');
+            });
+          }
+          indent.writeln('else -> super.writeValue(stream, value)');
+        });
+      });
+    });
+    indent.addln('');
+  }
+
+  void _writeWrapResult(Indent indent) {
+    indent.addln('');
+    indent.write('private fun wrapResult(result: Any?): List<Any?> ');
+    indent.scoped('{', '}', () {
+      indent.writeln('return listOf(result)');
+    });
+  }
+
+  void _writeWrapError(Indent indent) {
+    indent.addln('');
+    indent.write('private fun wrapError(exception: Throwable): List<Any> ');
+    indent.scoped('{', '}', () {
+      indent.write('return ');
+      indent.scoped('listOf<Any>(', ')', () {
+        indent.writeln('exception.javaClass.simpleName,');
+        indent.writeln('exception.toString(),');
+        indent.writeln(
+            '"Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception)');
+      });
+    });
+  }
+
+  @override
+  void writeGeneralUtilities(
+      KotlinOptions generatorOptions, Root root, Indent indent) {
+    _writeWrapResult(indent);
+    _writeWrapError(indent);
+  }
+}
+
+HostDatatype _getHostDatatype(Root root, NamedType field) {
+  return getFieldHostDatatype(field, root.classes, root.enums,
+      (TypeDeclaration x) => _kotlinTypeForBuiltinDartType(x));
 }
 
 /// Calculates the name of the codec that will be generated for [api].
 String _getCodecName(Api api) => '${api.name}Codec';
 
-/// Writes the codec class that will be used by [api].
-/// Example:
-/// private static class FooCodec extends StandardMessageCodec {...}
-void _writeCodec(Indent indent, Api api, Root root) {
-  assert(getCodecClasses(api, root).isNotEmpty);
-  final Iterable<EnumeratedClass> codecClasses = getCodecClasses(api, root);
-  final String codecName = _getCodecName(api);
-  indent.writeln('@Suppress("UNCHECKED_CAST")');
-  indent.write('private object $codecName : StandardMessageCodec() ');
-  indent.scoped('{', '}', () {
-    indent.write(
-        'override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? ');
-    indent.scoped('{', '}', () {
-      indent.write('return when (type) ');
-      indent.scoped('{', '}', () {
-        for (final EnumeratedClass customClass in codecClasses) {
-          indent.write('${customClass.enumeration}.toByte() -> ');
-          indent.scoped('{', '}', () {
-            indent.write('return (readValue(buffer) as? List<Any?>)?.let ');
-            indent.scoped('{', '}', () {
-              indent.writeln('${customClass.name}.fromList(it)');
-            });
-          });
-        }
-        indent.writeln('else -> super.readValueOfType(type, buffer)');
-      });
-    });
-
-    indent.write(
-        'override fun writeValue(stream: ByteArrayOutputStream, value: Any?) ');
-    indent.writeScoped('{', '}', () {
-      indent.write('when (value) ');
-      indent.scoped('{', '}', () {
-        for (final EnumeratedClass customClass in codecClasses) {
-          indent.write('is ${customClass.name} -> ');
-          indent.scoped('{', '}', () {
-            indent.writeln('stream.write(${customClass.enumeration})');
-            indent.writeln('writeValue(stream, value.toList())');
-          });
-        }
-        indent.writeln('else -> super.writeValue(stream, value)');
-      });
-    });
-  });
-}
-
-/// Write the kotlin code that represents a host [Api], [api].
-/// Example:
-/// interface Foo {
-///   Int add(x: Int, y: Int);
-///   companion object {
-///     fun setUp(binaryMessenger: BinaryMessenger, api: Api) {...}
-///   }
-/// }
-void _writeHostApi(Indent indent, Api api, Root root) {
-  assert(api.location == ApiLocation.host);
-
-  final String apiName = api.name;
-
-  final bool isCustomCodec = getCodecClasses(api, root).isNotEmpty;
-
-  const List<String> generatedMessages = <String>[
-    ' Generated interface from Pigeon that represents a handler of messages from Flutter.'
-  ];
-  addDocumentationComments(indent, api.documentationComments, _docCommentSpec,
-      generatorComments: generatedMessages);
-
-  indent.write('interface $apiName ');
-  indent.scoped('{', '}', () {
-    for (final Method method in api.methods) {
-      final List<String> argSignature = <String>[];
-      if (method.arguments.isNotEmpty) {
-        final Iterable<String> argTypes = method.arguments
-            .map((NamedType e) => _nullsafeKotlinTypeForDartType(e.type));
-        final Iterable<String> argNames =
-            method.arguments.map((NamedType e) => e.name);
-        argSignature
-            .addAll(map2(argTypes, argNames, (String argType, String argName) {
-          return '$argName: $argType';
-        }));
-      }
-
-      final String returnType = method.returnType.isVoid
-          ? ''
-          : _nullsafeKotlinTypeForDartType(method.returnType);
-
-      addDocumentationComments(
-          indent, method.documentationComments, _docCommentSpec);
-
-      if (method.isAsynchronous) {
-        argSignature.add('callback: ($returnType) -> Unit');
-        indent.writeln('fun ${method.name}(${argSignature.join(', ')})');
-      } else if (method.returnType.isVoid) {
-        indent.writeln('fun ${method.name}(${argSignature.join(', ')})');
-      } else {
-        indent.writeln(
-            'fun ${method.name}(${argSignature.join(', ')}): $returnType');
-      }
-    }
-
-    indent.addln('');
-    indent.write('companion object ');
-    indent.scoped('{', '}', () {
-      indent.writeln('/** The codec used by $apiName. */');
-      indent.write('val codec: MessageCodec<Any?> by lazy ');
-      indent.scoped('{', '}', () {
-        if (isCustomCodec) {
-          indent.writeln(_getCodecName(api));
-        } else {
-          indent.writeln('StandardMessageCodec()');
-        }
-      });
-      indent.writeln(
-          '/** Sets up an instance of `$apiName` to handle messages through the `binaryMessenger`. */');
-      indent.writeln('@Suppress("UNCHECKED_CAST")');
-      indent.write(
-          'fun setUp(binaryMessenger: BinaryMessenger, api: $apiName?) ');
-      indent.scoped('{', '}', () {
-        for (final Method method in api.methods) {
-          indent.write('run ');
-          indent.scoped('{', '}', () {
-            String? taskQueue;
-            if (method.taskQueueType != TaskQueueType.serial) {
-              taskQueue = 'taskQueue';
-              indent.writeln(
-                  'val $taskQueue = binaryMessenger.makeBackgroundTaskQueue()');
-            }
-
-            final String channelName = makeChannelName(api, method);
-
-            indent.write(
-                'val channel = BasicMessageChannel<Any?>(binaryMessenger, "$channelName", codec');
-
-            if (taskQueue != null) {
-              indent.addln(', $taskQueue)');
-            } else {
-              indent.addln(')');
-            }
-
-            indent.write('if (api != null) ');
-            indent.scoped('{', '}', () {
-              final String messageVarName =
-                  method.arguments.isNotEmpty ? 'message' : '_';
-
-              indent.write('channel.setMessageHandler ');
-              indent.scoped('{ $messageVarName, reply ->', '}', () {
-                indent.writeln('var wrapped = listOf<Any?>()');
-                indent.write('try ');
-                indent.scoped('{', '}', () {
-                  final List<String> methodArgument = <String>[];
-                  if (method.arguments.isNotEmpty) {
-                    indent.writeln('val args = message as List<Any?>');
-                    enumerate(method.arguments, (int index, NamedType arg) {
-                      final String argName = _getSafeArgumentName(index, arg);
-                      final String argIndex = 'args[$index]';
-                      indent.writeln(
-                          'val $argName = ${_castForceUnwrap(argIndex, arg.type, root)}');
-                      methodArgument.add(argName);
-                    });
-                  }
-                  final String call =
-                      'api.${method.name}(${methodArgument.join(', ')})';
-                  if (method.isAsynchronous) {
-                    indent.write('$call ');
-                    final String resultValue =
-                        method.returnType.isVoid ? 'null' : 'it';
-                    indent.scoped('{', '}', () {
-                      indent.writeln('reply.reply(wrapResult($resultValue))');
-                    });
-                  } else if (method.returnType.isVoid) {
-                    indent.writeln(call);
-                    indent.writeln('wrapped = listOf<Any?>(null)');
-                  } else {
-                    indent.writeln('wrapped = listOf<Any?>($call)');
-                  }
-                }, addTrailingNewline: false);
-                indent.add(' catch (exception: Error) ');
-                indent.scoped('{', '}', () {
-                  indent.writeln('wrapped = wrapError(exception)');
-                  if (method.isAsynchronous) {
-                    indent.writeln('reply.reply(wrapped)');
-                  }
-                });
-                if (!method.isAsynchronous) {
-                  indent.writeln('reply.reply(wrapped)');
-                }
-              });
-            }, addTrailingNewline: false);
-            indent.scoped(' else {', '}', () {
-              indent.writeln('channel.setMessageHandler(null)');
-            });
-          });
-        }
-      });
-    });
-  });
-}
-
 String _getArgumentName(int count, NamedType argument) =>
     argument.name.isEmpty ? 'arg$count' : argument.name;
 
@@ -287,88 +641,6 @@
 String _getSafeArgumentName(int count, NamedType argument) =>
     '${_getArgumentName(count, argument)}Arg';
 
-/// Writes the code for a flutter [Api], [api].
-/// Example:
-/// class Foo(private val binaryMessenger: BinaryMessenger) {
-///   fun add(x: Int, y: Int, callback: (Int?) -> Unit) {...}
-/// }
-void _writeFlutterApi(Indent indent, Api api, Root root) {
-  assert(api.location == ApiLocation.flutter);
-  final bool isCustomCodec = getCodecClasses(api, root).isNotEmpty;
-
-  const List<String> generatedMessages = <String>[
-    ' Generated class from Pigeon that represents Flutter messages that can be called from Kotlin.'
-  ];
-  addDocumentationComments(indent, api.documentationComments, _docCommentSpec,
-      generatorComments: generatedMessages);
-
-  final String apiName = api.name;
-  indent.writeln('@Suppress("UNCHECKED_CAST")');
-  indent.write('class $apiName(private val binaryMessenger: BinaryMessenger) ');
-  indent.scoped('{', '}', () {
-    indent.write('companion object ');
-    indent.scoped('{', '}', () {
-      indent.writeln('/** The codec used by $apiName. */');
-      indent.write('val codec: MessageCodec<Any?> by lazy ');
-      indent.scoped('{', '}', () {
-        if (isCustomCodec) {
-          indent.writeln(_getCodecName(api));
-        } else {
-          indent.writeln('StandardMessageCodec()');
-        }
-      });
-    });
-
-    for (final Method func in api.methods) {
-      final String channelName = makeChannelName(api, func);
-      final String returnType = func.returnType.isVoid
-          ? ''
-          : _nullsafeKotlinTypeForDartType(func.returnType);
-      String sendArgument;
-
-      addDocumentationComments(
-          indent, func.documentationComments, _docCommentSpec);
-
-      if (func.arguments.isEmpty) {
-        indent.write('fun ${func.name}(callback: ($returnType) -> Unit) ');
-        sendArgument = 'null';
-      } else {
-        final Iterable<String> argTypes = func.arguments
-            .map((NamedType e) => _nullsafeKotlinTypeForDartType(e.type));
-        final Iterable<String> argNames =
-            indexMap(func.arguments, _getSafeArgumentName);
-        sendArgument = 'listOf(${argNames.join(', ')})';
-        final String argsSignature = map2(argTypes, argNames,
-            (String type, String name) => '$name: $type').join(', ');
-        if (func.returnType.isVoid) {
-          indent
-              .write('fun ${func.name}($argsSignature, callback: () -> Unit) ');
-        } else {
-          indent.write(
-              'fun ${func.name}($argsSignature, callback: ($returnType) -> Unit) ');
-        }
-      }
-      indent.scoped('{', '}', () {
-        const String channel = 'channel';
-        indent.writeln(
-            'val $channel = BasicMessageChannel<Any?>(binaryMessenger, "$channelName", codec)');
-        indent.write('$channel.send($sendArgument) ');
-        if (func.returnType.isVoid) {
-          indent.scoped('{', '}', () {
-            indent.writeln('callback()');
-          });
-        } else {
-          final String forceUnwrap = func.returnType.isNullable ? '?' : '';
-          indent.scoped('{', '}', () {
-            indent.writeln('val result = it as$forceUnwrap $returnType');
-            indent.writeln('callback(result)');
-          });
-        }
-      });
-    }
-  });
-}
-
 String _castForceUnwrap(String value, TypeDeclaration type, Root root) {
   if (isEnum(root, type)) {
     final String forceUnwrap = type.isNullable ? '' : '!!';
@@ -448,258 +720,3 @@
   final String nullSafe = type.isNullable ? '?' : '';
   return '${_kotlinTypeForDartType(type)}$nullSafe';
 }
-
-/// Generates the ".kotlin" file for the AST represented by [root] to [sink] with the
-/// provided [options].
-void generateKotlin(KotlinOptions options, Root root, StringSink sink) {
-  final Set<String> rootClassNameSet =
-      root.classes.map((Class x) => x.name).toSet();
-  final Set<String> rootEnumNameSet =
-      root.enums.map((Enum x) => x.name).toSet();
-  final Indent indent = Indent(sink);
-
-  HostDatatype getHostDatatype(NamedType field) {
-    return getFieldHostDatatype(field, root.classes, root.enums,
-        (TypeDeclaration x) => _kotlinTypeForBuiltinDartType(x));
-  }
-
-  void writeHeader() {
-    if (options.copyrightHeader != null) {
-      addLines(indent, options.copyrightHeader!, linePrefix: '// ');
-    }
-    indent.writeln('// $generatedCodeWarning');
-    indent.writeln('// $seeAlsoWarning');
-  }
-
-  void writeImports() {
-    indent.writeln('import android.util.Log');
-    indent.writeln('import io.flutter.plugin.common.BasicMessageChannel');
-    indent.writeln('import io.flutter.plugin.common.BinaryMessenger');
-    indent.writeln('import io.flutter.plugin.common.MessageCodec');
-    indent.writeln('import io.flutter.plugin.common.StandardMessageCodec');
-    indent.writeln('import java.io.ByteArrayOutputStream');
-    indent.writeln('import java.nio.ByteBuffer');
-  }
-
-  void writeEnum(Enum anEnum) {
-    addDocumentationComments(
-        indent, anEnum.documentationComments, _docCommentSpec);
-    indent.write('enum class ${anEnum.name}(val raw: Int) ');
-    indent.scoped('{', '}', () {
-      enumerate(anEnum.members, (int index, final EnumMember member) {
-        addDocumentationComments(
-            indent, member.documentationComments, _docCommentSpec);
-        indent.write('${member.name.toUpperCase()}($index)');
-        if (index != anEnum.members.length - 1) {
-          indent.addln(',');
-        } else {
-          indent.addln(';');
-        }
-      });
-
-      indent.writeln('');
-      indent.write('companion object ');
-      indent.scoped('{', '}', () {
-        indent.write('fun ofRaw(raw: Int): ${anEnum.name}? ');
-        indent.scoped('{', '}', () {
-          indent.writeln('return values().firstOrNull { it.raw == raw }');
-        });
-      });
-    });
-  }
-
-  void writeDataClass(Class klass) {
-    void writeField(NamedType field) {
-      addDocumentationComments(
-          indent, field.documentationComments, _docCommentSpec);
-      indent.write(
-          'val ${field.name}: ${_nullsafeKotlinTypeForDartType(field.type)}');
-      final String defaultNil = field.type.isNullable ? ' = null' : '';
-      indent.add(defaultNil);
-    }
-
-    void writeToList() {
-      indent.write('fun toList(): List<Any?> ');
-      indent.scoped('{', '}', () {
-        indent.write('return listOf<Any?>');
-        indent.scoped('(', ')', () {
-          for (final NamedType field in getFieldsInSerializationOrder(klass)) {
-            final HostDatatype hostDatatype = getHostDatatype(field);
-            String toWriteValue = '';
-            final String fieldName = field.name;
-            if (!hostDatatype.isBuiltin &&
-                rootClassNameSet.contains(field.type.baseName)) {
-              toWriteValue = '$fieldName?.toList()';
-            } else if (!hostDatatype.isBuiltin &&
-                rootEnumNameSet.contains(field.type.baseName)) {
-              toWriteValue = '$fieldName?.raw';
-            } else {
-              toWriteValue = fieldName;
-            }
-            indent.writeln('$toWriteValue,');
-          }
-        });
-      });
-    }
-
-    void writeFromList() {
-      final String className = klass.name;
-
-      indent.write('companion object ');
-      indent.scoped('{', '}', () {
-        indent.writeln('@Suppress("UNCHECKED_CAST")');
-        indent.write('fun fromList(list: List<Any?>): $className ');
-
-        indent.scoped('{', '}', () {
-          enumerate(getFieldsInSerializationOrder(klass),
-              (int index, final NamedType field) {
-            final HostDatatype hostDatatype = getHostDatatype(field);
-
-            // The StandardMessageCodec can give us [Integer, Long] for
-            // a Dart 'int'.  To keep things simple we just use 64bit
-            // longs in Pigeon with Kotlin.
-            final bool isInt = field.type.baseName == 'int';
-
-            final String listValue = 'list[$index]';
-            final String fieldType = _kotlinTypeForDartType(field.type);
-
-            if (field.type.isNullable) {
-              if (!hostDatatype.isBuiltin &&
-                  rootClassNameSet.contains(field.type.baseName)) {
-                indent.write('val ${field.name}: $fieldType? = ');
-                indent.add('($listValue as? List<Any?>)?.let ');
-                indent.scoped('{', '}', () {
-                  indent.writeln('$fieldType.fromList(it)');
-                });
-              } else if (!hostDatatype.isBuiltin &&
-                  rootEnumNameSet.contains(field.type.baseName)) {
-                indent.write('val ${field.name}: $fieldType? = ');
-                indent.add('($listValue as? Int)?.let ');
-                indent.scoped('{', '}', () {
-                  indent.writeln('$fieldType.ofRaw(it)');
-                });
-              } else if (isInt) {
-                indent.write('val ${field.name} = $listValue');
-                indent.addln(
-                    '.let { if (it is Int) it.toLong() else it as? Long }');
-              } else {
-                indent.writeln('val ${field.name} = $listValue as? $fieldType');
-              }
-            } else {
-              if (!hostDatatype.isBuiltin &&
-                  rootClassNameSet.contains(field.type.baseName)) {
-                indent.writeln(
-                    'val ${field.name} = $fieldType.fromList($listValue as List<Any?>)');
-              } else if (!hostDatatype.isBuiltin &&
-                  rootEnumNameSet.contains(field.type.baseName)) {
-                indent.write(
-                    'val ${field.name} = $fieldType.ofRaw($listValue as Int)!!');
-              } else {
-                indent.writeln('val ${field.name} = $listValue as $fieldType');
-              }
-            }
-          });
-
-          indent.writeln('');
-          indent.write('return $className(');
-          for (final NamedType field in getFieldsInSerializationOrder(klass)) {
-            final String comma =
-                getFieldsInSerializationOrder(klass).last == field ? '' : ', ';
-            indent.add('${field.name}$comma');
-          }
-          indent.addln(')');
-        });
-      });
-    }
-
-    const List<String> generatedMessages = <String>[
-      ' Generated class from Pigeon that represents data sent in messages.'
-    ];
-    addDocumentationComments(
-        indent, klass.documentationComments, _docCommentSpec,
-        generatorComments: generatedMessages);
-
-    indent.write('data class ${klass.name} ');
-    indent.scoped('(', '', () {
-      for (final NamedType element in getFieldsInSerializationOrder(klass)) {
-        writeField(element);
-        if (getFieldsInSerializationOrder(klass).last != element) {
-          indent.addln(',');
-        } else {
-          indent.addln('');
-        }
-      }
-    });
-
-    indent.scoped(') {', '}', () {
-      writeFromList();
-      writeToList();
-    });
-  }
-
-  void writeApi(Api api) {
-    if (api.location == ApiLocation.host) {
-      _writeHostApi(indent, api, root);
-    } else if (api.location == ApiLocation.flutter) {
-      _writeFlutterApi(indent, api, root);
-    }
-  }
-
-  void writeWrapResult() {
-    indent.write('private fun wrapResult(result: Any?): List<Any?> ');
-    indent.scoped('{', '}', () {
-      indent.writeln('return listOf(result)');
-    });
-  }
-
-  void writeWrapError() {
-    indent.write('private fun wrapError(exception: Throwable): List<Any> ');
-    indent.scoped('{', '}', () {
-      indent.write('return ');
-      indent.scoped('listOf<Any>(', ')', () {
-        indent.writeln('exception.javaClass.simpleName,');
-        indent.writeln('exception.toString(),');
-        indent.writeln(
-            '"Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception)');
-      });
-    });
-  }
-
-  writeHeader();
-  indent.addln('');
-  if (options.package != null) {
-    indent.writeln('package ${options.package}');
-  }
-  indent.addln('');
-  writeImports();
-  indent.addln('');
-  indent.writeln('/** Generated class from Pigeon. */');
-  for (final Enum anEnum in root.enums) {
-    indent.writeln('');
-    writeEnum(anEnum);
-  }
-
-  for (final Class klass in root.classes) {
-    indent.addln('');
-    writeDataClass(klass);
-  }
-
-  if (root.apis.any((Api api) =>
-      api.location == ApiLocation.host &&
-      api.methods.any((Method it) => it.isAsynchronous))) {
-    indent.addln('');
-  }
-
-  for (final Api api in root.apis) {
-    if (getCodecClasses(api, root).isNotEmpty) {
-      _writeCodec(indent, api, root);
-      indent.addln('');
-    }
-    writeApi(api);
-  }
-
-  indent.addln('');
-  writeWrapResult();
-  indent.addln('');
-  writeWrapError();
-}
diff --git a/packages/pigeon/lib/objc_generator.dart b/packages/pigeon/lib/objc_generator.dart
index 3d7e3a6..2b165c9 100644
--- a/packages/pigeon/lib/objc_generator.dart
+++ b/packages/pigeon/lib/objc_generator.dart
@@ -64,26 +64,770 @@
   }
 }
 
-/// Class that manages all Objc header code generation.
+/// Class that manages all Objc code generation.
 class ObjcGenerator extends Generator<OutputFileOptions<ObjcOptions>> {
   /// Instantiates a Objc Generator.
-  ObjcGenerator();
+  const ObjcGenerator();
 
-  /// Generates Objc files with specified [OutputFileOptions<ObjcOptions>]
+  /// Generates Objc file of type specified in [generatorOptions]
   @override
-  void generate(OutputFileOptions<ObjcOptions> languageOptions, Root root,
+  void generate(OutputFileOptions<ObjcOptions> generatorOptions, Root root,
       StringSink sink) {
-    final FileType fileType = languageOptions.fileType;
-    assert(fileType == FileType.header || fileType == FileType.source);
-
-    if (fileType == FileType.header) {
-      generateObjcHeader(languageOptions.languageOptions, root, sink);
-    } else {
-      generateObjcSource(languageOptions.languageOptions, root, sink);
+    if (generatorOptions.fileType == FileType.header) {
+      const ObjcHeaderGenerator()
+          .generate(generatorOptions.languageOptions, root, sink);
+    } else if (generatorOptions.fileType == FileType.source) {
+      const ObjcSourceGenerator()
+          .generate(generatorOptions.languageOptions, root, sink);
     }
   }
 }
 
+/// Generates Objc .h file.
+class ObjcHeaderGenerator extends StructuredGenerator<ObjcOptions> {
+  /// Constructor.
+  const ObjcHeaderGenerator();
+
+  @override
+  void writeFilePrologue(
+      ObjcOptions generatorOptions, Root root, Indent indent) {
+    if (generatorOptions.copyrightHeader != null) {
+      addLines(indent, generatorOptions.copyrightHeader!, linePrefix: '// ');
+    }
+    indent.writeln('// $generatedCodeWarning');
+    indent.writeln('// $seeAlsoWarning');
+    indent.addln('');
+  }
+
+  @override
+  void writeFileImports(
+      ObjcOptions generatorOptions, Root root, Indent indent) {
+    indent.writeln('#import <Foundation/Foundation.h>');
+    indent.addln('');
+
+    indent.writeln('@protocol FlutterBinaryMessenger;');
+    indent.writeln('@protocol FlutterMessageCodec;');
+    indent.writeln('@class FlutterError;');
+    indent.writeln('@class FlutterStandardTypedData;');
+    indent.addln('');
+    indent.writeln('NS_ASSUME_NONNULL_BEGIN');
+  }
+
+  @override
+  void writeEnum(
+      ObjcOptions generatorOptions, Root root, Indent indent, Enum anEnum) {
+    final String enumName = _className(generatorOptions.prefix, anEnum.name);
+    indent.writeln('');
+    addDocumentationComments(
+        indent, anEnum.documentationComments, _docCommentSpec);
+
+    indent.write('typedef NS_ENUM(NSUInteger, $enumName) ');
+    indent.scoped('{', '};', () {
+      enumerate(anEnum.members, (int index, final EnumMember member) {
+        addDocumentationComments(
+            indent, member.documentationComments, _docCommentSpec);
+        // Capitalized first letter to ensure Swift compatibility
+        indent.writeln(
+            '$enumName${member.name[0].toUpperCase()}${member.name.substring(1)} = $index,');
+      });
+    });
+  }
+
+  @override
+  void writeDataClasses(
+      ObjcOptions generatorOptions, Root root, Indent indent) {
+    indent.writeln('');
+    for (final Class klass in root.classes) {
+      indent.writeln(
+          '@class ${_className(generatorOptions.prefix, klass.name)};');
+    }
+    indent.writeln('');
+    super.writeDataClasses(generatorOptions, root, indent);
+  }
+
+  @override
+  void writeDataClass(
+      ObjcOptions generatorOptions, Root root, Indent indent, Class klass) {
+    final List<Class> classes = root.classes;
+    final List<Enum> enums = root.enums;
+    final String? prefix = generatorOptions.prefix;
+    final List<String> customEnumNames = enums.map((Enum x) => x.name).toList();
+
+    addDocumentationComments(
+        indent, klass.documentationComments, _docCommentSpec);
+
+    indent.writeln('@interface ${_className(prefix, klass.name)} : NSObject');
+    if (getFieldsInSerializationOrder(klass).isNotEmpty) {
+      if (getFieldsInSerializationOrder(klass)
+          .map((NamedType e) => !e.type.isNullable)
+          .any((bool e) => e)) {
+        indent.writeln(
+            '$_docCommentPrefix `init` unavailable to enforce nonnull fields, see the `make` class method.');
+        indent.writeln('- (instancetype)init NS_UNAVAILABLE;');
+      }
+      _writeObjcSourceClassInitializerDeclaration(
+          indent, klass, classes, enums, prefix);
+      indent.addln(';');
+    }
+    for (final NamedType field in getFieldsInSerializationOrder(klass)) {
+      final HostDatatype hostDatatype = getFieldHostDatatype(
+          field,
+          classes,
+          enums,
+          (TypeDeclaration x) => _objcTypePtrForPrimitiveDartType(prefix, x),
+          customResolver: customEnumNames.contains(field.type.baseName)
+              ? (String x) => _className(prefix, x)
+              : (String x) => '${_className(prefix, x)} *');
+      late final String propertyType;
+      addDocumentationComments(
+          indent, field.documentationComments, _docCommentSpec);
+      if (customEnumNames.contains(field.type.baseName)) {
+        propertyType = 'assign';
+      } else {
+        propertyType = _propertyTypeForDartType(field);
+      }
+      final String nullability =
+          _isNullable(hostDatatype, field.type) ? ', nullable' : '';
+      indent.writeln(
+          '@property(nonatomic, $propertyType$nullability) ${hostDatatype.datatype} ${field.name};');
+    }
+    indent.writeln('@end');
+    indent.writeln('');
+  }
+
+  @override
+  void writeClassEncode(
+    ObjcOptions generatorOptions,
+    Root root,
+    Indent indent,
+    Class klass,
+    Set<String> customClassNames,
+    Set<String> customEnumNames,
+  ) {}
+
+  @override
+  void writeClassDecode(
+    ObjcOptions generatorOptions,
+    Root root,
+    Indent indent,
+    Class klass,
+    Set<String> customClassNames,
+    Set<String> customEnumNames,
+  ) {}
+
+  @override
+  void writeApis(ObjcOptions generatorOptions, Root root, Indent indent) {
+    super.writeApis(generatorOptions, root, indent);
+    indent.writeln('NS_ASSUME_NONNULL_END');
+  }
+
+  @override
+  void writeFlutterApi(
+    ObjcOptions generatorOptions,
+    Root root,
+    Indent indent,
+    Api api,
+  ) {
+    indent.writeln(
+        '$_docCommentPrefix The codec used by ${_className(generatorOptions.prefix, api.name)}.');
+    indent.writeln(
+        'NSObject<FlutterMessageCodec> *${_getCodecGetterName(generatorOptions.prefix, api.name)}(void);');
+    indent.addln('');
+    final String apiName = _className(generatorOptions.prefix, api.name);
+    addDocumentationComments(
+        indent, api.documentationComments, _docCommentSpec);
+
+    indent.writeln('@interface $apiName : NSObject');
+    indent.writeln(
+        '- (instancetype)initWithBinaryMessenger:(id<FlutterBinaryMessenger>)binaryMessenger;');
+    for (final Method func in api.methods) {
+      final _ObjcPtr returnType =
+          _objcTypeForDartType(generatorOptions.prefix, func.returnType);
+      final String callbackType = _callbackForType(func.returnType, returnType);
+      addDocumentationComments(
+          indent, func.documentationComments, _docCommentSpec);
+
+      indent.writeln('${_makeObjcSignature(
+        func: func,
+        options: generatorOptions,
+        returnType: 'void',
+        lastArgName: 'completion',
+        lastArgType: callbackType,
+        isEnum: (TypeDeclaration t) => isEnum(root, t),
+      )};');
+    }
+    indent.writeln('@end');
+    indent.writeln('');
+  }
+
+  @override
+  void writeHostApi(
+    ObjcOptions generatorOptions,
+    Root root,
+    Indent indent,
+    Api api,
+  ) {
+    indent.writeln(
+        '$_docCommentPrefix The codec used by ${_className(generatorOptions.prefix, api.name)}.');
+    indent.writeln(
+        'NSObject<FlutterMessageCodec> *${_getCodecGetterName(generatorOptions.prefix, api.name)}(void);');
+    indent.addln('');
+    final String apiName = _className(generatorOptions.prefix, api.name);
+    addDocumentationComments(
+        indent, api.documentationComments, _docCommentSpec);
+
+    indent.writeln('@protocol $apiName');
+    for (final Method func in api.methods) {
+      final _ObjcPtr returnTypeName =
+          _objcTypeForDartType(generatorOptions.prefix, func.returnType);
+
+      String? lastArgName;
+      String? lastArgType;
+      String? returnType;
+      if (func.isAsynchronous) {
+        returnType = 'void';
+        if (func.returnType.isVoid) {
+          lastArgType = 'void(^)(FlutterError *_Nullable)';
+          lastArgName = 'completion';
+        } else {
+          lastArgType =
+              'void(^)(${returnTypeName.ptr}_Nullable, FlutterError *_Nullable)';
+          lastArgName = 'completion';
+        }
+      } else {
+        returnType = func.returnType.isVoid
+            ? 'void'
+            : 'nullable ${returnTypeName.ptr.trim()}';
+        lastArgType = 'FlutterError *_Nullable *_Nonnull';
+        lastArgName = 'error';
+      }
+      final List<String> generatorComments = <String>[];
+      if (!func.returnType.isNullable &&
+          !func.returnType.isVoid &&
+          !func.isAsynchronous) {
+        generatorComments.add(' @return `nil` only when `error != nil`.');
+      }
+      addDocumentationComments(
+          indent, func.documentationComments, _docCommentSpec,
+          generatorComments: generatorComments);
+
+      final String signature = _makeObjcSignature(
+        func: func,
+        options: generatorOptions,
+        returnType: returnType,
+        lastArgName: lastArgName,
+        lastArgType: lastArgType,
+        isEnum: (TypeDeclaration t) => isEnum(root, t),
+      );
+      indent.writeln('$signature;');
+    }
+    indent.writeln('@end');
+    indent.writeln('');
+    indent.writeln(
+        'extern void ${apiName}Setup(id<FlutterBinaryMessenger> binaryMessenger, NSObject<$apiName> *_Nullable api);');
+    indent.writeln('');
+  }
+}
+
+/// Generates Objc .m file.
+class ObjcSourceGenerator extends StructuredGenerator<ObjcOptions> {
+  /// Constructor.
+  const ObjcSourceGenerator();
+
+  @override
+  void writeFilePrologue(
+      ObjcOptions generatorOptions, Root root, Indent indent) {
+    if (generatorOptions.copyrightHeader != null) {
+      addLines(indent, generatorOptions.copyrightHeader!, linePrefix: '// ');
+    }
+    indent.writeln('// $generatedCodeWarning');
+    indent.writeln('// $seeAlsoWarning');
+    indent.addln('');
+  }
+
+  @override
+  void writeFileImports(
+      ObjcOptions generatorOptions, Root root, Indent indent) {
+    indent.writeln('#import "${generatorOptions.headerIncludePath}"');
+    indent.writeln('#import <Flutter/Flutter.h>');
+    indent.addln('');
+
+    indent.writeln('#if !__has_feature(objc_arc)');
+    indent.writeln('#error File requires ARC to be enabled.');
+    indent.writeln('#endif');
+    indent.addln('');
+  }
+
+  @override
+  void writeDataClasses(
+      ObjcOptions generatorOptions, Root root, Indent indent) {
+    _writeObjcSourceHelperFunctions(indent);
+    indent.addln('');
+
+    for (final Class klass in root.classes) {
+      _writeObjcSourceDataClassExtension(generatorOptions, indent, klass);
+    }
+    indent.writeln('');
+    super.writeDataClasses(generatorOptions, root, indent);
+  }
+
+  @override
+  void writeDataClass(
+      ObjcOptions generatorOptions, Root root, Indent indent, Class klass) {
+    final Set<String> customClassNames =
+        root.classes.map((Class x) => x.name).toSet();
+    final Set<String> customEnumNames =
+        root.enums.map((Enum x) => x.name).toSet();
+    final String className = _className(generatorOptions.prefix, klass.name);
+
+    indent.writeln('@implementation $className');
+    _writeObjcSourceClassInitializer(generatorOptions, root, indent, klass,
+        customClassNames, customEnumNames, className);
+    writeClassDecode(generatorOptions, root, indent, klass, customClassNames,
+        customEnumNames);
+    writeClassEncode(generatorOptions, root, indent, klass, customClassNames,
+        customEnumNames);
+    indent.writeln('@end');
+    indent.writeln('');
+  }
+
+  @override
+  void writeClassEncode(
+    ObjcOptions generatorOptions,
+    Root root,
+    Indent indent,
+    Class klass,
+    Set<String> customClassNames,
+    Set<String> customEnumNames,
+  ) {
+    indent.write('- (NSArray *)toList ');
+    indent.scoped('{', '}', () {
+      indent.write('return');
+      indent.scoped(' @[', '];', () {
+        for (final NamedType field in klass.fields) {
+          indent.writeln(
+              '${_arrayValue(customClassNames, customEnumNames, field)},');
+        }
+      });
+    });
+  }
+
+  @override
+  void writeClassDecode(
+    ObjcOptions generatorOptions,
+    Root root,
+    Indent indent,
+    Class klass,
+    Set<String> customClassNames,
+    Set<String> customEnumNames,
+  ) {
+    final String className = _className(generatorOptions.prefix, klass.name);
+    indent.write('+ ($className *)fromList:(NSArray *)list ');
+    indent.scoped('{', '}', () {
+      const String resultName = 'pigeonResult';
+      indent.writeln('$className *$resultName = [[$className alloc] init];');
+      enumerate(getFieldsInSerializationOrder(klass),
+          (int index, final NamedType field) {
+        if (customEnumNames.contains(field.type.baseName)) {
+          indent.writeln(
+              '$resultName.${field.name} = [${_listGetter(customClassNames, 'list', field, index, generatorOptions.prefix)} integerValue];');
+        } else {
+          indent.writeln(
+              '$resultName.${field.name} = ${_listGetter(customClassNames, 'list', field, index, generatorOptions.prefix)};');
+          if (!field.type.isNullable) {
+            indent.writeln('NSAssert($resultName.${field.name} != nil, @"");');
+          }
+        }
+      });
+      indent.writeln('return $resultName;');
+    });
+
+    indent.writeln(
+        '+ (nullable $className *)nullableFromList:(NSArray *)list { return (list) ? [$className fromList:list] : nil; }');
+  }
+
+  void _writeCodecAndGetter(
+      ObjcOptions generatorOptions, Root root, Indent indent, Api api) {
+    final String codecName = _getCodecName(generatorOptions.prefix, api.name);
+    if (getCodecClasses(api, root).isNotEmpty) {
+      _writeCodec(indent, codecName, generatorOptions, api, root);
+      indent.addln('');
+    }
+    _writeCodecGetter(indent, codecName, generatorOptions, api, root);
+    indent.addln('');
+  }
+
+  @override
+  void writeFlutterApi(
+    ObjcOptions generatorOptions,
+    Root root,
+    Indent indent,
+    Api api,
+  ) {
+    assert(api.location == ApiLocation.flutter);
+    final String apiName = _className(generatorOptions.prefix, api.name);
+
+    _writeCodecAndGetter(generatorOptions, root, indent, api);
+
+    _writeExtension(indent, apiName);
+    indent.addln('');
+    indent.writeln('@implementation $apiName');
+    indent.addln('');
+    _writeInitializer(indent);
+    for (final Method func in api.methods) {
+      _writeMethod(generatorOptions, root, indent, api, func);
+    }
+    indent.writeln('@end');
+    indent.writeln('');
+  }
+
+  @override
+  void writeHostApi(
+    ObjcOptions generatorOptions,
+    Root root,
+    Indent indent,
+    Api api,
+  ) {
+    assert(api.location == ApiLocation.host);
+    final String apiName = _className(generatorOptions.prefix, api.name);
+
+    _writeCodecAndGetter(generatorOptions, root, indent, api);
+
+    const String channelName = 'channel';
+    indent.write(
+        'void ${apiName}Setup(id<FlutterBinaryMessenger> binaryMessenger, NSObject<$apiName> *api) ');
+    indent.scoped('{', '}', () {
+      for (final Method func in api.methods) {
+        indent.write('');
+        addDocumentationComments(
+            indent, func.documentationComments, _docCommentSpec);
+
+        indent.scoped('{', '}', () {
+          String? taskQueue;
+          if (func.taskQueueType != TaskQueueType.serial) {
+            taskQueue = 'taskQueue';
+            indent.writeln(
+                'NSObject<FlutterTaskQueue> *$taskQueue = [binaryMessenger makeBackgroundTaskQueue];');
+          }
+          _writeChannelAllocation(
+              generatorOptions, indent, api, func, channelName, taskQueue);
+          indent.write('if (api) ');
+          indent.scoped('{', '}', () {
+            _writeChannelApiBinding(
+                generatorOptions, root, indent, apiName, func, channelName);
+          });
+          indent.write('else ');
+          indent.scoped('{', '}', () {
+            indent.writeln('[$channelName setMessageHandler:nil];');
+          });
+        });
+      }
+    });
+  }
+
+  void _writeChannelApiBinding(ObjcOptions generatorOptions, Root root,
+      Indent indent, String apiName, Method func, String channel) {
+    void unpackArgs(String variable, Iterable<String> argNames) {
+      indent.writeln('NSArray *args = $variable;');
+      map3(wholeNumbers.take(func.arguments.length), argNames, func.arguments,
+          (int count, String argName, NamedType arg) {
+        if (isEnum(root, arg.type)) {
+          return '${_className(generatorOptions.prefix, arg.type.baseName)} $argName = [GetNullableObjectAtIndex(args, $count) integerValue];';
+        } else {
+          final _ObjcPtr argType =
+              _objcTypeForDartType(generatorOptions.prefix, arg.type);
+          return '${argType.ptr}$argName = GetNullableObjectAtIndex(args, $count);';
+        }
+      }).forEach(indent.writeln);
+    }
+
+    void writeAsyncBindings(Iterable<String> selectorComponents,
+        String callSignature, _ObjcPtr returnType) {
+      if (func.returnType.isVoid) {
+        const String callback = 'callback(wrapResult(nil, error));';
+        if (func.arguments.isEmpty) {
+          indent.writeScoped(
+              '[api ${selectorComponents.first}:^(FlutterError *_Nullable error) {',
+              '}];', () {
+            indent.writeln(callback);
+          });
+        } else {
+          indent.writeScoped(
+              '[api $callSignature ${selectorComponents.last}:^(FlutterError *_Nullable error) {',
+              '}];', () {
+            indent.writeln(callback);
+          });
+        }
+      } else {
+        const String callback = 'callback(wrapResult(output, error));';
+        if (func.arguments.isEmpty) {
+          indent.writeScoped(
+              '[api ${selectorComponents.first}:^(${returnType.ptr}_Nullable output, FlutterError *_Nullable error) {',
+              '}];', () {
+            indent.writeln(callback);
+          });
+        } else {
+          indent.writeScoped(
+              '[api $callSignature ${selectorComponents.last}:^(${returnType.ptr}_Nullable output, FlutterError *_Nullable error) {',
+              '}];', () {
+            indent.writeln(callback);
+          });
+        }
+      }
+    }
+
+    void writeSyncBindings(String call, _ObjcPtr returnType) {
+      indent.writeln('FlutterError *error;');
+      if (func.returnType.isVoid) {
+        indent.writeln('$call;');
+        indent.writeln('callback(wrapResult(nil, error));');
+      } else {
+        indent.writeln('${returnType.ptr}output = $call;');
+        indent.writeln('callback(wrapResult(output, error));');
+      }
+    }
+
+    // TODO(gaaclarke): Incorporate this into _getSelectorComponents.
+    final String lastSelectorComponent =
+        func.isAsynchronous ? 'completion' : 'error';
+    final String selector = _getSelector(func, lastSelectorComponent);
+    indent.writeln(
+        'NSCAssert([api respondsToSelector:@selector($selector)], @"$apiName api (%@) doesn\'t respond to @selector($selector)", api);');
+    indent.write(
+        '[$channel setMessageHandler:^(id _Nullable message, FlutterReply callback) ');
+    indent.scoped('{', '}];', () {
+      final _ObjcPtr returnType =
+          _objcTypeForDartType(generatorOptions.prefix, func.returnType);
+      final Iterable<String> selectorComponents =
+          _getSelectorComponents(func, lastSelectorComponent);
+      final Iterable<String> argNames =
+          indexMap(func.arguments, _getSafeArgName);
+      final String callSignature =
+          map2(selectorComponents.take(argNames.length), argNames,
+              (String selectorComponent, String argName) {
+        return '$selectorComponent:$argName';
+      }).join(' ');
+      if (func.arguments.isNotEmpty) {
+        unpackArgs('message', argNames);
+      }
+      if (func.isAsynchronous) {
+        writeAsyncBindings(selectorComponents, callSignature, returnType);
+      } else {
+        final String syncCall = func.arguments.isEmpty
+            ? '[api ${selectorComponents.first}:&error]'
+            : '[api $callSignature error:&error]';
+        writeSyncBindings(syncCall, returnType);
+      }
+    });
+  }
+
+  void _writeChannelAllocation(ObjcOptions generatorOptions, Indent indent,
+      Api api, Method func, String varName, String? taskQueue) {
+    indent.writeln('FlutterBasicMessageChannel *$varName =');
+    indent.inc();
+    indent.writeln('[[FlutterBasicMessageChannel alloc]');
+    indent.inc();
+    indent.writeln('initWithName:@"${makeChannelName(api, func)}"');
+    indent.writeln('binaryMessenger:binaryMessenger');
+    indent.write('codec:');
+    indent.add('${_getCodecGetterName(generatorOptions.prefix, api.name)}()');
+
+    if (taskQueue != null) {
+      indent.addln('');
+      indent.addln('taskQueue:$taskQueue];');
+    } else {
+      indent.addln('];');
+    }
+    indent.dec();
+    indent.dec();
+  }
+
+  void _writeObjcSourceHelperFunctions(Indent indent) {
+    indent.format('''
+static NSArray *wrapResult(id result, FlutterError *error) {
+\tif (error) {
+\t\treturn @[ error.code ?: [NSNull null], error.message ?: [NSNull null], error.details ?: [NSNull null] ];
+\t}
+\treturn @[ result ?: [NSNull null]  ];
+}''');
+    indent.format('''
+static id GetNullableObject(NSDictionary* dict, id key) {
+\tid result = dict[key];
+\treturn (result == [NSNull null]) ? nil : result;
+}
+static id GetNullableObjectAtIndex(NSArray* array, NSInteger key) {
+\tid result = array[key];
+\treturn (result == [NSNull null]) ? nil : result;
+}
+''');
+  }
+
+  void _writeObjcSourceDataClassExtension(
+      ObjcOptions languageOptions, Indent indent, Class klass) {
+    final String className = _className(languageOptions.prefix, klass.name);
+    indent.writeln('@interface $className ()');
+    indent.writeln('+ ($className *)fromList:(NSArray *)list;');
+    indent
+        .writeln('+ (nullable $className *)nullableFromList:(NSArray *)list;');
+    indent.writeln('- (NSArray *)toList;');
+    indent.writeln('@end');
+    indent.writeln('');
+  }
+
+  void _writeObjcSourceClassInitializer(
+    ObjcOptions languageOptions,
+    Root root,
+    Indent indent,
+    Class klass,
+    Set<String> customClassNames,
+    Set<String> customEnumNames,
+    String className,
+  ) {
+    _writeObjcSourceClassInitializerDeclaration(
+        indent, klass, root.classes, root.enums, languageOptions.prefix);
+    indent.writeScoped(' {', '}', () {
+      const String result = 'pigeonResult';
+      indent.writeln('$className* $result = [[$className alloc] init];');
+      for (final NamedType field in getFieldsInSerializationOrder(klass)) {
+        indent.writeln('$result.${field.name} = ${field.name};');
+      }
+      indent.writeln('return $result;');
+    });
+  }
+
+  /// Writes the codec that will be used for encoding messages for the [api].
+  ///
+  /// Example:
+  /// @interface FooHostApiCodecReader : FlutterStandardReader
+  /// ...
+  /// @interface FooHostApiCodecWriter : FlutterStandardWriter
+  /// ...
+  /// @interface FooHostApiCodecReaderWriter : FlutterStandardReaderWriter
+  /// ...
+  /// NSObject<FlutterMessageCodec> *FooHostApiCodecGetCodec() {...}
+  void _writeCodec(
+      Indent indent, String name, ObjcOptions options, Api api, Root root) {
+    assert(getCodecClasses(api, root).isNotEmpty);
+    final Iterable<EnumeratedClass> codecClasses = getCodecClasses(api, root);
+    final String readerWriterName = '${name}ReaderWriter';
+    final String readerName = '${name}Reader';
+    final String writerName = '${name}Writer';
+    indent.writeln('@interface $readerName : FlutterStandardReader');
+    indent.writeln('@end');
+    indent.writeln('@implementation $readerName');
+    indent.writeln('- (nullable id)readValueOfType:(UInt8)type ');
+    indent.scoped('{', '}', () {
+      indent.write('switch (type) ');
+      indent.scoped('{', '}', () {
+        for (final EnumeratedClass customClass in codecClasses) {
+          indent.write('case ${customClass.enumeration}: ');
+          indent.writeScoped('', '', () {
+            indent.writeln(
+                'return [${_className(options.prefix, customClass.name)} fromList:[self readValue]];');
+          });
+        }
+        indent.write('default:');
+        indent.writeScoped('', '', () {
+          indent.writeln('return [super readValueOfType:type];');
+        });
+      });
+    });
+    indent.writeln('@end');
+    indent.addln('');
+    indent.writeln('@interface $writerName : FlutterStandardWriter');
+    indent.writeln('@end');
+    indent.writeln('@implementation $writerName');
+    indent.writeln('- (void)writeValue:(id)value ');
+    indent.scoped('{', '}', () {
+      for (final EnumeratedClass customClass in codecClasses) {
+        indent.write(
+            'if ([value isKindOfClass:[${_className(options.prefix, customClass.name)} class]]) ');
+        indent.scoped('{', '} else ', () {
+          indent.writeln('[self writeByte:${customClass.enumeration}];');
+          indent.writeln('[self writeValue:[value toList]];');
+        });
+      }
+      indent.scoped('{', '}', () {
+        indent.writeln('[super writeValue:value];');
+      });
+    });
+    indent.writeln('@end');
+    indent.addln('');
+    indent.format('''
+@interface $readerWriterName : FlutterStandardReaderWriter
+@end
+@implementation $readerWriterName
+- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data {
+\treturn [[$writerName alloc] initWithData:data];
+}
+- (FlutterStandardReader *)readerWithData:(NSData *)data {
+\treturn [[$readerName alloc] initWithData:data];
+}
+@end
+''');
+  }
+
+  void _writeCodecGetter(
+      Indent indent, String name, ObjcOptions options, Api api, Root root) {
+    final String readerWriterName = '${name}ReaderWriter';
+
+    indent.write(
+        'NSObject<FlutterMessageCodec> *${_getCodecGetterName(options.prefix, api.name)}() ');
+    indent.scoped('{', '}', () {
+      indent
+          .writeln('static FlutterStandardMessageCodec *sSharedObject = nil;');
+      if (getCodecClasses(api, root).isNotEmpty) {
+        indent.writeln('static dispatch_once_t sPred = 0;');
+        indent.write('dispatch_once(&sPred, ^');
+        indent.scoped('{', '});', () {
+          indent.writeln(
+              '$readerWriterName *readerWriter = [[$readerWriterName alloc] init];');
+          indent.writeln(
+              'sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter];');
+        });
+      } else {
+        indent.writeln(
+            'sSharedObject = [FlutterStandardMessageCodec sharedInstance];');
+      }
+
+      indent.writeln('return sSharedObject;');
+    });
+  }
+}
+
+/// Writes the method declaration for the initializer.
+///
+/// Example '+ (instancetype)makeWithFoo:(NSString *)foo'
+void _writeObjcSourceClassInitializerDeclaration(Indent indent, Class klass,
+    List<Class> classes, List<Enum> enums, String? prefix) {
+  final List<String> customEnumNames = enums.map((Enum x) => x.name).toList();
+  indent.write('+ (instancetype)makeWith');
+  bool isFirst = true;
+  indent.nest(2, () {
+    for (final NamedType field in getFieldsInSerializationOrder(klass)) {
+      final String label = isFirst ? _capitalize(field.name) : field.name;
+      final void Function(String) printer = isFirst
+          ? indent.add
+          : (String x) {
+              indent.addln('');
+              indent.write(x);
+            };
+      isFirst = false;
+      final HostDatatype hostDatatype = getFieldHostDatatype(
+          field,
+          classes,
+          enums,
+          (TypeDeclaration x) => _objcTypePtrForPrimitiveDartType(prefix, x),
+          customResolver: customEnumNames.contains(field.type.baseName)
+              ? (String x) => _className(prefix, x)
+              : (String x) => '${_className(prefix, x)} *');
+      final String nullable =
+          _isNullable(hostDatatype, field.type) ? 'nullable ' : '';
+      printer('$label:($nullable${hostDatatype.datatype})${field.name}');
+    }
+  });
+}
+
 /// Calculates the ObjC class name, possibly prefixed.
 String _className(String? prefix, String className) {
   if (prefix != null) {
@@ -180,91 +924,6 @@
 bool _isNullable(HostDatatype hostDatatype, TypeDeclaration type) =>
     hostDatatype.datatype.contains('*') && type.isNullable;
 
-/// Writes the method declaration for the initializer.
-///
-/// Example '+ (instancetype)makeWithFoo:(NSString *)foo'
-void _writeInitializerDeclaration(Indent indent, Class klass,
-    List<Class> classes, List<Enum> enums, String? prefix) {
-  final List<String> enumNames = enums.map((Enum x) => x.name).toList();
-  indent.write('+ (instancetype)makeWith');
-  bool isFirst = true;
-  indent.nest(2, () {
-    for (final NamedType field in getFieldsInSerializationOrder(klass)) {
-      final String label = isFirst ? _capitalize(field.name) : field.name;
-      final void Function(String) printer = isFirst
-          ? indent.add
-          : (String x) {
-              indent.addln('');
-              indent.write(x);
-            };
-      isFirst = false;
-      final HostDatatype hostDatatype = getFieldHostDatatype(
-          field,
-          classes,
-          enums,
-          (TypeDeclaration x) => _objcTypePtrForPrimitiveDartType(prefix, x),
-          customResolver: enumNames.contains(field.type.baseName)
-              ? (String x) => _className(prefix, x)
-              : (String x) => '${_className(prefix, x)} *');
-      final String nullable =
-          _isNullable(hostDatatype, field.type) ? 'nullable ' : '';
-      printer('$label:($nullable${hostDatatype.datatype})${field.name}');
-    }
-  });
-}
-
-/// Writes the class declaration for a data class.
-///
-/// Example:
-/// @interface Foo : NSObject
-/// @property (nonatomic, copy) NSString *bar;
-/// @end
-void _writeClassDeclarations(
-    Indent indent, List<Class> classes, List<Enum> enums, String? prefix) {
-  final List<String> enumNames = enums.map((Enum x) => x.name).toList();
-  for (final Class klass in classes) {
-    addDocumentationComments(
-        indent, klass.documentationComments, _docCommentSpec);
-
-    indent.writeln('@interface ${_className(prefix, klass.name)} : NSObject');
-    if (getFieldsInSerializationOrder(klass).isNotEmpty) {
-      if (getFieldsInSerializationOrder(klass)
-          .map((NamedType e) => !e.type.isNullable)
-          .any((bool e) => e)) {
-        indent.writeln(
-            '$_docCommentPrefix `init` unavailable to enforce nonnull fields, see the `make` class method.');
-        indent.writeln('- (instancetype)init NS_UNAVAILABLE;');
-      }
-      _writeInitializerDeclaration(indent, klass, classes, enums, prefix);
-      indent.addln(';');
-    }
-    for (final NamedType field in getFieldsInSerializationOrder(klass)) {
-      final HostDatatype hostDatatype = getFieldHostDatatype(
-          field,
-          classes,
-          enums,
-          (TypeDeclaration x) => _objcTypePtrForPrimitiveDartType(prefix, x),
-          customResolver: enumNames.contains(field.type.baseName)
-              ? (String x) => _className(prefix, x)
-              : (String x) => '${_className(prefix, x)} *');
-      late final String propertyType;
-      addDocumentationComments(
-          indent, field.documentationComments, _docCommentSpec);
-      if (enumNames.contains(field.type.baseName)) {
-        propertyType = 'assign';
-      } else {
-        propertyType = _propertyTypeForDartType(field);
-      }
-      final String nullability =
-          _isNullable(hostDatatype, field.type) ? ', nullable' : '';
-      indent.writeln(
-          '@property(nonatomic, $propertyType$nullability) ${hostDatatype.datatype} ${field.name};');
-    }
-    indent.writeln('@end');
-    indent.writeln('');
-  }
-}
-
 /// Generates the name of the codec that will be generated.
 String _getCodecName(String? prefix, String className) =>
     '${_className(prefix, className)}Codec';
@@ -274,104 +933,6 @@
 String _getCodecGetterName(String? prefix, String className) =>
     '${_className(prefix, className)}GetCodec';
 
-/// Writes the codec that will be used for encoding messages for the [api].
-///
-/// Example:
-/// @interface FooHostApiCodecReader : FlutterStandardReader
-/// ...
-/// @interface FooHostApiCodecWriter : FlutterStandardWriter
-/// ...
-/// @interface FooHostApiCodecReaderWriter : FlutterStandardReaderWriter
-/// ...
-/// NSObject<FlutterMessageCodec> *FooHostApiCodecGetCodec() {...}
-void _writeCodec(
-    Indent indent, String name, ObjcOptions options, Api api, Root root) {
-  assert(getCodecClasses(api, root).isNotEmpty);
-  final Iterable<EnumeratedClass> codecClasses = getCodecClasses(api, root);
-  final String readerWriterName = '${name}ReaderWriter';
-  final String readerName = '${name}Reader';
-  final String writerName = '${name}Writer';
-  indent.writeln('@interface $readerName : FlutterStandardReader');
-  indent.writeln('@end');
-  indent.writeln('@implementation $readerName');
-  indent.writeln('- (nullable id)readValueOfType:(UInt8)type ');
-  indent.scoped('{', '}', () {
-    indent.write('switch (type) ');
-    indent.scoped('{', '}', () {
-      for (final EnumeratedClass customClass in codecClasses) {
-        indent.write('case ${customClass.enumeration}: ');
-        indent.writeScoped('', '', () {
-          indent.writeln(
-              'return [${_className(options.prefix, customClass.name)} fromList:[self readValue]];');
-        });
-      }
-      indent.write('default:');
-      indent.writeScoped('', '', () {
-        indent.writeln('return [super readValueOfType:type];');
-      });
-    });
-  });
-  indent.writeln('@end');
-  indent.addln('');
-  indent.writeln('@interface $writerName : FlutterStandardWriter');
-  indent.writeln('@end');
-  indent.writeln('@implementation $writerName');
-  indent.writeln('- (void)writeValue:(id)value ');
-  indent.scoped('{', '}', () {
-    for (final EnumeratedClass customClass in codecClasses) {
-      indent.write(
-          'if ([value isKindOfClass:[${_className(options.prefix, customClass.name)} class]]) ');
-      indent.scoped('{', '} else ', () {
-        indent.writeln('[self writeByte:${customClass.enumeration}];');
-        indent.writeln('[self writeValue:[value toList]];');
-      });
-    }
-    indent.scoped('{', '}', () {
-      indent.writeln('[super writeValue:value];');
-    });
-  });
-  indent.writeln('@end');
-  indent.addln('');
-  indent.format('''
-@interface $readerWriterName : FlutterStandardReaderWriter
-@end
-@implementation $readerWriterName
-- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data {
-\treturn [[$writerName alloc] initWithData:data];
-}
-- (FlutterStandardReader *)readerWithData:(NSData *)data {
-\treturn [[$readerName alloc] initWithData:data];
-}
-@end
-''');
-}
-
-void _writeCodecGetter(
-    Indent indent, String name, ObjcOptions options, Api api, Root root) {
-  final String readerWriterName = '${name}ReaderWriter';
-
-  indent.write(
-      'NSObject<FlutterMessageCodec> *${_getCodecGetterName(options.prefix, api.name)}() ');
-  indent.scoped('{', '}', () {
-    indent.writeln('static FlutterStandardMessageCodec *sSharedObject = nil;');
-    if (getCodecClasses(api, root).isNotEmpty) {
-      indent.writeln('static dispatch_once_t sPred = 0;');
-      indent.write('dispatch_once(&sPred, ^');
-      indent.scoped('{', '});', () {
-        indent.writeln(
-            '$readerWriterName *readerWriter = [[$readerWriterName alloc] init];');
-        indent.writeln(
-            'sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter];');
-      });
-    } else {
-      indent.writeln(
-          'sSharedObject = [FlutterStandardMessageCodec sharedInstance];');
-    }
-
-    indent.writeln('return sSharedObject;');
-  });
-}
-
 String _capitalize(String str) =>
     (str.isEmpty) ? '' : str[0].toUpperCase() + str.substring(1);
 
@@ -451,187 +1012,13 @@
   return '- ($returnType)$argSignature';
 }
 
-/// Writes the declaration for an host [Api].
-///
-/// Example:
-/// @protocol Foo
-/// - (NSInteger)add:(NSInteger)x to:(NSInteger)y error:(NSError**)error;
-/// @end
-///
-/// extern void FooSetup(id<FlutterBinaryMessenger> binaryMessenger, NSObject<Foo> *_Nullable api);
-void _writeHostApiDeclaration(
-    Indent indent, Api api, ObjcOptions options, Root root) {
-  final String apiName = _className(options.prefix, api.name);
-  addDocumentationComments(indent, api.documentationComments, _docCommentSpec);
-
-  indent.writeln('@protocol $apiName');
-  for (final Method func in api.methods) {
-    final _ObjcPtr returnTypeName =
-        _objcTypeForDartType(options.prefix, func.returnType);
-
-    String? lastArgName;
-    String? lastArgType;
-    String? returnType;
-    if (func.isAsynchronous) {
-      returnType = 'void';
-      if (func.returnType.isVoid) {
-        lastArgType = 'void(^)(FlutterError *_Nullable)';
-        lastArgName = 'completion';
-      } else {
-        lastArgType =
-            'void(^)(${returnTypeName.ptr}_Nullable, FlutterError *_Nullable)';
-        lastArgName = 'completion';
-      }
-    } else {
-      returnType = func.returnType.isVoid
-          ? 'void'
-          : 'nullable ${returnTypeName.ptr.trim()}';
-      lastArgType = 'FlutterError *_Nullable *_Nonnull';
-      lastArgName = 'error';
-    }
-    final List<String> generatorComments = <String>[];
-    if (!func.returnType.isNullable &&
-        !func.returnType.isVoid &&
-        !func.isAsynchronous) {
-      generatorComments.add(' @return `nil` only when `error != nil`.');
-    }
-    addDocumentationComments(
-        indent, func.documentationComments, _docCommentSpec,
-        generatorComments: generatorComments);
-
-    final String signature = _makeObjcSignature(
-      func: func,
-      options: options,
-      returnType: returnType,
-      lastArgName: lastArgName,
-      lastArgType: lastArgType,
-      isEnum: (TypeDeclaration t) => isEnum(root, t),
-    );
-    indent.writeln('$signature;');
-  }
-  indent.writeln('@end');
-  indent.writeln('');
-  indent.writeln(
-      'extern void ${apiName}Setup(id<FlutterBinaryMessenger> binaryMessenger, NSObject<$apiName> *_Nullable api);');
-  indent.writeln('');
-}
-
-/// Writes the declaration for an flutter [Api].
-///
-/// Example:
-///
-/// @interface Foo : NSObject
-/// - (instancetype)initWithBinaryMessenger:(id<FlutterBinaryMessenger>)binaryMessenger;
-/// - (void)add:(NSInteger)x to:(NSInteger)y completion:(void(^)(NSError *, NSInteger result)completion;
-/// @end
-void _writeFlutterApiDeclaration(
-    Indent indent, Api api, ObjcOptions options, Root root) {
-  final String apiName = _className(options.prefix, api.name);
-  addDocumentationComments(indent, api.documentationComments, _docCommentSpec);
-
-  indent.writeln('@interface $apiName : NSObject');
-  indent.writeln(
-      '- (instancetype)initWithBinaryMessenger:(id<FlutterBinaryMessenger>)binaryMessenger;');
-  for (final Method func in api.methods) {
-    final _ObjcPtr returnType =
-        _objcTypeForDartType(options.prefix, func.returnType);
-    final String callbackType = _callbackForType(func.returnType, returnType);
-    addDocumentationComments(
-        indent, func.documentationComments, _docCommentSpec);
-
-    indent.writeln('${_makeObjcSignature(
-      func: func,
-      options: options,
-      returnType: 'void',
-      lastArgName: 'completion',
-      lastArgType: callbackType,
-      isEnum: (TypeDeclaration t) => isEnum(root, t),
-    )};');
-  }
-  indent.writeln('@end');
-}
-
 /// Generates the ".h" file for the AST represented by [root] to [sink] with the
 /// provided [options].
-void generateObjcHeader(ObjcOptions options, Root root, StringSink sink) {
-  final Indent indent = Indent(sink);
+void generateObjcHeader(ObjcOptions options, Root root, Indent indent) {}
 
-  void writeHeader() {
-    if (options.copyrightHeader != null) {
-      addLines(indent, options.copyrightHeader!, linePrefix: '// ');
-    }
-    indent.writeln('// $generatedCodeWarning');
-    indent.writeln('// $seeAlsoWarning');
-  }
-
-  void writeImports() {
-    indent.writeln('#import <Foundation/Foundation.h>');
-  }
-
-  void writeForwardDeclarations() {
-    indent.writeln('@protocol FlutterBinaryMessenger;');
-    indent.writeln('@protocol FlutterMessageCodec;');
-    indent.writeln('@class FlutterError;');
-    indent.writeln('@class FlutterStandardTypedData;');
-  }
-
-  void writeEnum(Enum anEnum) {
-    final String enumName = _className(options.prefix, anEnum.name);
-    addDocumentationComments(
-        indent, anEnum.documentationComments, _docCommentSpec);
-
-    indent.write('typedef NS_ENUM(NSUInteger, $enumName) ');
-    indent.scoped('{', '};', () {
-      enumerate(anEnum.members, (int index, final EnumMember member) {
-        addDocumentationComments(
-            indent, member.documentationComments, _docCommentSpec);
-        // Capitalized first letter to ensure Swift compatibility
-        indent.writeln(
-            '$enumName${member.name[0].toUpperCase()}${member.name.substring(1)} = $index,');
-      });
-    });
-  }
-
-  writeHeader();
-  writeImports();
-  writeForwardDeclarations();
-  indent.writeln('');
-
-  indent.writeln('NS_ASSUME_NONNULL_BEGIN');
-
-  for (final Enum anEnum in root.enums) {
-    indent.writeln('');
-    writeEnum(anEnum);
-  }
-  indent.writeln('');
-
-  for (final Class klass in root.classes) {
-    indent.writeln('@class ${_className(options.prefix, klass.name)};');
-  }
-
-  indent.writeln('');
-
-  _writeClassDeclarations(indent, root.classes, root.enums, options.prefix);
-
-  for (final Api api in root.apis) {
-    indent.writeln(
-        '$_docCommentPrefix The codec used by ${_className(options.prefix, api.name)}.');
-    indent.writeln(
-        'NSObject<FlutterMessageCodec> *${_getCodecGetterName(options.prefix, api.name)}(void);');
-    indent.addln('');
-    if (api.location == ApiLocation.host) {
-      _writeHostApiDeclaration(indent, api, options, root);
-    } else if (api.location == ApiLocation.flutter) {
-      _writeFlutterApiDeclaration(indent, api, options, root);
-    }
-  }
-
-  indent.writeln('NS_ASSUME_NONNULL_END');
-}
-
-String _listGetter(List<String> classNames, String list, NamedType field,
+String _listGetter(Set<String> customClassNames, String list, NamedType field,
     int index, String? prefix) {
-  if (classNames.contains(field.type.baseName)) {
+  if (customClassNames.contains(field.type.baseName)) {
     String className = field.type.baseName;
     if (prefix != null) {
       className = '$prefix$className';
@@ -642,11 +1029,11 @@
   }
 }
 
-String _arrayValue(
-    List<String> classNames, List<String> enumNames, NamedType field) {
-  if (classNames.contains(field.type.baseName)) {
+String _arrayValue(Set<String> customClassNames, Set<String> customEnumNames,
+    NamedType field) {
+  if (customClassNames.contains(field.type.baseName)) {
     return '(self.${field.name} ? [self.${field.name} toList] : [NSNull null])';
-  } else if (enumNames.contains(field.type.baseName)) {
+  } else if (customEnumNames.contains(field.type.baseName)) {
     return '@(self.${field.name})';
   } else {
     return '(self.${field.name} ?: [NSNull null])';
@@ -660,386 +1047,72 @@
 String _getSafeArgName(int count, NamedType arg) =>
     arg.name.isEmpty ? 'arg$count' : 'arg_${arg.name}';
 
-/// Writes the definition code for a host [Api].
-/// See also: [_writeHostApiDeclaration]
-void _writeHostApiSource(
-    Indent indent, ObjcOptions options, Api api, Root root) {
-  assert(api.location == ApiLocation.host);
-  final String apiName = _className(options.prefix, api.name);
-
-  void writeChannelAllocation(Method func, String varName, String? taskQueue) {
-    indent.writeln('FlutterBasicMessageChannel *$varName =');
-    indent.inc();
-    indent.writeln('[[FlutterBasicMessageChannel alloc]');
-    indent.inc();
-    indent.writeln('initWithName:@"${makeChannelName(api, func)}"');
-    indent.writeln('binaryMessenger:binaryMessenger');
-    indent.write('codec:');
-    indent.add('${_getCodecGetterName(options.prefix, api.name)}()');
-
-    if (taskQueue != null) {
-      indent.addln('');
-      indent.addln('taskQueue:$taskQueue];');
-    } else {
-      indent.addln('];');
-    }
-    indent.dec();
-    indent.dec();
-  }
-
-  void writeChannelApiBinding(Method func, String channel) {
-    void unpackArgs(String variable, Iterable<String> argNames) {
-      indent.writeln('NSArray *args = $variable;');
-      map3(wholeNumbers.take(func.arguments.length), argNames, func.arguments,
-          (int count, String argName, NamedType arg) {
-        if (isEnum(root, arg.type)) {
-          return '${_className(options.prefix, arg.type.baseName)} $argName = [GetNullableObjectAtIndex(args, $count) integerValue];';
-        } else {
-          final _ObjcPtr argType =
-              _objcTypeForDartType(options.prefix, arg.type);
-          return '${argType.ptr}$argName = GetNullableObjectAtIndex(args, $count);';
-        }
-      }).forEach(indent.writeln);
-    }
-
-    void writeAsyncBindings(Iterable<String> selectorComponents,
-        String callSignature, _ObjcPtr returnType) {
-      if (func.returnType.isVoid) {
-        const String callback = 'callback(wrapResult(nil, error));';
-        if (func.arguments.isEmpty) {
-          indent.writeScoped(
-              '[api ${selectorComponents.first}:^(FlutterError *_Nullable error) {',
-              '}];', () {
-            indent.writeln(callback);
-          });
-        } else {
-          indent.writeScoped(
-              '[api $callSignature ${selectorComponents.last}:^(FlutterError *_Nullable error) {',
-              '}];', () {
-            indent.writeln(callback);
-          });
-        }
-      } else {
-        const String callback = 'callback(wrapResult(output, error));';
-        if (func.arguments.isEmpty) {
-          indent.writeScoped(
-              '[api ${selectorComponents.first}:^(${returnType.ptr}_Nullable output, FlutterError *_Nullable error) {',
-              '}];', () {
-            indent.writeln(callback);
-          });
-        } else {
-          indent.writeScoped(
-              '[api $callSignature ${selectorComponents.last}:^(${returnType.ptr}_Nullable output, FlutterError *_Nullable error) {',
-              '}];', () {
-            indent.writeln(callback);
-          });
-        }
-      }
-    }
-
-    void writeSyncBindings(String call, _ObjcPtr returnType) {
-      indent.writeln('FlutterError *error;');
-      if (func.returnType.isVoid) {
-        indent.writeln('$call;');
-        indent.writeln('callback(wrapResult(nil, error));');
-      } else {
-        indent.writeln('${returnType.ptr}output = $call;');
-        indent.writeln('callback(wrapResult(output, error));');
-      }
-    }
-
-    // TODO(gaaclarke): Incorporate this into _getSelectorComponents.
-    final String lastSelectorComponent =
-        func.isAsynchronous ? 'completion' : 'error';
-    final String selector = _getSelector(func, lastSelectorComponent);
-    indent.writeln(
-        'NSCAssert([api respondsToSelector:@selector($selector)], @"$apiName api (%@) doesn\'t respond to @selector($selector)", api);');
-    indent.write(
-        '[$channel setMessageHandler:^(id _Nullable message, FlutterReply callback) ');
-    indent.scoped('{', '}];', () {
-      final _ObjcPtr returnType =
-          _objcTypeForDartType(options.prefix, func.returnType);
-      final Iterable<String> selectorComponents =
-          _getSelectorComponents(func, lastSelectorComponent);
-      final Iterable<String> argNames =
-          indexMap(func.arguments, _getSafeArgName);
-      final String callSignature =
-          map2(selectorComponents.take(argNames.length), argNames,
-              (String selectorComponent, String argName) {
-        return '$selectorComponent:$argName';
-      }).join(' ');
-      if (func.arguments.isNotEmpty) {
-        unpackArgs('message', argNames);
-      }
-      if (func.isAsynchronous) {
-        writeAsyncBindings(selectorComponents, callSignature, returnType);
-      } else {
-        final String syncCall = func.arguments.isEmpty
-            ? '[api ${selectorComponents.first}:&error]'
-            : '[api $callSignature error:&error]';
-        writeSyncBindings(syncCall, returnType);
-      }
-    });
-  }
-
-  const String channelName = 'channel';
-  indent.write(
-      'void ${apiName}Setup(id<FlutterBinaryMessenger> binaryMessenger, NSObject<$apiName> *api) ');
-  indent.scoped('{', '}', () {
-    for (final Method func in api.methods) {
-      indent.write('');
-      addDocumentationComments(
-          indent, func.documentationComments, _docCommentSpec);
-
-      indent.scoped('{', '}', () {
-        String? taskQueue;
-        if (func.taskQueueType != TaskQueueType.serial) {
-          taskQueue = 'taskQueue';
-          indent.writeln(
-              'NSObject<FlutterTaskQueue> *$taskQueue = [binaryMessenger makeBackgroundTaskQueue];');
-        }
-        writeChannelAllocation(func, channelName, taskQueue);
-        indent.write('if (api) ');
-        indent.scoped('{', '}', () {
-          writeChannelApiBinding(func, channelName);
-        });
-        indent.write('else ');
-        indent.scoped('{', '}', () {
-          indent.writeln('[$channelName setMessageHandler:nil];');
-        });
-      });
-    }
-  });
-}
-
-/// Writes the definition code for a flutter [Api].
-/// See also: [_writeFlutterApiDeclaration]
-void _writeFlutterApiSource(
-    Indent indent, ObjcOptions options, Api api, Root root) {
-  assert(api.location == ApiLocation.flutter);
-  final String apiName = _className(options.prefix, api.name);
-
-  void writeExtension() {
-    indent.writeln('@interface $apiName ()');
-    indent.writeln(
-        '@property (nonatomic, strong) NSObject<FlutterBinaryMessenger> *binaryMessenger;');
-    indent.writeln('@end');
-  }
-
-  void writeInitializer() {
-    indent.write(
-        '- (instancetype)initWithBinaryMessenger:(NSObject<FlutterBinaryMessenger> *)binaryMessenger ');
-    indent.scoped('{', '}', () {
-      indent.writeln('self = [super init];');
-      indent.write('if (self) ');
-      indent.scoped('{', '}', () {
-        indent.writeln('_binaryMessenger = binaryMessenger;');
-      });
-      indent.writeln('return self;');
-    });
-  }
-
-  void writeMethod(Method func) {
-    final _ObjcPtr returnType =
-        _objcTypeForDartType(options.prefix, func.returnType);
-    final String callbackType = _callbackForType(func.returnType, returnType);
-
-    String argNameFunc(int count, NamedType arg) => _getSafeArgName(count, arg);
-    final Iterable<String> argNames = indexMap(func.arguments, argNameFunc);
-    String sendArgument;
-    if (func.arguments.isEmpty) {
-      sendArgument = 'nil';
-    } else {
-      String makeVarOrNSNullExpression(String x) => '$x ?: [NSNull null]';
-      sendArgument = '@[${argNames.map(makeVarOrNSNullExpression).join(', ')}]';
-    }
-    indent.write(_makeObjcSignature(
-      func: func,
-      options: options,
-      returnType: 'void',
-      lastArgName: 'completion',
-      lastArgType: callbackType,
-      argNameFunc: argNameFunc,
-      isEnum: (TypeDeclaration t) => isEnum(root, t),
-    ));
-    indent.scoped(' {', '}', () {
-      indent.writeln('FlutterBasicMessageChannel *channel =');
-      indent.inc();
-      indent.writeln('[FlutterBasicMessageChannel');
-      indent.inc();
-      indent.writeln('messageChannelWithName:@"${makeChannelName(api, func)}"');
-      indent.writeln('binaryMessenger:self.binaryMessenger');
-      indent.write('codec:${_getCodecGetterName(options.prefix, api.name)}()');
-      indent.addln('];');
-      indent.dec();
-      indent.dec();
-      indent.write('[channel sendMessage:$sendArgument reply:^(id reply) ');
-      indent.scoped('{', '}];', () {
-        if (func.returnType.isVoid) {
-          indent.writeln('completion(nil);');
-        } else {
-          indent.writeln('${returnType.ptr}output = reply;');
-          indent.writeln('completion(output, nil);');
-        }
-      });
-    });
-  }
-
-  writeExtension();
-  indent.addln('');
-  indent.writeln('@implementation $apiName');
-  indent.addln('');
-  writeInitializer();
-  api.methods.forEach(writeMethod);
+void _writeExtension(Indent indent, String apiName) {
+  indent.writeln('@interface $apiName ()');
+  indent.writeln(
+      '@property (nonatomic, strong) NSObject<FlutterBinaryMessenger> *binaryMessenger;');
   indent.writeln('@end');
 }
 
-/// Generates the ".m" file for the AST represented by [root] to [sink] with the
-/// provided [options].
-void generateObjcSource(ObjcOptions options, Root root, StringSink sink) {
-  final Indent indent = Indent(sink);
-  final List<String> classNames =
-      root.classes.map((Class x) => x.name).toList();
-  final List<String> enumNames = root.enums.map((Enum x) => x.name).toList();
-
-  void writeHeader() {
-    if (options.copyrightHeader != null) {
-      addLines(indent, options.copyrightHeader!, linePrefix: '// ');
-    }
-    indent.writeln('// $generatedCodeWarning');
-    indent.writeln('// $seeAlsoWarning');
-  }
-
-  void writeImports() {
-    indent.writeln('#import "${options.headerIncludePath}"');
-    indent.writeln('#import <Flutter/Flutter.h>');
-  }
-
-  void writeArcEnforcer() {
-    indent.writeln('#if !__has_feature(objc_arc)');
-    indent.writeln('#error File requires ARC to be enabled.');
-    indent.writeln('#endif');
-  }
-
-  void writeHelperFunctions() {
-    indent.format('''
-static NSArray *wrapResult(id result, FlutterError *error) {
-\tif (error) {
-\t\treturn @[ error.code ?: [NSNull null], error.message ?: [NSNull null], error.details ?: [NSNull null] ];
-\t}
-\treturn @[ result ?: [NSNull null]  ];
-}''');
-    indent.format('''
-static id GetNullableObject(NSDictionary* dict, id key) {
-\tid result = dict[key];
-\treturn (result == [NSNull null]) ? nil : result;
+void _writeInitializer(Indent indent) {
+  indent.write(
+      '- (instancetype)initWithBinaryMessenger:(NSObject<FlutterBinaryMessenger> *)binaryMessenger ');
+  indent.scoped('{', '}', () {
+    indent.writeln('self = [super init];');
+    indent.write('if (self) ');
+    indent.scoped('{', '}', () {
+      indent.writeln('_binaryMessenger = binaryMessenger;');
+    });
+    indent.writeln('return self;');
+  });
 }
-static id GetNullableObjectAtIndex(NSArray* array, NSInteger key) {
-\tid result = array[key];
-\treturn (result == [NSNull null]) ? nil : result;
-}
-''');
+
+void _writeMethod(ObjcOptions languageOptions, Root root, Indent indent,
+    Api api, Method func) {
+  final _ObjcPtr returnType =
+      _objcTypeForDartType(languageOptions.prefix, func.returnType);
+  final String callbackType = _callbackForType(func.returnType, returnType);
+
+  String argNameFunc(int count, NamedType arg) => _getSafeArgName(count, arg);
+  final Iterable<String> argNames = indexMap(func.arguments, argNameFunc);
+  String sendArgument;
+  if (func.arguments.isEmpty) {
+    sendArgument = 'nil';
+  } else {
+    String makeVarOrNSNullExpression(String x) => '$x ?: [NSNull null]';
+    sendArgument = '@[${argNames.map(makeVarOrNSNullExpression).join(', ')}]';
   }
-
-  void writeDataClassExtension(Class klass) {
-    final String className = _className(options.prefix, klass.name);
-    indent.writeln('@interface $className ()');
-    indent.writeln('+ ($className *)fromList:(NSArray *)list;');
-    indent
-        .writeln('+ (nullable $className *)nullableFromList:(NSArray *)list;');
-    indent.writeln('- (NSArray *)toList;');
-    indent.writeln('@end');
-  }
-
-  void writeDataClassImplementation(Class klass) {
-    final String className = _className(options.prefix, klass.name);
-    void writeInitializer() {
-      _writeInitializerDeclaration(
-          indent, klass, root.classes, root.enums, options.prefix);
-      indent.writeScoped(' {', '}', () {
-        const String result = 'pigeonResult';
-        indent.writeln('$className* $result = [[$className alloc] init];');
-        for (final NamedType field in getFieldsInSerializationOrder(klass)) {
-          indent.writeln('$result.${field.name} = ${field.name};');
-        }
-        indent.writeln('return $result;');
-      });
-    }
-
-    void writeFromList() {
-      indent.write('+ ($className *)fromList:(NSArray *)list ');
-      indent.scoped('{', '}', () {
-        const String resultName = 'pigeonResult';
-        indent.writeln('$className *$resultName = [[$className alloc] init];');
-        enumerate(getFieldsInSerializationOrder(klass),
-            (int index, final NamedType field) {
-          if (enumNames.contains(field.type.baseName)) {
-            indent.writeln(
-                '$resultName.${field.name} = [${_listGetter(classNames, 'list', field, index, options.prefix)} integerValue];');
-          } else {
-            indent.writeln(
-                '$resultName.${field.name} = ${_listGetter(classNames, 'list', field, index, options.prefix)};');
-            if (!field.type.isNullable) {
-              indent
-                  .writeln('NSAssert($resultName.${field.name} != nil, @"");');
-            }
-          }
-        });
-        indent.writeln('return $resultName;');
-      });
-
-      indent.writeln(
-          '+ (nullable $className *)nullableFromList:(NSArray *)list { return (list) ? [$className fromList:list] : nil; }');
-    }
-
-    void writeToList() {
-      indent.write('- (NSArray *)toList ');
-      indent.scoped('{', '}', () {
-        indent.write('return');
-        indent.scoped(' @[', '];', () {
-          for (final NamedType field in klass.fields) {
-            indent.writeln('${_arrayValue(classNames, enumNames, field)},');
-          }
-        });
-      });
-    }
-
-    indent.writeln('@implementation $className');
-    writeInitializer();
-    writeFromList();
-    writeToList();
-    indent.writeln('@end');
-  }
-
-  void writeApi(Api api) {
-    final String codecName = _getCodecName(options.prefix, api.name);
-    if (getCodecClasses(api, root).isNotEmpty) {
-      _writeCodec(indent, codecName, options, api, root);
-      indent.addln('');
-    }
-    _writeCodecGetter(indent, codecName, options, api, root);
-    indent.addln('');
-    if (api.location == ApiLocation.host) {
-      _writeHostApiSource(indent, options, api, root);
-    } else if (api.location == ApiLocation.flutter) {
-      _writeFlutterApiSource(indent, options, api, root);
-    }
-  }
-
-  writeHeader();
-  writeImports();
-  indent.writeln('');
-  writeArcEnforcer();
-  indent.addln('');
-  writeHelperFunctions();
-  indent.addln('');
-  root.classes.forEach(writeDataClassExtension);
-  indent.writeln('');
-  for (final Class klass in root.classes) {
-    writeDataClassImplementation(klass);
-    indent.writeln('');
-  }
-  root.apis.forEach(writeApi);
+  indent.write(_makeObjcSignature(
+    func: func,
+    options: languageOptions,
+    returnType: 'void',
+    lastArgName: 'completion',
+    lastArgType: callbackType,
+    argNameFunc: argNameFunc,
+    isEnum: (TypeDeclaration t) => isEnum(root, t),
+  ));
+  indent.scoped(' {', '}', () {
+    indent.writeln('FlutterBasicMessageChannel *channel =');
+    indent.inc();
+    indent.writeln('[FlutterBasicMessageChannel');
+    indent.inc();
+    indent.writeln('messageChannelWithName:@"${makeChannelName(api, func)}"');
+    indent.writeln('binaryMessenger:self.binaryMessenger');
+    indent.write(
+        'codec:${_getCodecGetterName(languageOptions.prefix, api.name)}()');
+    indent.addln('];');
+    indent.dec();
+    indent.dec();
+    indent.write('[channel sendMessage:$sendArgument reply:^(id reply) ');
+    indent.scoped('{', '}];', () {
+      if (func.returnType.isVoid) {
+        indent.writeln('completion(nil);');
+      } else {
+        indent.writeln('${returnType.ptr}output = reply;');
+        indent.writeln('completion(output, nil);');
+      }
+    });
+  });
 }
 
 /// Looks through the AST for features that aren't supported by the ObjC
diff --git a/packages/pigeon/lib/pigeon_lib.dart b/packages/pigeon/lib/pigeon_lib.dart
index 5082dd1..04c0b95 100644
--- a/packages/pigeon/lib/pigeon_lib.dart
+++ b/packages/pigeon/lib/pigeon_lib.dart
@@ -426,7 +426,7 @@
       StringSink sink, PigeonOptions options, Root root, FileType fileType) {
     final DartOptions dartOptionsWithHeader = _dartOptionsWithCopyrightHeader(
         options.dartOptions, options.copyrightHeader);
-    final DartGenerator generator = DartGenerator();
+    const DartGenerator generator = DartGenerator();
     generator.generate(dartOptionsWithHeader, root, sink);
   }
 
@@ -455,12 +455,8 @@
       dartOutPath: options.dartOut,
       testOutPath: options.dartTestOut,
     );
-    final DartGenerator testGenerator = DartGenerator();
-    testGenerator.generateTest(
-      dartOptionsWithHeader,
-      root,
-      sink,
-    );
+    const DartGenerator testGenerator = DartGenerator();
+    testGenerator.generateTest(dartOptionsWithHeader, root, sink);
   }
 
   @override
@@ -497,7 +493,7 @@
     final OutputFileOptions<ObjcOptions> outputFileOptions =
         OutputFileOptions<ObjcOptions>(
             fileType: fileType, languageOptions: objcOptionsWithHeader);
-    final ObjcGenerator generator = ObjcGenerator();
+    const ObjcGenerator generator = ObjcGenerator();
     generator.generate(outputFileOptions, root, sink);
   }
 
@@ -532,7 +528,7 @@
         copyrightHeader: options.copyrightHeader != null
             ? _lineReader(options.copyrightHeader!)
             : null));
-    final JavaGenerator generator = JavaGenerator();
+    const JavaGenerator generator = JavaGenerator();
     generator.generate(javaOptions, root, sink);
   }
 
@@ -560,7 +556,7 @@
         copyrightHeader: options.copyrightHeader != null
             ? _lineReader(options.copyrightHeader!)
             : null));
-    final SwiftGenerator generator = SwiftGenerator();
+    const SwiftGenerator generator = SwiftGenerator();
     generator.generate(swiftOptions, root, sink);
   }
 
@@ -593,7 +589,7 @@
     final OutputFileOptions<CppOptions> outputFileOptions =
         OutputFileOptions<CppOptions>(
             fileType: fileType, languageOptions: cppOptionsWithHeader);
-    final CppGenerator generator = CppGenerator();
+    const CppGenerator generator = CppGenerator();
     generator.generate(outputFileOptions, root, sink);
   }
 
@@ -627,7 +623,7 @@
         copyrightHeader: options.copyrightHeader != null
             ? _lineReader(options.copyrightHeader!)
             : null));
-    final KotlinGenerator generator = KotlinGenerator();
+    const KotlinGenerator generator = KotlinGenerator();
     generator.generate(kotlinOptions, root, sink);
   }
 
diff --git a/packages/pigeon/lib/swift_generator.dart b/packages/pigeon/lib/swift_generator.dart
index 77fff7e..46e1400 100644
--- a/packages/pigeon/lib/swift_generator.dart
+++ b/packages/pigeon/lib/swift_generator.dart
@@ -49,230 +49,565 @@
 }
 
 /// Class that manages all Swift code generation.
-class SwiftGenerator extends Generator<SwiftOptions> {
+class SwiftGenerator extends StructuredGenerator<SwiftOptions> {
   /// Instantiates a Swift Generator.
-  SwiftGenerator();
+  const SwiftGenerator();
 
-  /// Generates Swift files with specified [SwiftOptions]
   @override
-  void generate(SwiftOptions languageOptions, Root root, StringSink sink,
-      {FileType fileType = FileType.na}) {
-    assert(fileType == FileType.na);
-    generateSwift(languageOptions, root, sink);
+  void writeFilePrologue(
+      SwiftOptions generatorOptions, Root root, Indent indent) {
+    if (generatorOptions.copyrightHeader != null) {
+      addLines(indent, generatorOptions.copyrightHeader!, linePrefix: '// ');
+    }
+    indent.writeln('// $generatedCodeWarning');
+    indent.writeln('// $seeAlsoWarning');
+    indent.addln('');
   }
+
+  @override
+  void writeFileImports(
+      SwiftOptions generatorOptions, Root root, Indent indent) {
+    indent.writeln('import Foundation');
+    indent.format('''
+#if os(iOS)
+import Flutter
+#elseif os(macOS)
+import FlutterMacOS
+#else
+#error("Unsupported platform.")
+#endif
+''');
+    indent.writeln('');
+  }
+
+  @override
+  void writeEnum(
+      SwiftOptions generatorOptions, Root root, Indent indent, Enum anEnum) {
+    indent.writeln('');
+    addDocumentationComments(
+        indent, anEnum.documentationComments, _docCommentSpec);
+
+    indent.write('enum ${anEnum.name}: Int ');
+    indent.scoped('{', '}', () {
+      enumerate(anEnum.members, (int index, final EnumMember member) {
+        addDocumentationComments(
+            indent, member.documentationComments, _docCommentSpec);
+        indent.writeln('case ${_camelCase(member.name)} = $index');
+      });
+    });
+  }
+
+  @override
+  void writeDataClass(
+      SwiftOptions generatorOptions, Root root, Indent indent, Class klass) {
+    final Set<String> customClassNames =
+        root.classes.map((Class x) => x.name).toSet();
+    final Set<String> customEnumNames =
+        root.enums.map((Enum x) => x.name).toSet();
+
+    const List<String> generatedComments = <String>[
+      ' Generated class from Pigeon that represents data sent in messages.'
+    ];
+    indent.addln('');
+    addDocumentationComments(
+        indent, klass.documentationComments, _docCommentSpec,
+        generatorComments: generatedComments);
+
+    indent.write('struct ${klass.name} ');
+    indent.scoped('{', '}', () {
+      getFieldsInSerializationOrder(klass).forEach((NamedType field) {
+        _writeClassField(indent, field);
+      });
+
+      indent.writeln('');
+      writeClassDecode(generatorOptions, root, indent, klass, customClassNames,
+          customEnumNames);
+      writeClassEncode(generatorOptions, root, indent, klass, customClassNames,
+          customEnumNames);
+    });
+  }
+
+  @override
+  void writeClassEncode(
+    SwiftOptions generatorOptions,
+    Root root,
+    Indent indent,
+    Class klass,
+    Set<String> customClassNames,
+    Set<String> customEnumNames,
+  ) {
+    indent.write('func toList() -> [Any?] ');
+    indent.scoped('{', '}', () {
+      indent.write('return ');
+      indent.scoped('[', ']', () {
+        for (final NamedType field in getFieldsInSerializationOrder(klass)) {
+          final HostDatatype hostDatatype = _getHostDatatype(root, field);
+          String toWriteValue = '';
+          final String fieldName = field.name;
+          final String nullsafe = field.type.isNullable ? '?' : '';
+          if (!hostDatatype.isBuiltin &&
+              customClassNames.contains(field.type.baseName)) {
+            toWriteValue = '$fieldName$nullsafe.toList()';
+          } else if (!hostDatatype.isBuiltin &&
+              customEnumNames.contains(field.type.baseName)) {
+            toWriteValue = '$fieldName$nullsafe.rawValue';
+          } else {
+            toWriteValue = field.name;
+          }
+
+          indent.writeln('$toWriteValue,');
+        }
+      });
+    });
+  }
+
+  @override
+  void writeClassDecode(
+    SwiftOptions generatorOptions,
+    Root root,
+    Indent indent,
+    Class klass,
+    Set<String> customClassNames,
+    Set<String> customEnumNames,
+  ) {
+    final String className = klass.name;
+    indent.write('static func fromList(_ list: [Any?]) -> $className? ');
+
+    indent.scoped('{', '}', () {
+      enumerate(getFieldsInSerializationOrder(klass),
+          (int index, final NamedType field) {
+        final HostDatatype hostDatatype = _getHostDatatype(root, field);
+
+        final String listValue = 'list[$index]';
+        final String fieldType = _swiftTypeForDartType(field.type);
+
+        if (field.type.isNullable) {
+          if (!hostDatatype.isBuiltin &&
+              customClassNames.contains(field.type.baseName)) {
+            indent.writeln('var ${field.name}: $fieldType? = nil');
+            indent.write('if let ${field.name}List = $listValue as? [Any?] ');
+            indent.scoped('{', '}', () {
+              indent.writeln(
+                  '${field.name} = $fieldType.fromList(${field.name}List)');
+            });
+          } else if (!hostDatatype.isBuiltin &&
+              customEnumNames.contains(field.type.baseName)) {
+            indent.writeln('var ${field.name}: $fieldType? = nil');
+            indent.write('if let ${field.name}RawValue = $listValue as? Int ');
+            indent.scoped('{', '}', () {
+              indent.writeln(
+                  '${field.name} = $fieldType(rawValue: ${field.name}RawValue)');
+            });
+          } else {
+            indent.writeln('let ${field.name} = $listValue as? $fieldType ');
+          }
+        } else {
+          if (!hostDatatype.isBuiltin &&
+              customClassNames.contains(field.type.baseName)) {
+            indent.writeln(
+                'let ${field.name} = $fieldType.fromList($listValue as! [Any?])!');
+          } else if (!hostDatatype.isBuiltin &&
+              customEnumNames.contains(field.type.baseName)) {
+            indent.writeln(
+                'let ${field.name} = $fieldType(rawValue: $listValue as! Int)!');
+          } else {
+            indent.writeln('let ${field.name} = $listValue as! $fieldType');
+          }
+        }
+      });
+
+      indent.writeln('');
+      indent.write('return ');
+      indent.scoped('$className(', ')', () {
+        for (final NamedType field in getFieldsInSerializationOrder(klass)) {
+          final String comma =
+              getFieldsInSerializationOrder(klass).last == field ? '' : ',';
+          indent.writeln('${field.name}: ${field.name}$comma');
+        }
+      });
+    });
+  }
+
+  void _writeClassField(Indent indent, NamedType field) {
+    addDocumentationComments(
+        indent, field.documentationComments, _docCommentSpec);
+
+    indent.write(
+        'var ${field.name}: ${_nullsafeSwiftTypeForDartType(field.type)}');
+    final String defaultNil = field.type.isNullable ? ' = nil' : '';
+    indent.addln(defaultNil);
+  }
+
+  @override
+  void writeApis(
+    SwiftOptions generatorOptions,
+    Root root,
+    Indent indent,
+  ) {
+    if (root.apis.any((Api api) =>
+        api.location == ApiLocation.host &&
+        api.methods.any((Method it) => it.isAsynchronous))) {
+      indent.addln('');
+    }
+    super.writeApis(generatorOptions, root, indent);
+  }
+
+  /// Writes the code for a flutter [Api], [api].
+  /// Example:
+  /// class Foo {
+  ///   private let binaryMessenger: FlutterBinaryMessenger
+  ///   init(binaryMessenger: FlutterBinaryMessenger) {...}
+  ///   func add(x: Int32, y: Int32, completion: @escaping (Int32?) -> Void) {...}
+  /// }
+  @override
+  void writeFlutterApi(
+    SwiftOptions generatorOptions,
+    Root root,
+    Indent indent,
+    Api api,
+  ) {
+    assert(api.location == ApiLocation.flutter);
+    final bool isCustomCodec = getCodecClasses(api, root).isNotEmpty;
+    if (isCustomCodec) {
+      _writeCodec(indent, api, root);
+    }
+    const List<String> generatedComments = <String>[
+      ' Generated class from Pigeon that represents Flutter messages that can be called from Swift.'
+    ];
+    addDocumentationComments(indent, api.documentationComments, _docCommentSpec,
+        generatorComments: generatedComments);
+
+    indent.write('class ${api.name} ');
+    indent.scoped('{', '}', () {
+      indent.writeln('private let binaryMessenger: FlutterBinaryMessenger');
+      indent.write('init(binaryMessenger: FlutterBinaryMessenger)');
+      indent.scoped('{', '}', () {
+        indent.writeln('self.binaryMessenger = binaryMessenger');
+      });
+      final String codecName = _getCodecName(api);
+      String codecArgumentString = '';
+      if (getCodecClasses(api, root).isNotEmpty) {
+        codecArgumentString = ', codec: codec';
+        indent.write('var codec: FlutterStandardMessageCodec ');
+        indent.scoped('{', '}', () {
+          indent.writeln('return $codecName.shared');
+        });
+      }
+      for (final Method func in api.methods) {
+        final String channelName = makeChannelName(api, func);
+        final String returnType = func.returnType.isVoid
+            ? ''
+            : _nullsafeSwiftTypeForDartType(func.returnType);
+        String sendArgument;
+        addDocumentationComments(
+            indent, func.documentationComments, _docCommentSpec);
+
+        if (func.arguments.isEmpty) {
+          indent.write(
+              'func ${func.name}(completion: @escaping ($returnType) -> Void) ');
+          sendArgument = 'nil';
+        } else {
+          final Iterable<String> argTypes = func.arguments
+              .map((NamedType e) => _nullsafeSwiftTypeForDartType(e.type));
+          final Iterable<String> argLabels =
+              indexMap(func.arguments, _getArgumentName);
+          final Iterable<String> argNames =
+              indexMap(func.arguments, _getSafeArgumentName);
+          sendArgument = '[${argNames.join(', ')}]';
+          final String argsSignature = map3(
+              argTypes,
+              argLabels,
+              argNames,
+              (String type, String label, String name) =>
+                  '$label $name: $type').join(', ');
+          if (func.returnType.isVoid) {
+            indent.write(
+                'func ${func.name}($argsSignature, completion: @escaping () -> Void) ');
+          } else {
+            indent.write(
+                'func ${func.name}($argsSignature, completion: @escaping ($returnType) -> Void) ');
+          }
+        }
+        indent.scoped('{', '}', () {
+          const String channel = 'channel';
+          indent.writeln(
+              'let $channel = FlutterBasicMessageChannel(name: "$channelName", binaryMessenger: binaryMessenger$codecArgumentString)');
+          indent.write('$channel.sendMessage($sendArgument) ');
+          if (func.returnType.isVoid) {
+            indent.scoped('{ _ in', '}', () {
+              indent.writeln('completion()');
+            });
+          } else {
+            indent.scoped('{ response in', '}', () {
+              indent.writeln(
+                  'let result = ${_castForceUnwrap("response", func.returnType, root)}');
+              indent.writeln('completion(result)');
+            });
+          }
+        });
+      }
+    });
+  }
+
+  /// Write the swift code that represents a host [Api], [api].
+  /// Example:
+  /// protocol Foo {
+  ///   Int32 add(x: Int32, y: Int32)
+  /// }
+  @override
+  void writeHostApi(
+    SwiftOptions generatorOptions,
+    Root root,
+    Indent indent,
+    Api api,
+  ) {
+    assert(api.location == ApiLocation.host);
+
+    final String apiName = api.name;
+
+    final bool isCustomCodec = getCodecClasses(api, root).isNotEmpty;
+    if (isCustomCodec) {
+      _writeCodec(indent, api, root);
+    }
+
+    const List<String> generatedComments = <String>[
+      ' Generated protocol from Pigeon that represents a handler of messages from Flutter.'
+    ];
+    addDocumentationComments(indent, api.documentationComments, _docCommentSpec,
+        generatorComments: generatedComments);
+
+    indent.write('protocol $apiName ');
+    indent.scoped('{', '}', () {
+      for (final Method method in api.methods) {
+        final List<String> argSignature = <String>[];
+        if (method.arguments.isNotEmpty) {
+          final Iterable<String> argTypes = method.arguments
+              .map((NamedType e) => _nullsafeSwiftTypeForDartType(e.type));
+          final Iterable<String> argNames =
+              method.arguments.map((NamedType e) => e.name);
+          argSignature.addAll(
+              map2(argTypes, argNames, (String argType, String argName) {
+            return '$argName: $argType';
+          }));
+        }
+
+        final String returnType = method.returnType.isVoid
+            ? ''
+            : _nullsafeSwiftTypeForDartType(method.returnType);
+        addDocumentationComments(
+            indent, method.documentationComments, _docCommentSpec);
+
+        if (method.isAsynchronous) {
+          argSignature.add('completion: @escaping ($returnType) -> Void');
+          indent.writeln('func ${method.name}(${argSignature.join(', ')})');
+        } else if (method.returnType.isVoid) {
+          indent.writeln('func ${method.name}(${argSignature.join(', ')})');
+        } else {
+          indent.writeln(
+              'func ${method.name}(${argSignature.join(', ')}) -> $returnType');
+        }
+      }
+    });
+
+    indent.addln('');
+    indent.writeln(
+        '$_docCommentPrefix Generated setup class from Pigeon to handle messages through the `binaryMessenger`.');
+    indent.write('class ${apiName}Setup ');
+    indent.scoped('{', '}', () {
+      final String codecName = _getCodecName(api);
+      indent.writeln('$_docCommentPrefix The codec used by $apiName.');
+      String codecArgumentString = '';
+      if (getCodecClasses(api, root).isNotEmpty) {
+        codecArgumentString = ', codec: codec';
+        indent.writeln(
+            'static var codec: FlutterStandardMessageCodec { $codecName.shared }');
+      }
+      indent.writeln(
+          '$_docCommentPrefix Sets up an instance of `$apiName` to handle messages through the `binaryMessenger`.');
+      indent.write(
+          'static func setUp(binaryMessenger: FlutterBinaryMessenger, api: $apiName?) ');
+      indent.scoped('{', '}', () {
+        for (final Method method in api.methods) {
+          final String channelName = makeChannelName(api, method);
+          final String varChannelName = '${method.name}Channel';
+          addDocumentationComments(
+              indent, method.documentationComments, _docCommentSpec);
+
+          indent.writeln(
+              'let $varChannelName = FlutterBasicMessageChannel(name: "$channelName", binaryMessenger: binaryMessenger$codecArgumentString)');
+          indent.write('if let api = api ');
+          indent.scoped('{', '}', () {
+            indent.write('$varChannelName.setMessageHandler ');
+            final String messageVarName =
+                method.arguments.isNotEmpty ? 'message' : '_';
+            indent.scoped('{ $messageVarName, reply in', '}', () {
+              final List<String> methodArgument = <String>[];
+              if (method.arguments.isNotEmpty) {
+                indent.writeln('let args = message as! [Any?]');
+                enumerate(method.arguments, (int index, NamedType arg) {
+                  final String argName = _getSafeArgumentName(index, arg);
+                  final String argIndex = 'args[$index]';
+                  indent.writeln(
+                      'let $argName = ${_castForceUnwrap(argIndex, arg.type, root)}');
+                  methodArgument.add('${arg.name}: $argName');
+                });
+              }
+              final String call =
+                  'api.${method.name}(${methodArgument.join(', ')})';
+              if (method.isAsynchronous) {
+                indent.write('$call ');
+                if (method.returnType.isVoid) {
+                  indent.scoped('{', '}', () {
+                    indent.writeln('reply(wrapResult(nil))');
+                  });
+                } else {
+                  indent.scoped('{ result in', '}', () {
+                    indent.writeln('reply(wrapResult(result))');
+                  });
+                }
+              } else {
+                if (method.returnType.isVoid) {
+                  indent.writeln(call);
+                  indent.writeln('reply(wrapResult(nil))');
+                } else {
+                  indent.writeln('let result = $call');
+                  indent.writeln('reply(wrapResult(result))');
+                }
+              }
+            });
+          }, addTrailingNewline: false);
+          indent.scoped(' else {', '}', () {
+            indent.writeln('$varChannelName.setMessageHandler(nil)');
+          });
+        }
+      });
+    });
+  }
+
+  /// Writes the codec classwill be used for encoding messages for the [api].
+  /// Example:
+  /// private class FooHostApiCodecReader: FlutterStandardReader {...}
+  /// private class FooHostApiCodecWriter: FlutterStandardWriter {...}
+  /// private class FooHostApiCodecReaderWriter: FlutterStandardReaderWriter {...}
+  void _writeCodec(Indent indent, Api api, Root root) {
+    assert(getCodecClasses(api, root).isNotEmpty);
+    final String codecName = _getCodecName(api);
+    final String readerWriterName = '${codecName}ReaderWriter';
+    final String readerName = '${codecName}Reader';
+    final String writerName = '${codecName}Writer';
+
+    // Generate Reader
+    indent.write('private class $readerName: FlutterStandardReader ');
+    indent.scoped('{', '}', () {
+      if (getCodecClasses(api, root).isNotEmpty) {
+        indent.write('override func readValue(ofType type: UInt8) -> Any? ');
+        indent.scoped('{', '}', () {
+          indent.write('switch type ');
+          indent.scoped('{', '}', () {
+            for (final EnumeratedClass customClass
+                in getCodecClasses(api, root)) {
+              indent.write('case ${customClass.enumeration}:');
+              indent.scoped('', '', () {
+                indent.write(
+                    'return ${customClass.name}.fromList(self.readValue() as! [Any])');
+              });
+            }
+            indent.write('default:');
+            indent.scoped('', '', () {
+              indent.writeln('return super.readValue(ofType: type)');
+            });
+          });
+        });
+      }
+    });
+
+    // Generate Writer
+    indent.write('private class $writerName: FlutterStandardWriter ');
+    indent.scoped('{', '}', () {
+      if (getCodecClasses(api, root).isNotEmpty) {
+        indent.write('override func writeValue(_ value: Any) ');
+        indent.scoped('{', '}', () {
+          indent.write('');
+          for (final EnumeratedClass customClass
+              in getCodecClasses(api, root)) {
+            indent.add('if let value = value as? ${customClass.name} ');
+            indent.scoped('{', '} else ', () {
+              indent.writeln('super.writeByte(${customClass.enumeration})');
+              indent.writeln('super.writeValue(value.toList())');
+            }, addTrailingNewline: false);
+          }
+          indent.scoped('{', '}', () {
+            indent.writeln('super.writeValue(value)');
+          });
+        });
+      }
+    });
+    indent.writeln('');
+
+    // Generate ReaderWriter
+    indent
+        .write('private class $readerWriterName: FlutterStandardReaderWriter ');
+    indent.scoped('{', '}', () {
+      indent.write(
+          'override func reader(with data: Data) -> FlutterStandardReader ');
+      indent.scoped('{', '}', () {
+        indent.writeln('return $readerName(data: data)');
+      });
+      indent.writeln('');
+      indent.write(
+          'override func writer(with data: NSMutableData) -> FlutterStandardWriter ');
+      indent.scoped('{', '}', () {
+        indent.writeln('return $writerName(data: data)');
+      });
+    });
+    indent.writeln('');
+
+    // Generate Codec
+    indent.write('class $codecName: FlutterStandardMessageCodec ');
+    indent.scoped('{', '}', () {
+      indent.writeln(
+          'static let shared = $codecName(readerWriter: $readerWriterName())');
+    });
+    indent.addln('');
+  }
+
+  void _writeWrapResult(Indent indent) {
+    indent.addln('');
+    indent.write('private func wrapResult(_ result: Any?) -> [Any?] ');
+    indent.scoped('{', '}', () {
+      indent.writeln('return [result]');
+    });
+  }
+
+  void _writeWrapError(Indent indent) {
+    indent.addln('');
+    indent.write('private func wrapError(_ error: FlutterError) -> [Any?] ');
+    indent.scoped('{', '}', () {
+      indent.write('return ');
+      indent.scoped('[', ']', () {
+        indent.writeln('error.code,');
+        indent.writeln('error.message,');
+        indent.writeln('error.details');
+      });
+    });
+  }
+
+  @override
+  void writeGeneralUtilities(
+      SwiftOptions generatorOptions, Root root, Indent indent) {
+    _writeWrapResult(indent);
+    _writeWrapError(indent);
+  }
+}
+
+HostDatatype _getHostDatatype(Root root, NamedType field) {
+  return getFieldHostDatatype(field, root.classes, root.enums,
+      (TypeDeclaration x) => _swiftTypeForBuiltinDartType(x));
 }
 
 /// Calculates the name of the codec that will be generated for [api].
 String _getCodecName(Api api) => '${api.name}Codec';
 
-/// Writes the codec classwill be used for encoding messages for the [api].
-/// Example:
-/// private class FooHostApiCodecReader: FlutterStandardReader {...}
-/// private class FooHostApiCodecWriter: FlutterStandardWriter {...}
-/// private class FooHostApiCodecReaderWriter: FlutterStandardReaderWriter {...}
-void _writeCodec(Indent indent, Api api, Root root) {
-  assert(getCodecClasses(api, root).isNotEmpty);
-  final String codecName = _getCodecName(api);
-  final String readerWriterName = '${codecName}ReaderWriter';
-  final String readerName = '${codecName}Reader';
-  final String writerName = '${codecName}Writer';
-
-  // Generate Reader
-  indent.write('private class $readerName: FlutterStandardReader ');
-  indent.scoped('{', '}', () {
-    if (getCodecClasses(api, root).isNotEmpty) {
-      indent.write('override func readValue(ofType type: UInt8) -> Any? ');
-      indent.scoped('{', '}', () {
-        indent.write('switch type ');
-        indent.scoped('{', '}', () {
-          for (final EnumeratedClass customClass
-              in getCodecClasses(api, root)) {
-            indent.write('case ${customClass.enumeration}:');
-            indent.scoped('', '', () {
-              indent.write(
-                  'return ${customClass.name}.fromList(self.readValue() as! [Any])');
-            });
-          }
-          indent.write('default:');
-          indent.scoped('', '', () {
-            indent.writeln('return super.readValue(ofType: type)');
-          });
-        });
-      });
-    }
-  });
-
-  // Generate Writer
-  indent.write('private class $writerName: FlutterStandardWriter ');
-  indent.scoped('{', '}', () {
-    if (getCodecClasses(api, root).isNotEmpty) {
-      indent.write('override func writeValue(_ value: Any) ');
-      indent.scoped('{', '}', () {
-        indent.write('');
-        for (final EnumeratedClass customClass in getCodecClasses(api, root)) {
-          indent.add('if let value = value as? ${customClass.name} ');
-          indent.scoped('{', '} else ', () {
-            indent.writeln('super.writeByte(${customClass.enumeration})');
-            indent.writeln('super.writeValue(value.toList())');
-          }, addTrailingNewline: false);
-        }
-        indent.scoped('{', '}', () {
-          indent.writeln('super.writeValue(value)');
-        });
-      });
-    }
-  });
-  indent.writeln('');
-
-  // Generate ReaderWriter
-  indent.write('private class $readerWriterName: FlutterStandardReaderWriter ');
-  indent.scoped('{', '}', () {
-    indent.write(
-        'override func reader(with data: Data) -> FlutterStandardReader ');
-    indent.scoped('{', '}', () {
-      indent.writeln('return $readerName(data: data)');
-    });
-    indent.writeln('');
-    indent.write(
-        'override func writer(with data: NSMutableData) -> FlutterStandardWriter ');
-    indent.scoped('{', '}', () {
-      indent.writeln('return $writerName(data: data)');
-    });
-  });
-  indent.writeln('');
-
-  // Generate Codec
-  indent.write('class $codecName: FlutterStandardMessageCodec ');
-  indent.scoped('{', '}', () {
-    indent.writeln(
-        'static let shared = $codecName(readerWriter: $readerWriterName())');
-  });
-}
-
-/// Write the swift code that represents a host [Api], [api].
-/// Example:
-/// protocol Foo {
-///   Int32 add(x: Int32, y: Int32)
-/// }
-void _writeHostApi(Indent indent, Api api, Root root) {
-  assert(api.location == ApiLocation.host);
-
-  final String apiName = api.name;
-
-  const List<String> generatedComments = <String>[
-    ' Generated protocol from Pigeon that represents a handler of messages from Flutter.'
-  ];
-  addDocumentationComments(indent, api.documentationComments, _docCommentSpec,
-      generatorComments: generatedComments);
-
-  indent.write('protocol $apiName ');
-  indent.scoped('{', '}', () {
-    for (final Method method in api.methods) {
-      final List<String> argSignature = <String>[];
-      if (method.arguments.isNotEmpty) {
-        final Iterable<String> argTypes = method.arguments
-            .map((NamedType e) => _nullsafeSwiftTypeForDartType(e.type));
-        final Iterable<String> argNames =
-            method.arguments.map((NamedType e) => e.name);
-        argSignature
-            .addAll(map2(argTypes, argNames, (String argType, String argName) {
-          return '$argName: $argType';
-        }));
-      }
-
-      final String returnType = method.returnType.isVoid
-          ? ''
-          : _nullsafeSwiftTypeForDartType(method.returnType);
-      addDocumentationComments(
-          indent, method.documentationComments, _docCommentSpec);
-
-      if (method.isAsynchronous) {
-        argSignature.add('completion: @escaping ($returnType) -> Void');
-        indent.writeln('func ${method.name}(${argSignature.join(', ')})');
-      } else if (method.returnType.isVoid) {
-        indent.writeln('func ${method.name}(${argSignature.join(', ')})');
-      } else {
-        indent.writeln(
-            'func ${method.name}(${argSignature.join(', ')}) -> $returnType');
-      }
-    }
-  });
-
-  indent.addln('');
-  indent.writeln(
-      '$_docCommentPrefix Generated setup class from Pigeon to handle messages through the `binaryMessenger`.');
-  indent.write('class ${apiName}Setup ');
-  indent.scoped('{', '}', () {
-    final String codecName = _getCodecName(api);
-    indent.writeln('$_docCommentPrefix The codec used by $apiName.');
-    String codecArgumentString = '';
-    if (getCodecClasses(api, root).isNotEmpty) {
-      codecArgumentString = ', codec: codec';
-      indent.writeln(
-          'static var codec: FlutterStandardMessageCodec { $codecName.shared }');
-    }
-    indent.writeln(
-        '$_docCommentPrefix Sets up an instance of `$apiName` to handle messages through the `binaryMessenger`.');
-    indent.write(
-        'static func setUp(binaryMessenger: FlutterBinaryMessenger, api: $apiName?) ');
-    indent.scoped('{', '}', () {
-      for (final Method method in api.methods) {
-        final String channelName = makeChannelName(api, method);
-        final String varChannelName = '${method.name}Channel';
-        addDocumentationComments(
-            indent, method.documentationComments, _docCommentSpec);
-
-        indent.writeln(
-            'let $varChannelName = FlutterBasicMessageChannel(name: "$channelName", binaryMessenger: binaryMessenger$codecArgumentString)');
-        indent.write('if let api = api ');
-        indent.scoped('{', '}', () {
-          indent.write('$varChannelName.setMessageHandler ');
-          final String messageVarName =
-              method.arguments.isNotEmpty ? 'message' : '_';
-          indent.scoped('{ $messageVarName, reply in', '}', () {
-            final List<String> methodArgument = <String>[];
-            if (method.arguments.isNotEmpty) {
-              indent.writeln('let args = message as! [Any?]');
-              enumerate(method.arguments, (int index, NamedType arg) {
-                final String argName = _getSafeArgumentName(index, arg);
-                final String argIndex = 'args[$index]';
-                indent.writeln(
-                    'let $argName = ${_castForceUnwrap(argIndex, arg.type, root)}');
-                methodArgument.add('${arg.name}: $argName');
-              });
-            }
-            final String call =
-                'api.${method.name}(${methodArgument.join(', ')})';
-            if (method.isAsynchronous) {
-              indent.write('$call ');
-              if (method.returnType.isVoid) {
-                indent.scoped('{', '}', () {
-                  indent.writeln('reply(wrapResult(nil))');
-                });
-              } else {
-                indent.scoped('{ result in', '}', () {
-                  indent.writeln('reply(wrapResult(result))');
-                });
-              }
-            } else {
-              if (method.returnType.isVoid) {
-                indent.writeln(call);
-                indent.writeln('reply(wrapResult(nil))');
-              } else {
-                indent.writeln('let result = $call');
-                indent.writeln('reply(wrapResult(result))');
-              }
-            }
-          });
-        }, addTrailingNewline: false);
-        indent.scoped(' else {', '}', () {
-          indent.writeln('$varChannelName.setMessageHandler(nil)');
-        });
-      }
-    });
-  });
-}
-
 String _getArgumentName(int count, NamedType argument) =>
     argument.name.isEmpty ? 'arg$count' : argument.name;
 
@@ -287,93 +622,6 @@
   return pascal[0].toLowerCase() + pascal.substring(1);
 }
 
-/// Writes the code for a flutter [Api], [api].
-/// Example:
-/// class Foo {
-///   private let binaryMessenger: FlutterBinaryMessenger
-///   init(binaryMessenger: FlutterBinaryMessenger) {...}
-///   func add(x: Int32, y: Int32, completion: @escaping (Int32?) -> Void) {...}
-/// }
-void _writeFlutterApi(Indent indent, Api api, Root root) {
-  assert(api.location == ApiLocation.flutter);
-  const List<String> generatedComments = <String>[
-    ' Generated class from Pigeon that represents Flutter messages that can be called from Swift.'
-  ];
-  addDocumentationComments(indent, api.documentationComments, _docCommentSpec,
-      generatorComments: generatedComments);
-
-  indent.write('class ${api.name} ');
-  indent.scoped('{', '}', () {
-    indent.writeln('private let binaryMessenger: FlutterBinaryMessenger');
-    indent.write('init(binaryMessenger: FlutterBinaryMessenger)');
-    indent.scoped('{', '}', () {
-      indent.writeln('self.binaryMessenger = binaryMessenger');
-    });
-    final String codecName = _getCodecName(api);
-    String codecArgumentString = '';
-    if (getCodecClasses(api, root).isNotEmpty) {
-      codecArgumentString = ', codec: codec';
-      indent.write('var codec: FlutterStandardMessageCodec ');
-      indent.scoped('{', '}', () {
-        indent.writeln('return $codecName.shared');
-      });
-    }
-    for (final Method func in api.methods) {
-      final String channelName = makeChannelName(api, func);
-      final String returnType = func.returnType.isVoid
-          ? ''
-          : _nullsafeSwiftTypeForDartType(func.returnType);
-      String sendArgument;
-      addDocumentationComments(
-          indent, func.documentationComments, _docCommentSpec);
-
-      if (func.arguments.isEmpty) {
-        indent.write(
-            'func ${func.name}(completion: @escaping ($returnType) -> Void) ');
-        sendArgument = 'nil';
-      } else {
-        final Iterable<String> argTypes = func.arguments
-            .map((NamedType e) => _nullsafeSwiftTypeForDartType(e.type));
-        final Iterable<String> argLabels =
-            indexMap(func.arguments, _getArgumentName);
-        final Iterable<String> argNames =
-            indexMap(func.arguments, _getSafeArgumentName);
-        sendArgument = '[${argNames.join(', ')}]';
-        final String argsSignature = map3(
-            argTypes,
-            argLabels,
-            argNames,
-            (String type, String label, String name) =>
-                '$label $name: $type').join(', ');
-        if (func.returnType.isVoid) {
-          indent.write(
-              'func ${func.name}($argsSignature, completion: @escaping () -> Void) ');
-        } else {
-          indent.write(
-              'func ${func.name}($argsSignature, completion: @escaping ($returnType) -> Void) ');
-        }
-      }
-      indent.scoped('{', '}', () {
-        const String channel = 'channel';
-        indent.writeln(
-            'let $channel = FlutterBasicMessageChannel(name: "$channelName", binaryMessenger: binaryMessenger$codecArgumentString)');
-        indent.write('$channel.sendMessage($sendArgument) ');
-        if (func.returnType.isVoid) {
-          indent.scoped('{ _ in', '}', () {
-            indent.writeln('completion()');
-          });
-        } else {
-          indent.scoped('{ response in', '}', () {
-            indent.writeln(
-                'let result = ${_castForceUnwrap("response", func.returnType, root)}');
-            indent.writeln('completion(result)');
-          });
-        }
-      });
-    }
-  });
-}
-
 String _castForceUnwrap(String value, TypeDeclaration type, Root root) {
   if (isEnum(root, type)) {
     final String forceUnwrap = type.isNullable ? '' : '!';
@@ -446,228 +694,3 @@
   final String nullSafe = type.isNullable ? '?' : '';
   return '${_swiftTypeForDartType(type)}$nullSafe';
 }
-
-/// Generates the ".swift" file for the AST represented by [root] to [sink] with the
-/// provided [options].
-void generateSwift(SwiftOptions options, Root root, StringSink sink) {
-  final Set<String> rootClassNameSet =
-      root.classes.map((Class x) => x.name).toSet();
-  final Set<String> rootEnumNameSet =
-      root.enums.map((Enum x) => x.name).toSet();
-  final Indent indent = Indent(sink);
-
-  HostDatatype getHostDatatype(NamedType field) {
-    return getFieldHostDatatype(field, root.classes, root.enums,
-        (TypeDeclaration x) => _swiftTypeForBuiltinDartType(x));
-  }
-
-  void writeHeader() {
-    if (options.copyrightHeader != null) {
-      addLines(indent, options.copyrightHeader!, linePrefix: '// ');
-    }
-    indent.writeln('// $generatedCodeWarning');
-    indent.writeln('// $seeAlsoWarning');
-  }
-
-  void writeImports() {
-    indent.writeln('import Foundation');
-    indent.format('''
-#if os(iOS)
-import Flutter
-#elseif os(macOS)
-import FlutterMacOS
-#else
-#error("Unsupported platform.")
-#endif
-''');
-  }
-
-  void writeEnum(Enum anEnum) {
-    addDocumentationComments(
-        indent, anEnum.documentationComments, _docCommentSpec);
-
-    indent.write('enum ${anEnum.name}: Int ');
-    indent.scoped('{', '}', () {
-      enumerate(anEnum.members, (int index, final EnumMember member) {
-        addDocumentationComments(
-            indent, member.documentationComments, _docCommentSpec);
-        indent.writeln('case ${_camelCase(member.name)} = $index');
-      });
-    });
-  }
-
-  void writeDataClass(Class klass) {
-    void writeField(NamedType field) {
-      addDocumentationComments(
-          indent, field.documentationComments, _docCommentSpec);
-
-      indent.write(
-          'var ${field.name}: ${_nullsafeSwiftTypeForDartType(field.type)}');
-      final String defaultNil = field.type.isNullable ? ' = nil' : '';
-      indent.addln(defaultNil);
-    }
-
-    void writeToList() {
-      indent.write('func toList() -> [Any?] ');
-      indent.scoped('{', '}', () {
-        indent.write('return ');
-        indent.scoped('[', ']', () {
-          for (final NamedType field in getFieldsInSerializationOrder(klass)) {
-            final HostDatatype hostDatatype = getHostDatatype(field);
-            String toWriteValue = '';
-            final String fieldName = field.name;
-            final String nullsafe = field.type.isNullable ? '?' : '';
-            if (!hostDatatype.isBuiltin &&
-                rootClassNameSet.contains(field.type.baseName)) {
-              toWriteValue = '$fieldName$nullsafe.toList()';
-            } else if (!hostDatatype.isBuiltin &&
-                rootEnumNameSet.contains(field.type.baseName)) {
-              toWriteValue = '$fieldName$nullsafe.rawValue';
-            } else {
-              toWriteValue = field.name;
-            }
-
-            indent.writeln('$toWriteValue,');
-          }
-        });
-      });
-    }
-
-    void writeFromList() {
-      final String className = klass.name;
-      indent.write('static func fromList(_ list: [Any?]) -> $className? ');
-
-      indent.scoped('{', '}', () {
-        enumerate(getFieldsInSerializationOrder(klass),
-            (int index, final NamedType field) {
-          final HostDatatype hostDatatype = getHostDatatype(field);
-
-          final String listValue = 'list[$index]';
-          final String fieldType = _swiftTypeForDartType(field.type);
-
-          if (field.type.isNullable) {
-            if (!hostDatatype.isBuiltin &&
-                rootClassNameSet.contains(field.type.baseName)) {
-              indent.writeln('var ${field.name}: $fieldType? = nil');
-              indent.write('if let ${field.name}List = $listValue as? [Any?] ');
-              indent.scoped('{', '}', () {
-                indent.writeln(
-                    '${field.name} = $fieldType.fromList(${field.name}List)');
-              });
-            } else if (!hostDatatype.isBuiltin &&
-                rootEnumNameSet.contains(field.type.baseName)) {
-              indent.writeln('var ${field.name}: $fieldType? = nil');
-              indent
-                  .write('if let ${field.name}RawValue = $listValue as? Int ');
-              indent.scoped('{', '}', () {
-                indent.writeln(
-                    '${field.name} = $fieldType(rawValue: ${field.name}RawValue)');
-              });
-            } else {
-              indent.writeln('let ${field.name} = $listValue as? $fieldType ');
-            }
-          } else {
-            if (!hostDatatype.isBuiltin &&
-                rootClassNameSet.contains(field.type.baseName)) {
-              indent.writeln(
-                  'let ${field.name} = $fieldType.fromList($listValue as! [Any?])!');
-            } else if (!hostDatatype.isBuiltin &&
-                rootEnumNameSet.contains(field.type.baseName)) {
-              indent.writeln(
-                  'let ${field.name} = $fieldType(rawValue: $listValue as! Int)!');
-            } else {
-              indent.writeln('let ${field.name} = $listValue as! $fieldType');
-            }
-          }
-        });
-
-        indent.writeln('');
-        indent.write('return ');
-        indent.scoped('$className(', ')', () {
-          for (final NamedType field in getFieldsInSerializationOrder(klass)) {
-            final String comma =
-                getFieldsInSerializationOrder(klass).last == field ? '' : ',';
-            indent.writeln('${field.name}: ${field.name}$comma');
-          }
-        });
-      });
-    }
-
-    const List<String> generatedComments = <String>[
-      ' Generated class from Pigeon that represents data sent in messages.'
-    ];
-    addDocumentationComments(
-        indent, klass.documentationComments, _docCommentSpec,
-        generatorComments: generatedComments);
-
-    indent.write('struct ${klass.name} ');
-    indent.scoped('{', '}', () {
-      getFieldsInSerializationOrder(klass).forEach(writeField);
-
-      indent.writeln('');
-      writeFromList();
-      writeToList();
-    });
-  }
-
-  void writeApi(Api api, Root root) {
-    if (api.location == ApiLocation.host) {
-      _writeHostApi(indent, api, root);
-    } else if (api.location == ApiLocation.flutter) {
-      _writeFlutterApi(indent, api, root);
-    }
-  }
-
-  void writeWrapResult() {
-    indent.write('private func wrapResult(_ result: Any?) -> [Any?] ');
-    indent.scoped('{', '}', () {
-      indent.writeln('return [result]');
-    });
-  }
-
-  void writeWrapError() {
-    indent.write('private func wrapError(_ error: FlutterError) -> [Any?] ');
-    indent.scoped('{', '}', () {
-      indent.write('return ');
-      indent.scoped('[', ']', () {
-        indent.writeln('error.code,');
-        indent.writeln('error.message,');
-        indent.writeln('error.details');
-      });
-    });
-  }
-
-  writeHeader();
-  indent.addln('');
-  writeImports();
-  indent.addln('');
-  indent.writeln('$_docCommentPrefix Generated class from Pigeon.');
-  for (final Enum anEnum in root.enums) {
-    indent.writeln('');
-    writeEnum(anEnum);
-  }
-
-  for (final Class klass in root.classes) {
-    indent.addln('');
-    writeDataClass(klass);
-  }
-
-  if (root.apis.any((Api api) =>
-      api.location == ApiLocation.host &&
-      api.methods.any((Method it) => it.isAsynchronous))) {
-    indent.addln('');
-  }
-
-  for (final Api api in root.apis) {
-    if (getCodecClasses(api, root).isNotEmpty) {
-      _writeCodec(indent, api, root);
-      indent.addln('');
-    }
-    writeApi(api, root);
-  }
-
-  indent.addln('');
-  writeWrapResult();
-  indent.addln('');
-  writeWrapError();
-}
diff --git a/packages/pigeon/mock_handler_tester/test/message.dart b/packages/pigeon/mock_handler_tester/test/message.dart
index 4f6c9d1..bf02b40 100644
--- a/packages/pigeon/mock_handler_tester/test/message.dart
+++ b/packages/pigeon/mock_handler_tester/test/message.dart
@@ -2,9 +2,10 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 //
-// Autogenerated from Pigeon (v5.0.0), do not edit directly.
+// Autogenerated from Pigeon (v6.0.0), do not edit directly.
 // See also: https://pub.dev/packages/pigeon
 // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import
+
 import 'dart:async';
 import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;
 
diff --git a/packages/pigeon/mock_handler_tester/test/test.dart b/packages/pigeon/mock_handler_tester/test/test.dart
index 9b97826..5cc8473 100644
--- a/packages/pigeon/mock_handler_tester/test/test.dart
+++ b/packages/pigeon/mock_handler_tester/test/test.dart
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 //
-// Autogenerated from Pigeon (v5.0.0), do not edit directly.
+// Autogenerated from Pigeon (v6.0.0), do not edit directly.
 // See also: https://pub.dev/packages/pigeon
 // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, unnecessary_import
 // ignore_for_file: avoid_relative_lib_imports
diff --git a/packages/pigeon/platform_tests/alternate_language_test_plugin/android/src/main/java/com/example/alternate_language_test_plugin/CoreTests.java b/packages/pigeon/platform_tests/alternate_language_test_plugin/android/src/main/java/com/example/alternate_language_test_plugin/CoreTests.java
index ea8e343..d97d0cc 100644
--- a/packages/pigeon/platform_tests/alternate_language_test_plugin/android/src/main/java/com/example/alternate_language_test_plugin/CoreTests.java
+++ b/packages/pigeon/platform_tests/alternate_language_test_plugin/android/src/main/java/com/example/alternate_language_test_plugin/CoreTests.java
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 //
-// Autogenerated from Pigeon (v5.0.1), do not edit directly.
+// Autogenerated from Pigeon (v6.0.0), do not edit directly.
 // See also: https://pub.dev/packages/pigeon
 
 package com.example.alternate_language_test_plugin;
@@ -24,6 +24,15 @@
 /** Generated class from Pigeon. */
 @SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression"})
 public class CoreTests {
+  @NonNull
+  private static ArrayList<Object> wrapError(@NonNull Throwable exception) {
+    ArrayList<Object> errorList = new ArrayList<>(3);
+    errorList.add(exception.toString());
+    errorList.add(exception.getClass().getSimpleName());
+    errorList.add(
+        "Cause: " + exception.getCause() + ", Stacktrace: " + Log.getStackTraceString(exception));
+    return errorList;
+  }
 
   public enum AnEnum {
     ONE(0),
@@ -1647,14 +1656,4 @@
       }
     }
   }
-
-  @NonNull
-  private static ArrayList<Object> wrapError(@NonNull Throwable exception) {
-    ArrayList<Object> errorList = new ArrayList<>(3);
-    errorList.add(exception.toString());
-    errorList.add(exception.getClass().getSimpleName());
-    errorList.add(
-        "Cause: " + exception.getCause() + ", Stacktrace: " + Log.getStackTraceString(exception));
-    return errorList;
-  }
 }
diff --git a/packages/pigeon/platform_tests/alternate_language_test_plugin/ios/Classes/CoreTests.gen.h b/packages/pigeon/platform_tests/alternate_language_test_plugin/ios/Classes/CoreTests.gen.h
index 2db8f71..bfab566 100644
--- a/packages/pigeon/platform_tests/alternate_language_test_plugin/ios/Classes/CoreTests.gen.h
+++ b/packages/pigeon/platform_tests/alternate_language_test_plugin/ios/Classes/CoreTests.gen.h
@@ -2,9 +2,11 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 //
-// Autogenerated from Pigeon (v5.0.1), do not edit directly.
+// Autogenerated from Pigeon (v6.0.0), do not edit directly.
 // See also: https://pub.dev/packages/pigeon
+
 #import <Foundation/Foundation.h>
+
 @protocol FlutterBinaryMessenger;
 @protocol FlutterMessageCodec;
 @class FlutterError;
@@ -207,6 +209,7 @@
 - (void)echoString:(NSString *)aString
         completion:(void (^)(NSString *_Nullable, NSError *_Nullable))completion;
 @end
+
 /// The codec used by HostTrivialApi.
 NSObject<FlutterMessageCodec> *HostTrivialApiGetCodec(void);
 
diff --git a/packages/pigeon/platform_tests/alternate_language_test_plugin/ios/Classes/CoreTests.gen.m b/packages/pigeon/platform_tests/alternate_language_test_plugin/ios/Classes/CoreTests.gen.m
index 078bf32..7f73cf6 100644
--- a/packages/pigeon/platform_tests/alternate_language_test_plugin/ios/Classes/CoreTests.gen.m
+++ b/packages/pigeon/platform_tests/alternate_language_test_plugin/ios/Classes/CoreTests.gen.m
@@ -2,8 +2,9 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 //
-// Autogenerated from Pigeon (v5.0.1), do not edit directly.
+// Autogenerated from Pigeon (v6.0.0), do not edit directly.
 // See also: https://pub.dev/packages/pigeon
+
 #import "CoreTests.gen.h"
 #import <Flutter/Flutter.h>
 
@@ -33,11 +34,13 @@
 + (nullable AllTypes *)nullableFromList:(NSArray *)list;
 - (NSArray *)toList;
 @end
+
 @interface AllNullableTypes ()
 + (AllNullableTypes *)fromList:(NSArray *)list;
 + (nullable AllNullableTypes *)nullableFromList:(NSArray *)list;
 - (NSArray *)toList;
 @end
+
 @interface AllNullableTypesWrapper ()
 + (AllNullableTypesWrapper *)fromList:(NSArray *)list;
 + (nullable AllNullableTypesWrapper *)nullableFromList:(NSArray *)list;
@@ -894,6 +897,7 @@
                  }];
 }
 @end
+
 NSObject<FlutterMessageCodec> *HostTrivialApiGetCodec() {
   static FlutterStandardMessageCodec *sSharedObject = nil;
   sSharedObject = [FlutterStandardMessageCodec sharedInstance];
diff --git a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/core_tests.gen.dart b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/core_tests.gen.dart
index fe0fe33..1040f61 100644
--- a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/core_tests.gen.dart
+++ b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/core_tests.gen.dart
@@ -2,9 +2,10 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 //
-// Autogenerated from Pigeon (v5.0.0), do not edit directly.
+// Autogenerated from Pigeon (v6.0.0), do not edit directly.
 // See also: https://pub.dev/packages/pigeon
 // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import
+
 import 'dart:async';
 import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;
 
diff --git a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/multiple_arity.gen.dart b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/multiple_arity.gen.dart
index e92ab29..c3b44b0 100644
--- a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/multiple_arity.gen.dart
+++ b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/multiple_arity.gen.dart
@@ -2,9 +2,10 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 //
-// Autogenerated from Pigeon (v5.0.0), do not edit directly.
+// Autogenerated from Pigeon (v6.0.0), do not edit directly.
 // See also: https://pub.dev/packages/pigeon
 // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import
+
 import 'dart:async';
 import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;
 
diff --git a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/non_null_fields.gen.dart b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/non_null_fields.gen.dart
index 1ff71bf..3bf1fa5 100644
--- a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/non_null_fields.gen.dart
+++ b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/non_null_fields.gen.dart
@@ -2,9 +2,10 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 //
-// Autogenerated from Pigeon (v5.0.0), do not edit directly.
+// Autogenerated from Pigeon (v6.0.0), do not edit directly.
 // See also: https://pub.dev/packages/pigeon
 // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import
+
 import 'dart:async';
 import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;
 
diff --git a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/null_fields.gen.dart b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/null_fields.gen.dart
index 88f4611..82bfce2 100644
--- a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/null_fields.gen.dart
+++ b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/null_fields.gen.dart
@@ -2,9 +2,10 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 //
-// Autogenerated from Pigeon (v5.0.0), do not edit directly.
+// Autogenerated from Pigeon (v6.0.0), do not edit directly.
 // See also: https://pub.dev/packages/pigeon
 // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import
+
 import 'dart:async';
 import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;
 
diff --git a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/null_safe_pigeon.dart b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/null_safe_pigeon.dart
index 13fe926..a4a52e6 100644
--- a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/null_safe_pigeon.dart
+++ b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/null_safe_pigeon.dart
@@ -2,9 +2,10 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 //
-// Autogenerated from Pigeon (v5.0.0), do not edit directly.
+// Autogenerated from Pigeon (v6.0.0), do not edit directly.
 // See also: https://pub.dev/packages/pigeon
 // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import
+
 import 'dart:async';
 import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;
 
diff --git a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/nullable_returns.gen.dart b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/nullable_returns.gen.dart
index be1979c..51af74c 100644
--- a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/nullable_returns.gen.dart
+++ b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/nullable_returns.gen.dart
@@ -2,9 +2,10 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 //
-// Autogenerated from Pigeon (v5.0.0), do not edit directly.
+// Autogenerated from Pigeon (v6.0.0), do not edit directly.
 // See also: https://pub.dev/packages/pigeon
 // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import
+
 import 'dart:async';
 import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;
 
diff --git a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/primitive.dart b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/primitive.dart
index 36a569e..f9855f6 100644
--- a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/primitive.dart
+++ b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/primitive.dart
@@ -2,9 +2,10 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 //
-// Autogenerated from Pigeon (v5.0.0), do not edit directly.
+// Autogenerated from Pigeon (v6.0.0), do not edit directly.
 // See also: https://pub.dev/packages/pigeon
 // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import
+
 import 'dart:async';
 import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;
 
diff --git a/packages/pigeon/platform_tests/shared_test_plugin_code/lib/src/generated/core_tests.gen.dart b/packages/pigeon/platform_tests/shared_test_plugin_code/lib/src/generated/core_tests.gen.dart
index 3560cc2..1040f61 100644
--- a/packages/pigeon/platform_tests/shared_test_plugin_code/lib/src/generated/core_tests.gen.dart
+++ b/packages/pigeon/platform_tests/shared_test_plugin_code/lib/src/generated/core_tests.gen.dart
@@ -2,9 +2,10 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 //
-// Autogenerated from Pigeon (v5.0.1), do not edit directly.
+// Autogenerated from Pigeon (v6.0.0), do not edit directly.
 // See also: https://pub.dev/packages/pigeon
 // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import
+
 import 'dart:async';
 import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;
 
diff --git a/packages/pigeon/platform_tests/test_plugin/android/src/main/kotlin/com/example/test_plugin/CoreTests.gen.kt b/packages/pigeon/platform_tests/test_plugin/android/src/main/kotlin/com/example/test_plugin/CoreTests.gen.kt
index 17d7e9f..c2c0390 100644
--- a/packages/pigeon/platform_tests/test_plugin/android/src/main/kotlin/com/example/test_plugin/CoreTests.gen.kt
+++ b/packages/pigeon/platform_tests/test_plugin/android/src/main/kotlin/com/example/test_plugin/CoreTests.gen.kt
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 // 
-// Autogenerated from Pigeon (v5.0.1), do not edit directly.
+// Autogenerated from Pigeon (v6.0.0), do not edit directly.
 // See also: https://pub.dev/packages/pigeon
 
 package com.example.test_plugin
@@ -15,7 +15,17 @@
 import java.io.ByteArrayOutputStream
 import java.nio.ByteBuffer
 
-/** Generated class from Pigeon. */
+private fun wrapResult(result: Any?): List<Any?> {
+  return listOf(result)
+}
+
+private fun wrapError(exception: Throwable): List<Any> {
+  return listOf<Any>(
+    exception.javaClass.simpleName,
+    exception.toString(),
+    "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception)
+  )
+}
 
 enum class AnEnum(val raw: Int) {
   ONE(0),
@@ -810,15 +820,3 @@
     }
   }
 }
-
-private fun wrapResult(result: Any?): List<Any?> {
-  return listOf(result)
-}
-
-private fun wrapError(exception: Throwable): List<Any> {
-  return listOf<Any>(
-    exception.javaClass.simpleName,
-    exception.toString(),
-    "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception)
-  )
-}
diff --git a/packages/pigeon/platform_tests/test_plugin/ios/Classes/CoreTests.gen.swift b/packages/pigeon/platform_tests/test_plugin/ios/Classes/CoreTests.gen.swift
index bcf93bc..407ee69 100644
--- a/packages/pigeon/platform_tests/test_plugin/ios/Classes/CoreTests.gen.swift
+++ b/packages/pigeon/platform_tests/test_plugin/ios/Classes/CoreTests.gen.swift
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 // 
-// Autogenerated from Pigeon (v5.0.1), do not edit directly.
+// Autogenerated from Pigeon (v6.0.0), do not edit directly.
 // See also: https://pub.dev/packages/pigeon
 
 import Foundation
@@ -15,7 +15,18 @@
 #endif
 
 
-/// Generated class from Pigeon.
+
+private func wrapResult(_ result: Any?) -> [Any?] {
+  return [result]
+}
+
+private func wrapError(_ error: FlutterError) -> [Any?] {
+  return [
+    error.code,
+    error.message,
+    error.details
+  ]
+}
 
 enum AnEnum: Int {
   case one = 0
@@ -666,15 +677,3 @@
     }
   }
 }
-
-private func wrapResult(_ result: Any?) -> [Any?] {
-  return [result]
-}
-
-private func wrapError(_ error: FlutterError) -> [Any?] {
-  return [
-    error.code,
-    error.message,
-    error.details
-  ]
-}
diff --git a/packages/pigeon/platform_tests/test_plugin/macos/Classes/CoreTests.gen.swift b/packages/pigeon/platform_tests/test_plugin/macos/Classes/CoreTests.gen.swift
index bcf93bc..407ee69 100644
--- a/packages/pigeon/platform_tests/test_plugin/macos/Classes/CoreTests.gen.swift
+++ b/packages/pigeon/platform_tests/test_plugin/macos/Classes/CoreTests.gen.swift
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 // 
-// Autogenerated from Pigeon (v5.0.1), do not edit directly.
+// Autogenerated from Pigeon (v6.0.0), do not edit directly.
 // See also: https://pub.dev/packages/pigeon
 
 import Foundation
@@ -15,7 +15,18 @@
 #endif
 
 
-/// Generated class from Pigeon.
+
+private func wrapResult(_ result: Any?) -> [Any?] {
+  return [result]
+}
+
+private func wrapError(_ error: FlutterError) -> [Any?] {
+  return [
+    error.code,
+    error.message,
+    error.details
+  ]
+}
 
 enum AnEnum: Int {
   case one = 0
@@ -666,15 +677,3 @@
     }
   }
 }
-
-private func wrapResult(_ result: Any?) -> [Any?] {
-  return [result]
-}
-
-private func wrapError(_ error: FlutterError) -> [Any?] {
-  return [
-    error.code,
-    error.message,
-    error.details
-  ]
-}
diff --git a/packages/pigeon/platform_tests/test_plugin/windows/pigeon/core_tests.gen.cpp b/packages/pigeon/platform_tests/test_plugin/windows/pigeon/core_tests.gen.cpp
index 98b9395..6c39bce 100644
--- a/packages/pigeon/platform_tests/test_plugin/windows/pigeon/core_tests.gen.cpp
+++ b/packages/pigeon/platform_tests/test_plugin/windows/pigeon/core_tests.gen.cpp
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 //
-// Autogenerated from Pigeon (v5.0.1), do not edit directly.
+// Autogenerated from Pigeon (v6.0.0), do not edit directly.
 // See also: https://pub.dev/packages/pigeon
 
 #undef _HAS_EXCEPTIONS
diff --git a/packages/pigeon/platform_tests/test_plugin/windows/pigeon/core_tests.gen.h b/packages/pigeon/platform_tests/test_plugin/windows/pigeon/core_tests.gen.h
index aac405e..c752a5a 100644
--- a/packages/pigeon/platform_tests/test_plugin/windows/pigeon/core_tests.gen.h
+++ b/packages/pigeon/platform_tests/test_plugin/windows/pigeon/core_tests.gen.h
@@ -2,11 +2,11 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 //
-// Autogenerated from Pigeon (v5.0.1), do not edit directly.
+// Autogenerated from Pigeon (v6.0.0), do not edit directly.
 // See also: https://pub.dev/packages/pigeon
 
-#ifndef PIGEON_CORE_TESTS_PIGEONTEST_H_
-#define PIGEON_CORE_TESTS_PIGEONTEST_H_
+#ifndef PIGEON_CORE_TESTS_GEN_H_H_
+#define PIGEON_CORE_TESTS_GEN_H_H_
 #include <flutter/basic_message_channel.h>
 #include <flutter/binary_messenger.h>
 #include <flutter/encodable_value.h>
@@ -17,12 +17,11 @@
 #include <string>
 
 namespace core_tests_pigeontest {
+
 class CoreTestsTest;
 
 // Generated class from Pigeon.
 
-enum class AnEnum { one = 0, two = 1, three = 2 };
-
 class FlutterError {
  public:
   explicit FlutterError(const std::string& code) : code_(code) {}
@@ -64,6 +63,8 @@
   std::variant<T, FlutterError> v_;
 };
 
+enum class AnEnum { one = 0, two = 1, three = 2 };
+
 // Generated class from Pigeon that represents data sent in messages.
 class AllTypes {
  public:
@@ -412,4 +413,4 @@
   HostTrivialApi() = default;
 };
 }  // namespace core_tests_pigeontest
-#endif  // PIGEON_CORE_TESTS_PIGEONTEST_H_
+#endif  // PIGEON_CORE_TESTS_GEN_H_H_
diff --git a/packages/pigeon/pubspec.yaml b/packages/pigeon/pubspec.yaml
index f2a5a9a..caa82a3 100644
--- a/packages/pigeon/pubspec.yaml
+++ b/packages/pigeon/pubspec.yaml
@@ -2,7 +2,7 @@
 description: Code generator tool to make communication between Flutter and the host platform type-safe and easier.
 repository: https://github.com/flutter/packages/tree/main/packages/pigeon
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3Apigeon
-version: 5.0.1 # This must match the version in lib/generator_tools.dart
+version: 6.0.0 # This must match the version in lib/generator_tools.dart
 
 environment:
   sdk: ">=2.12.0 <3.0.0"
diff --git a/packages/pigeon/test/cpp_generator_test.dart b/packages/pigeon/test/cpp_generator_test.dart
index d8e9917..97dc129 100644
--- a/packages/pigeon/test/cpp_generator_test.dart
+++ b/packages/pigeon/test/cpp_generator_test.dart
@@ -4,6 +4,7 @@
 
 import 'package:pigeon/ast.dart';
 import 'package:pigeon/cpp_generator.dart';
+import 'package:pigeon/generator_tools.dart';
 import 'package:pigeon/pigeon.dart' show Error;
 import 'package:test/test.dart';
 
@@ -45,7 +46,13 @@
     ], enums: <Enum>[]);
     {
       final StringBuffer sink = StringBuffer();
-      generateCppHeader(const CppOptions(), root, sink);
+      const CppGenerator generator = CppGenerator();
+      final OutputFileOptions<CppOptions> generatorOptions =
+          OutputFileOptions<CppOptions>(
+        fileType: FileType.header,
+        languageOptions: const CppOptions(),
+      );
+      generator.generate(generatorOptions, root, sink);
       final String code = sink.toString();
       expect(code, contains('class Input'));
       expect(code, contains('class Output'));
@@ -53,7 +60,13 @@
     }
     {
       final StringBuffer sink = StringBuffer();
-      generateCppSource(const CppOptions(), root, sink);
+      const CppGenerator generator = CppGenerator();
+      final OutputFileOptions<CppOptions> generatorOptions =
+          OutputFileOptions<CppOptions>(
+        fileType: FileType.source,
+        languageOptions: const CppOptions(),
+      );
+      generator.generate(generatorOptions, root, sink);
       final String code = sink.toString();
       expect(code, contains('Input::Input()'));
       expect(code, contains('Output::Output'));
@@ -101,7 +114,13 @@
     ], enums: <Enum>[]);
     {
       final StringBuffer sink = StringBuffer();
-      generateCppHeader(const CppOptions(), root, sink);
+      const CppGenerator generator = CppGenerator();
+      final OutputFileOptions<CppOptions> generatorOptions =
+          OutputFileOptions<CppOptions>(
+        fileType: FileType.header,
+        languageOptions: const CppOptions(),
+      );
+      generator.generate(generatorOptions, root, sink);
       final String code = sink.toString();
       // Method name and argument names should be adjusted.
       expect(code, contains(' DoSomething(const Input& some_input)'));
@@ -116,7 +135,13 @@
     }
     {
       final StringBuffer sink = StringBuffer();
-      generateCppSource(const CppOptions(), root, sink);
+      const CppGenerator generator = CppGenerator();
+      final OutputFileOptions<CppOptions> generatorOptions =
+          OutputFileOptions<CppOptions>(
+        fileType: FileType.source,
+        languageOptions: const CppOptions(),
+      );
+      generator.generate(generatorOptions, root, sink);
       final String code = sink.toString();
       expect(code, contains('pointer_input_field'));
       expect(code, contains('Output::output_field()'));
@@ -144,7 +169,17 @@
     ], classes: <Class>[], enums: <Enum>[]);
     {
       final StringBuffer sink = StringBuffer();
-      generateCppHeader(const CppOptions(), root, sink);
+      const CppGenerator generator = CppGenerator();
+      final OutputFileOptions<CppOptions> generatorOptions =
+          OutputFileOptions<CppOptions>(
+        fileType: FileType.header,
+        languageOptions: const CppOptions(),
+      );
+      generator.generate(
+        generatorOptions,
+        root,
+        sink,
+      );
       final String code = sink.toString();
 
       expect(
@@ -184,7 +219,13 @@
     ], classes: <Class>[], enums: <Enum>[]);
     {
       final StringBuffer sink = StringBuffer();
-      generateCppHeader(const CppOptions(), root, sink);
+      const CppGenerator generator = CppGenerator();
+      final OutputFileOptions<CppOptions> generatorOptions =
+          OutputFileOptions<CppOptions>(
+        fileType: FileType.header,
+        languageOptions: const CppOptions(),
+      );
+      generator.generate(generatorOptions, root, sink);
       final String code = sink.toString();
 
       expect(
@@ -238,14 +279,26 @@
     ], enums: <Enum>[]);
     {
       final StringBuffer sink = StringBuffer();
-      generateCppHeader(const CppOptions(), root, sink);
+      const CppGenerator generator = CppGenerator();
+      final OutputFileOptions<CppOptions> generatorOptions =
+          OutputFileOptions<CppOptions>(
+        fileType: FileType.header,
+        languageOptions: const CppOptions(),
+      );
+      generator.generate(generatorOptions, root, sink);
       final String code = sink.toString();
       expect(code, isNot(contains('){')));
       expect(code, isNot(contains('const{')));
     }
     {
       final StringBuffer sink = StringBuffer();
-      generateCppSource(const CppOptions(), root, sink);
+      const CppGenerator generator = CppGenerator();
+      final OutputFileOptions<CppOptions> generatorOptions =
+          OutputFileOptions<CppOptions>(
+        fileType: FileType.source,
+        languageOptions: const CppOptions(),
+      );
+      generator.generate(generatorOptions, root, sink);
       final String code = sink.toString();
       expect(code, isNot(contains('){')));
       expect(code, isNot(contains('const{')));
@@ -271,7 +324,13 @@
     ], classes: <Class>[], enums: <Enum>[]);
     {
       final StringBuffer sink = StringBuffer();
-      generateCppHeader(const CppOptions(), root, sink);
+      const CppGenerator generator = CppGenerator();
+      final OutputFileOptions<CppOptions> generatorOptions =
+          OutputFileOptions<CppOptions>(
+        fileType: FileType.header,
+        languageOptions: const CppOptions(),
+      );
+      generator.generate(generatorOptions, root, sink);
       final String code = sink.toString();
       expect(code, contains('''
 #include <flutter/basic_message_channel.h>
@@ -286,8 +345,13 @@
     }
     {
       final StringBuffer sink = StringBuffer();
-      generateCppSource(
-          const CppOptions(headerIncludePath: 'a_header.h'), root, sink);
+      const CppGenerator generator = CppGenerator();
+      final OutputFileOptions<CppOptions> generatorOptions =
+          OutputFileOptions<CppOptions>(
+        fileType: FileType.source,
+        languageOptions: const CppOptions(headerIncludePath: 'a_header.h'),
+      );
+      generator.generate(generatorOptions, root, sink);
       final String code = sink.toString();
       expect(code, contains('''
 #include "a_header.h"
@@ -323,14 +387,26 @@
     ], classes: <Class>[], enums: <Enum>[]);
     {
       final StringBuffer sink = StringBuffer();
-      generateCppHeader(const CppOptions(namespace: 'foo'), root, sink);
+      const CppGenerator generator = CppGenerator();
+      final OutputFileOptions<CppOptions> generatorOptions =
+          OutputFileOptions<CppOptions>(
+        fileType: FileType.header,
+        languageOptions: const CppOptions(namespace: 'foo'),
+      );
+      generator.generate(generatorOptions, root, sink);
       final String code = sink.toString();
       expect(code, contains('namespace foo {'));
       expect(code, contains('}  // namespace foo'));
     }
     {
       final StringBuffer sink = StringBuffer();
-      generateCppSource(const CppOptions(namespace: 'foo'), root, sink);
+      const CppGenerator generator = CppGenerator();
+      final OutputFileOptions<CppOptions> generatorOptions =
+          OutputFileOptions<CppOptions>(
+        fileType: FileType.source,
+        languageOptions: const CppOptions(namespace: 'foo'),
+      );
+      generator.generate(generatorOptions, root, sink);
       final String code = sink.toString();
       expect(code, contains('namespace foo {'));
       expect(code, contains('}  // namespace foo'));
@@ -391,7 +467,13 @@
     ], enums: <Enum>[]);
     {
       final StringBuffer sink = StringBuffer();
-      generateCppHeader(const CppOptions(), root, sink);
+      const CppGenerator generator = CppGenerator();
+      final OutputFileOptions<CppOptions> generatorOptions =
+          OutputFileOptions<CppOptions>(
+        fileType: FileType.header,
+        languageOptions: const CppOptions(),
+      );
+      generator.generate(generatorOptions, root, sink);
       final String code = sink.toString();
       // Getters should return const pointers.
       expect(code, contains('const bool* nullable_bool()'));
@@ -423,7 +505,13 @@
     }
     {
       final StringBuffer sink = StringBuffer();
-      generateCppSource(const CppOptions(), root, sink);
+      const CppGenerator generator = CppGenerator();
+      final OutputFileOptions<CppOptions> generatorOptions =
+          OutputFileOptions<CppOptions>(
+        fileType: FileType.source,
+        languageOptions: const CppOptions(),
+      );
+      generator.generate(generatorOptions, root, sink);
       final String code = sink.toString();
       // Getters extract optionals.
       expect(code,
@@ -522,7 +610,13 @@
     ], enums: <Enum>[]);
     {
       final StringBuffer sink = StringBuffer();
-      generateCppHeader(const CppOptions(), root, sink);
+      const CppGenerator generator = CppGenerator();
+      final OutputFileOptions<CppOptions> generatorOptions =
+          OutputFileOptions<CppOptions>(
+        fileType: FileType.header,
+        languageOptions: const CppOptions(),
+      );
+      generator.generate(generatorOptions, root, sink);
       final String code = sink.toString();
       // POD getters should return copies references.
       expect(code, contains('bool non_nullable_bool()'));
@@ -547,7 +641,13 @@
     }
     {
       final StringBuffer sink = StringBuffer();
-      generateCppSource(const CppOptions(), root, sink);
+      const CppGenerator generator = CppGenerator();
+      final OutputFileOptions<CppOptions> generatorOptions =
+          OutputFileOptions<CppOptions>(
+        fileType: FileType.source,
+        languageOptions: const CppOptions(),
+      );
+      generator.generate(generatorOptions, root, sink);
       final String code = sink.toString();
       // Getters just return the value.
       expect(code, contains('return non_nullable_bool_;'));
@@ -645,7 +745,13 @@
     ], enums: <Enum>[]);
     {
       final StringBuffer sink = StringBuffer();
-      generateCppHeader(const CppOptions(), root, sink);
+      const CppGenerator generator = CppGenerator();
+      final OutputFileOptions<CppOptions> generatorOptions =
+          OutputFileOptions<CppOptions>(
+        fileType: FileType.header,
+        languageOptions: const CppOptions(),
+      );
+      generator.generate(generatorOptions, root, sink);
       final String code = sink.toString();
       expect(
           code, contains('ErrorOr<std::optional<bool>> ReturnNullableBool()'));
@@ -750,7 +856,13 @@
     ], enums: <Enum>[]);
     {
       final StringBuffer sink = StringBuffer();
-      generateCppHeader(const CppOptions(), root, sink);
+      const CppGenerator generator = CppGenerator();
+      final OutputFileOptions<CppOptions> generatorOptions =
+          OutputFileOptions<CppOptions>(
+        fileType: FileType.header,
+        languageOptions: const CppOptions(),
+      );
+      generator.generate(generatorOptions, root, sink);
       final String code = sink.toString();
       expect(code, contains('ErrorOr<bool> ReturnBool()'));
       expect(code, contains('ErrorOr<int64_t> ReturnInt()'));
@@ -832,7 +944,13 @@
     ], enums: <Enum>[]);
     {
       final StringBuffer sink = StringBuffer();
-      generateCppHeader(const CppOptions(), root, sink);
+      const CppGenerator generator = CppGenerator();
+      final OutputFileOptions<CppOptions> generatorOptions =
+          OutputFileOptions<CppOptions>(
+        fileType: FileType.header,
+        languageOptions: const CppOptions(),
+      );
+      generator.generate(generatorOptions, root, sink);
       final String code = sink.toString();
       expect(
           code,
@@ -846,7 +964,13 @@
     }
     {
       final StringBuffer sink = StringBuffer();
-      generateCppSource(const CppOptions(), root, sink);
+      const CppGenerator generator = CppGenerator();
+      final OutputFileOptions<CppOptions> generatorOptions =
+          OutputFileOptions<CppOptions>(
+        fileType: FileType.source,
+        languageOptions: const CppOptions(),
+      );
+      generator.generate(generatorOptions, root, sink);
       final String code = sink.toString();
       // Most types should just use get_if, since the parameter is a pointer,
       // and get_if will automatically handle null values (since a null
@@ -963,7 +1087,13 @@
     ], enums: <Enum>[]);
     {
       final StringBuffer sink = StringBuffer();
-      generateCppHeader(const CppOptions(), root, sink);
+      const CppGenerator generator = CppGenerator();
+      final OutputFileOptions<CppOptions> generatorOptions =
+          OutputFileOptions<CppOptions>(
+        fileType: FileType.header,
+        languageOptions: const CppOptions(),
+      );
+      generator.generate(generatorOptions, root, sink);
       final String code = sink.toString();
       expect(
           code,
@@ -977,7 +1107,13 @@
     }
     {
       final StringBuffer sink = StringBuffer();
-      generateCppSource(const CppOptions(), root, sink);
+      const CppGenerator generator = CppGenerator();
+      final OutputFileOptions<CppOptions> generatorOptions =
+          OutputFileOptions<CppOptions>(
+        fileType: FileType.source,
+        languageOptions: const CppOptions(),
+      );
+      generator.generate(generatorOptions, root, sink);
       final String code = sink.toString();
       // Most types should extract references. Since the type is non-nullable,
       // there's only one possible type.
@@ -1037,7 +1173,13 @@
     ], classes: <Class>[], enums: <Enum>[]);
 
     final StringBuffer sink = StringBuffer();
-    generateCppSource(const CppOptions(), root, sink);
+    const CppGenerator generator = CppGenerator();
+    final OutputFileOptions<CppOptions> generatorOptions =
+        OutputFileOptions<CppOptions>(
+      fileType: FileType.source,
+      languageOptions: const CppOptions(),
+    );
+    generator.generate(generatorOptions, root, sink);
     final String code = sink.toString();
     // A bare 'auto' here would create a copy, not a reference, which is
     // ineffecient.
@@ -1149,7 +1291,13 @@
       ],
     );
     final StringBuffer sink = StringBuffer();
-    generateCppHeader(const CppOptions(headerIncludePath: 'foo'), root, sink);
+    const CppGenerator generator = CppGenerator();
+    final OutputFileOptions<CppOptions> generatorOptions =
+        OutputFileOptions<CppOptions>(
+      fileType: FileType.header,
+      languageOptions: const CppOptions(headerIncludePath: 'foo'),
+    );
+    generator.generate(generatorOptions, root, sink);
     final String code = sink.toString();
     for (final String comment in comments) {
       expect(code, contains('//$comment'));
@@ -1184,7 +1332,13 @@
       enums: <Enum>[],
     );
     final StringBuffer sink = StringBuffer();
-    generateCppHeader(const CppOptions(), root, sink);
+    const CppGenerator generator = CppGenerator();
+    final OutputFileOptions<CppOptions> generatorOptions =
+        OutputFileOptions<CppOptions>(
+      fileType: FileType.header,
+      languageOptions: const CppOptions(),
+    );
+    generator.generate(generatorOptions, root, sink);
     final String code = sink.toString();
     expect(code, isNot(contains(' : public flutter::StandardCodecSerializer')));
   });
@@ -1226,7 +1380,13 @@
       ])
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
-    generateCppHeader(const CppOptions(), root, sink);
+    const CppGenerator generator = CppGenerator();
+    final OutputFileOptions<CppOptions> generatorOptions =
+        OutputFileOptions<CppOptions>(
+      fileType: FileType.header,
+      languageOptions: const CppOptions(),
+    );
+    generator.generate(generatorOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains(' : public flutter::StandardCodecSerializer'));
   });
@@ -1295,7 +1455,13 @@
       ]),
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
-    generateCppSource(const CppOptions(), root, sink);
+    const CppGenerator generator = CppGenerator();
+    final OutputFileOptions<CppOptions> generatorOptions =
+        OutputFileOptions<CppOptions>(
+      fileType: FileType.source,
+      languageOptions: const CppOptions(),
+    );
+    generator.generate(generatorOptions, root, sink);
     final String code = sink.toString();
     expect(code, isNot(contains('reply(wrap')));
     expect(code, contains('reply(flutter::EncodableValue('));
@@ -1349,7 +1515,13 @@
       ])
     ], classes: <Class>[], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
-    generateCppSource(const CppOptions(), root, sink);
+    const CppGenerator generator = CppGenerator();
+    final OutputFileOptions<CppOptions> generatorOptions =
+        OutputFileOptions<CppOptions>(
+      fileType: FileType.source,
+      languageOptions: const CppOptions(),
+    );
+    generator.generate(generatorOptions, root, sink);
     final String code = sink.toString();
     // Nothing should be captured by reference for async handlers, since their
     // lifetime is unknown (and expected to be longer than the stack's).
diff --git a/packages/pigeon/test/dart_generator_test.dart b/packages/pigeon/test/dart_generator_test.dart
index c77e5d8..d99dde7 100644
--- a/packages/pigeon/test/dart_generator_test.dart
+++ b/packages/pigeon/test/dart_generator_test.dart
@@ -29,7 +29,8 @@
       enums: <Enum>[],
     );
     final StringBuffer sink = StringBuffer();
-    generateDart(DartOptions(), root, sink);
+    const DartGenerator generator = DartGenerator();
+    generator.generate(DartOptions(), root, sink);
     final String code = sink.toString();
     expect(code, contains('class Foobar'));
     expect(code, contains('  dataType1? field1;'));
@@ -49,7 +50,8 @@
       enums: <Enum>[anEnum],
     );
     final StringBuffer sink = StringBuffer();
-    generateDart(DartOptions(), root, sink);
+    const DartGenerator generator = DartGenerator();
+    generator.generate(DartOptions(), root, sink);
     final String code = sink.toString();
     expect(code, contains('enum Foobar'));
     expect(code, contains('  one,'));
@@ -92,7 +94,8 @@
       ])
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
-    generateDart(DartOptions(), root, sink);
+    const DartGenerator generator = DartGenerator();
+    generator.generate(DartOptions(), root, sink);
     final String code = sink.toString();
     expect(code, contains('class Api'));
     expect(code, contains('Future<Output> doSomething(Input arg_input)'));
@@ -118,7 +121,8 @@
       ])
     ], classes: <Class>[], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
-    generateDart(DartOptions(), root, sink);
+    const DartGenerator generator = DartGenerator();
+    generator.generate(DartOptions(), root, sink);
     final String code = sink.toString();
     expect(code, contains('class Api'));
     expect(code, contains('Future<int> add(int arg_x, int arg_y)'));
@@ -145,7 +149,8 @@
       ])
     ], classes: <Class>[], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
-    generateDart(DartOptions(), root, sink);
+    const DartGenerator generator = DartGenerator();
+    generator.generate(DartOptions(), root, sink);
     final String code = sink.toString();
     expect(code, contains('class Api'));
     expect(code, contains('int add(int x, int y)'));
@@ -182,7 +187,8 @@
       )
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
-    generateDart(DartOptions(), root, sink);
+    const DartGenerator generator = DartGenerator();
+    generator.generate(DartOptions(), root, sink);
     final String code = sink.toString();
     expect(
       code,
@@ -224,7 +230,8 @@
       )
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
-    generateDart(DartOptions(), root, sink);
+    const DartGenerator generator = DartGenerator();
+    generator.generate(DartOptions(), root, sink);
     final String code = sink.toString();
     expect(
       code,
@@ -276,7 +283,8 @@
       ])
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
-    generateDart(DartOptions(), root, sink);
+    const DartGenerator generator = DartGenerator();
+    generator.generate(DartOptions(), root, sink);
     final String code = sink.toString();
     expect(code, contains('abstract class Api'));
     expect(code, contains('static void setup(Api'));
@@ -310,7 +318,8 @@
       ]),
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
-    generateDart(DartOptions(), root, sink);
+    const DartGenerator generator = DartGenerator();
+    generator.generate(DartOptions(), root, sink);
     final String code = sink.toString();
     expect(code, contains('Future<void> doSomething'));
     expect(code, contains('return;'));
@@ -343,7 +352,8 @@
       ]),
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
-    generateDart(DartOptions(), root, sink);
+    const DartGenerator generator = DartGenerator();
+    generator.generate(DartOptions(), root, sink);
     final String code = sink.toString();
     // The next line verifies that we're not setting a variable to the value of "doSomething", but
     // ignores the line where we assert the value of the argument isn't null, since on that line
@@ -373,7 +383,8 @@
       ]),
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
-    generateDart(DartOptions(), root, sink);
+    const DartGenerator generator = DartGenerator();
+    generator.generate(DartOptions(), root, sink);
     final String code = sink.toString();
     expect(code, matches('output.*=.*doSomething[(][)]'));
     expect(code, contains('Output doSomething();'));
@@ -415,7 +426,8 @@
       )
     ]);
     final StringBuffer sink = StringBuffer();
-    generateDart(DartOptions(), root, sink);
+    const DartGenerator generator = DartGenerator();
+    generator.generate(DartOptions(), root, sink);
     final String code = sink.toString();
     expect(code, contains('enum1?.index,'));
     expect(code, contains('? Enum.values[result[0]! as int]'));
@@ -442,7 +454,8 @@
       ])
     ]);
     final StringBuffer sink = StringBuffer();
-    generateDart(DartOptions(), root, sink);
+    const DartGenerator generator = DartGenerator();
+    generator.generate(DartOptions(), root, sink);
     final String code = sink.toString();
     expect(code, contains('enum Foo {'));
     expect(code, contains('Future<void> bar(Foo? arg_foo) async'));
@@ -485,7 +498,8 @@
       )
     ]);
     final StringBuffer sink = StringBuffer();
-    generateDart(DartOptions(), root, sink);
+    const DartGenerator generator = DartGenerator();
+    generator.generate(DartOptions(), root, sink);
     final String code = sink.toString();
     expect(code, contains('enum1.index,'));
     expect(code, contains('enum1: Enum.values[result[0]! as int]'));
@@ -512,7 +526,8 @@
       ]),
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
-    generateDart(DartOptions(), root, sink);
+    const DartGenerator generator = DartGenerator();
+    generator.generate(DartOptions(), root, sink);
     final String code = sink.toString();
     expect(code, matches('channel.send[(]null[)]'));
   });
@@ -570,7 +585,8 @@
     ], enums: <Enum>[]);
     final StringBuffer mainCodeSink = StringBuffer();
     final StringBuffer testCodeSink = StringBuffer();
-    generateDart(DartOptions(), root, mainCodeSink);
+    const DartGenerator generator = DartGenerator();
+    generator.generate(DartOptions(), root, mainCodeSink);
     final String mainCode = mainCodeSink.toString();
     expect(mainCode, isNot(contains(r"import 'fo\'o.dart';")));
     expect(mainCode, contains('class Api {'));
@@ -578,12 +594,15 @@
     expect(mainCode, isNot(contains('.ApiMock.doSomething')));
     expect(mainCode, isNot(contains("'${Keys.result}': output")));
     expect(mainCode, isNot(contains('return <Object>[];')));
-    generateTestDart(
-      DartOptions(),
+
+    const DartGenerator testGenerator = DartGenerator();
+    testGenerator.generateTest(
+      DartOptions(
+        sourceOutPath: "fo'o.dart",
+        testOutPath: 'test.dart',
+      ),
       root,
       testCodeSink,
-      sourceOutPath: "fo'o.dart",
-      testOutPath: 'test.dart',
     );
     final String testCode = testCodeSink.toString();
     expect(testCode, contains(r"import 'fo\'o.dart';"));
@@ -631,7 +650,8 @@
       ])
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
-    generateDart(DartOptions(), root, sink);
+    const DartGenerator generator = DartGenerator();
+    generator.generate(DartOptions(), root, sink);
     final String code = sink.toString();
     expect(code, contains('abstract class Api'));
     expect(code, contains('Future<Output> doSomething(Input arg0);'));
@@ -675,7 +695,8 @@
       ])
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
-    generateDart(DartOptions(), root, sink);
+    const DartGenerator generator = DartGenerator();
+    generator.generate(DartOptions(), root, sink);
     final String code = sink.toString();
     expect(code, isNot(matches('=.s*doSomething')));
     expect(code, contains('await api.doSomething('));
@@ -719,7 +740,8 @@
       ])
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
-    generateDart(DartOptions(), root, sink);
+    const DartGenerator generator = DartGenerator();
+    generator.generate(DartOptions(), root, sink);
     final String code = sink.toString();
     expect(code, contains('class Api'));
     expect(code, matches('Output.*doSomething.*Input'));
@@ -747,7 +769,8 @@
       ]),
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
-    generateDart(DartOptions(), root, sink);
+    const DartGenerator generator = DartGenerator();
+    generator.generate(DartOptions(), root, sink);
     final String code = sink.toString();
     expect(code, matches('channel.send[(]null[)]'));
   });
@@ -759,7 +782,9 @@
   test('header', () {
     final Root root = Root(apis: <Api>[], classes: <Class>[], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
-    generateDart(
+
+    const DartGenerator generator = DartGenerator();
+    generator.generate(
       DartOptions(copyrightHeader: makeIterable('hello world')),
       root,
       sink,
@@ -788,7 +813,8 @@
       enums: <Enum>[],
     );
     final StringBuffer sink = StringBuffer();
-    generateDart(DartOptions(), root, sink);
+    const DartGenerator generator = DartGenerator();
+    generator.generate(DartOptions(), root, sink);
     final String code = sink.toString();
     expect(code, contains('class Foobar'));
     expect(code, contains('  List<int?>? field1;'));
@@ -815,7 +841,8 @@
       enums: <Enum>[],
     );
     final StringBuffer sink = StringBuffer();
-    generateDart(DartOptions(), root, sink);
+    const DartGenerator generator = DartGenerator();
+    generator.generate(DartOptions(), root, sink);
     final String code = sink.toString();
     expect(code, contains('class Foobar'));
     expect(code, contains('  Map<String?, int?>? field1;'));
@@ -844,7 +871,8 @@
       enums: <Enum>[],
     );
     final StringBuffer sink = StringBuffer();
-    generateDart(DartOptions(), root, sink);
+    const DartGenerator generator = DartGenerator();
+    generator.generate(DartOptions(), root, sink);
     final String code = sink.toString();
     expect(code, contains('doit(List<int?> arg'));
   });
@@ -872,7 +900,8 @@
       enums: <Enum>[],
     );
     final StringBuffer sink = StringBuffer();
-    generateDart(DartOptions(), root, sink);
+    const DartGenerator generator = DartGenerator();
+    generator.generate(DartOptions(), root, sink);
     final String code = sink.toString();
     expect(code, contains('doit(List<int?> arg'));
   });
@@ -896,7 +925,8 @@
       enums: <Enum>[],
     );
     final StringBuffer sink = StringBuffer();
-    generateDart(DartOptions(), root, sink);
+    const DartGenerator generator = DartGenerator();
+    generator.generate(DartOptions(), root, sink);
     final String code = sink.toString();
     expect(code, contains('Future<List<int?>> doit('));
     expect(code,
@@ -931,7 +961,8 @@
       enums: <Enum>[],
     );
     final StringBuffer sink = StringBuffer();
-    generateDart(DartOptions(), root, sink);
+    const DartGenerator generator = DartGenerator();
+    generator.generate(DartOptions(), root, sink);
     final String code = sink.toString();
     expect(code, contains('List<int?> doit('));
     expect(
@@ -958,7 +989,8 @@
       enums: <Enum>[],
     );
     final StringBuffer sink = StringBuffer();
-    generateDart(DartOptions(), root, sink);
+    const DartGenerator generator = DartGenerator();
+    generator.generate(DartOptions(), root, sink);
     final String code = sink.toString();
     expect(code, contains('Future<int?> doit()'));
     expect(code, contains('return (replyList[0] as int?);'));
@@ -983,7 +1015,8 @@
       enums: <Enum>[],
     );
     final StringBuffer sink = StringBuffer();
-    generateDart(DartOptions(), root, sink);
+    const DartGenerator generator = DartGenerator();
+    generator.generate(DartOptions(), root, sink);
     final String code = sink.toString();
     expect(code, contains('Future<List<int?>?> doit()'));
     expect(code,
@@ -1008,7 +1041,8 @@
       enums: <Enum>[],
     );
     final StringBuffer sink = StringBuffer();
-    generateDart(DartOptions(), root, sink);
+    const DartGenerator generator = DartGenerator();
+    generator.generate(DartOptions(), root, sink);
     final String code = sink.toString();
     expect(code, contains('Future<int?> doit()'));
     expect(code, contains('return (replyList[0] as int?);'));
@@ -1031,7 +1065,8 @@
       enums: <Enum>[],
     );
     final StringBuffer sink = StringBuffer();
-    generateDart(DartOptions(), root, sink);
+    const DartGenerator generator = DartGenerator();
+    generator.generate(DartOptions(), root, sink);
     final String code = sink.toString();
     expect(code, contains('int? doit();'));
     expect(code, contains('final int? output = api.doit();'));
@@ -1055,7 +1090,8 @@
       enums: <Enum>[],
     );
     final StringBuffer sink = StringBuffer();
-    generateDart(DartOptions(), root, sink);
+    const DartGenerator generator = DartGenerator();
+    generator.generate(DartOptions(), root, sink);
     final String code = sink.toString();
     expect(code, contains('Future<int?> doit();'));
     expect(code, contains('final int? output = await api.doit();'));
@@ -1078,7 +1114,8 @@
       enums: <Enum>[],
     );
     final StringBuffer sink = StringBuffer();
-    generateDart(DartOptions(), root, sink);
+    const DartGenerator generator = DartGenerator();
+    generator.generate(DartOptions(), root, sink);
     final String code = sink.toString();
     expect(
         code,
@@ -1107,7 +1144,8 @@
       enums: <Enum>[],
     );
     final StringBuffer sink = StringBuffer();
-    generateDart(DartOptions(), root, sink);
+    const DartGenerator generator = DartGenerator();
+    generator.generate(DartOptions(), root, sink);
     final String code = sink.toString();
     expect(code, contains('Future<void> doit(int? arg_foo) async {'));
   });
@@ -1133,7 +1171,8 @@
       enums: <Enum>[],
     );
     final StringBuffer sink = StringBuffer();
-    generateDart(DartOptions(), root, sink);
+    const DartGenerator generator = DartGenerator();
+    generator.generate(DartOptions(), root, sink);
     final String code = sink.toString();
     expect(code, contains('void doit(int? foo);'));
   });
@@ -1150,12 +1189,14 @@
       final Root root =
           Root(classes: <Class>[], apis: <Api>[], enums: <Enum>[]);
       final StringBuffer sink = StringBuffer();
-      generateTestDart(
-        DartOptions(),
+      const DartGenerator testGenerator = DartGenerator();
+      testGenerator.generateTest(
+        DartOptions(
+          sourceOutPath: path.join(foo.path, 'bar.dart'),
+          testOutPath: path.join(tempDir.path, 'test', 'bar_test.dart'),
+        ),
         root,
         sink,
-        sourceOutPath: path.join(foo.path, 'bar.dart'),
-        testOutPath: path.join(tempDir.path, 'test', 'bar_test.dart'),
       );
       final String code = sink.toString();
       expect(code, contains("import 'package:foobar/foo/bar.dart';"));
@@ -1238,7 +1279,8 @@
       ],
     );
     final StringBuffer sink = StringBuffer();
-    generateDart(DartOptions(), root, sink);
+    const DartGenerator generator = DartGenerator();
+    generator.generate(DartOptions(), root, sink);
     final String code = sink.toString();
     for (final String comment in comments) {
       expect(code, contains('///$comment'));
@@ -1273,7 +1315,8 @@
       enums: <Enum>[],
     );
     final StringBuffer sink = StringBuffer();
-    generateDart(DartOptions(), root, sink);
+    const DartGenerator generator = DartGenerator();
+    generator.generate(DartOptions(), root, sink);
     final String code = sink.toString();
     expect(code, isNot(contains('extends StandardMessageCodec')));
     expect(code, contains('StandardMessageCodec'));
@@ -1316,7 +1359,8 @@
       ])
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
-    generateDart(DartOptions(), root, sink);
+    const DartGenerator generator = DartGenerator();
+    generator.generate(DartOptions(), root, sink);
     final String code = sink.toString();
     expect(code, contains('extends StandardMessageCodec'));
   });
@@ -1354,13 +1398,17 @@
       ],
     );
     final StringBuffer sink = StringBuffer();
-    generateTestDart(
-      DartOptions(),
+
+    const DartGenerator testGenerator = DartGenerator();
+    testGenerator.generateTest(
+      DartOptions(
+        sourceOutPath: 'code.dart',
+        testOutPath: 'test.dart',
+      ),
       root,
       sink,
-      sourceOutPath: 'code.dart',
-      testOutPath: 'test.dart',
     );
+
     final String testCode = sink.toString();
     expect(
         testCode,
diff --git a/packages/pigeon/test/java_generator_test.dart b/packages/pigeon/test/java_generator_test.dart
index bfcea98..40ec005 100644
--- a/packages/pigeon/test/java_generator_test.dart
+++ b/packages/pigeon/test/java_generator_test.dart
@@ -27,7 +27,8 @@
     );
     final StringBuffer sink = StringBuffer();
     const JavaOptions javaOptions = JavaOptions(className: 'Messages');
-    generateJava(javaOptions, root, sink);
+    const JavaGenerator generator = JavaGenerator();
+    generator.generate(javaOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('public class Messages'));
     expect(code, contains('public static class Foobar'));
@@ -55,7 +56,8 @@
     );
     final StringBuffer sink = StringBuffer();
     const JavaOptions javaOptions = JavaOptions(className: 'Messages');
-    generateJava(javaOptions, root, sink);
+    const JavaGenerator generator = JavaGenerator();
+    generator.generate(javaOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('public enum Foobar'));
     expect(code, contains('    ONE(0),'));
@@ -86,7 +88,8 @@
     final StringBuffer sink = StringBuffer();
     const JavaOptions javaOptions =
         JavaOptions(className: 'Messages', package: 'com.google.foobar');
-    generateJava(javaOptions, root, sink);
+    const JavaGenerator generator = JavaGenerator();
+    generator.generate(javaOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('package com.google.foobar;'));
     expect(code, contains('ArrayList<Object> toList()'));
@@ -129,7 +132,8 @@
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
     const JavaOptions javaOptions = JavaOptions(className: 'Messages');
-    generateJava(javaOptions, root, sink);
+    const JavaGenerator generator = JavaGenerator();
+    generator.generate(javaOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('public interface Api'));
     expect(code, matches('Output.*doSomething.*Input'));
@@ -200,7 +204,8 @@
 
     final StringBuffer sink = StringBuffer();
     const JavaOptions javaOptions = JavaOptions(className: 'Messages');
-    generateJava(javaOptions, root, sink);
+    const JavaGenerator generator = JavaGenerator();
+    generator.generate(javaOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('private @Nullable Boolean aBool;'));
     expect(code, contains('private @Nullable Long aInt;'));
@@ -249,7 +254,8 @@
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
     const JavaOptions javaOptions = JavaOptions(className: 'Messages');
-    generateJava(javaOptions, root, sink);
+    const JavaGenerator generator = JavaGenerator();
+    generator.generate(javaOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('public static class Api'));
     expect(code, matches('doSomething.*Input.*Output'));
@@ -283,7 +289,8 @@
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
     const JavaOptions javaOptions = JavaOptions(className: 'Messages');
-    generateJava(javaOptions, root, sink);
+    const JavaGenerator generator = JavaGenerator();
+    generator.generate(javaOptions, root, sink);
     final String code = sink.toString();
     expect(code, isNot(matches('=.*doSomething')));
     expect(code, contains('doSomething('));
@@ -317,7 +324,8 @@
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
     const JavaOptions javaOptions = JavaOptions(className: 'Messages');
-    generateJava(javaOptions, root, sink);
+    const JavaGenerator generator = JavaGenerator();
+    generator.generate(javaOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('Reply<Void>'));
     expect(code, contains('callback.reply(null)'));
@@ -345,7 +353,8 @@
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
     const JavaOptions javaOptions = JavaOptions(className: 'Messages');
-    generateJava(javaOptions, root, sink);
+    const JavaGenerator generator = JavaGenerator();
+    generator.generate(javaOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('Output doSomething()'));
     expect(code, contains('api.doSomething()'));
@@ -373,7 +382,8 @@
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
     const JavaOptions javaOptions = JavaOptions(className: 'Messages');
-    generateJava(javaOptions, root, sink);
+    const JavaGenerator generator = JavaGenerator();
+    generator.generate(javaOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('doSomething(Reply<Output>'));
     expect(code, contains('channel.send(null'));
@@ -392,7 +402,8 @@
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
     const JavaOptions javaOptions = JavaOptions(className: 'Messages');
-    generateJava(javaOptions, root, sink);
+    const JavaGenerator generator = JavaGenerator();
+    generator.generate(javaOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('public static class Foobar'));
     expect(code, contains('private @Nullable List<Object> field1;'));
@@ -411,7 +422,8 @@
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
     const JavaOptions javaOptions = JavaOptions(className: 'Messages');
-    generateJava(javaOptions, root, sink);
+    const JavaGenerator generator = JavaGenerator();
+    generator.generate(javaOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('public static class Foobar'));
     expect(code, contains('private @Nullable Map<Object, Object> field1;'));
@@ -447,7 +459,8 @@
     );
     final StringBuffer sink = StringBuffer();
     const JavaOptions javaOptions = JavaOptions(className: 'Messages');
-    generateJava(javaOptions, root, sink);
+    const JavaGenerator generator = JavaGenerator();
+    generator.generate(javaOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('public class Messages'));
     expect(code, contains('public static class Outer'));
@@ -498,7 +511,8 @@
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
     const JavaOptions javaOptions = JavaOptions(className: 'Messages');
-    generateJava(javaOptions, root, sink);
+    const JavaGenerator generator = JavaGenerator();
+    generator.generate(javaOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('public interface Api'));
     expect(code, contains('public interface Result<T> {'));
@@ -549,7 +563,8 @@
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
     const JavaOptions javaOptions = JavaOptions(className: 'Messages');
-    generateJava(javaOptions, root, sink);
+    const JavaGenerator generator = JavaGenerator();
+    generator.generate(javaOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('public static class Api'));
     expect(code, matches('doSomething.*Input.*Output'));
@@ -582,7 +597,8 @@
     );
     final StringBuffer sink = StringBuffer();
     const JavaOptions javaOptions = JavaOptions(className: 'Messages');
-    generateJava(javaOptions, root, sink);
+    const JavaGenerator generator = JavaGenerator();
+    generator.generate(javaOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('public enum Enum1'));
     expect(code, contains('    ONE(0),'));
@@ -621,7 +637,8 @@
     ]);
     final StringBuffer sink = StringBuffer();
     const JavaOptions javaOptions = JavaOptions(className: 'Messages');
-    generateJava(javaOptions, root, sink);
+    const JavaGenerator generator = JavaGenerator();
+    generator.generate(javaOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('public enum Foo'));
     expect(
@@ -641,7 +658,8 @@
       className: 'Messages',
       copyrightHeader: makeIterable('hello world'),
     );
-    generateJava(javaOptions, root, sink);
+    const JavaGenerator generator = JavaGenerator();
+    generator.generate(javaOptions, root, sink);
     final String code = sink.toString();
     expect(code, startsWith('// hello world'));
   });
@@ -667,7 +685,8 @@
     );
     final StringBuffer sink = StringBuffer();
     const JavaOptions javaOptions = JavaOptions(className: 'Messages');
-    generateJava(javaOptions, root, sink);
+    const JavaGenerator generator = JavaGenerator();
+    generator.generate(javaOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('class Foobar'));
     expect(code, contains('List<Long> field1;'));
@@ -695,7 +714,8 @@
     );
     final StringBuffer sink = StringBuffer();
     const JavaOptions javaOptions = JavaOptions(className: 'Messages');
-    generateJava(javaOptions, root, sink);
+    const JavaGenerator generator = JavaGenerator();
+    generator.generate(javaOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('class Foobar'));
     expect(code, contains('Map<String, String> field1;'));
@@ -725,7 +745,8 @@
     );
     final StringBuffer sink = StringBuffer();
     const JavaOptions javaOptions = JavaOptions(className: 'Messages');
-    generateJava(javaOptions, root, sink);
+    const JavaGenerator generator = JavaGenerator();
+    generator.generate(javaOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('doit(@NonNull List<Long> arg'));
   });
@@ -754,7 +775,8 @@
     );
     final StringBuffer sink = StringBuffer();
     const JavaOptions javaOptions = JavaOptions(className: 'Messages');
-    generateJava(javaOptions, root, sink);
+    const JavaGenerator generator = JavaGenerator();
+    generator.generate(javaOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('doit(@NonNull List<Long> arg'));
   });
@@ -779,7 +801,8 @@
     );
     final StringBuffer sink = StringBuffer();
     const JavaOptions javaOptions = JavaOptions(className: 'Messages');
-    generateJava(javaOptions, root, sink);
+    const JavaGenerator generator = JavaGenerator();
+    generator.generate(javaOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('List<Long> doit('));
     expect(code, contains('List<Long> output ='));
@@ -805,7 +828,8 @@
     );
     final StringBuffer sink = StringBuffer();
     const JavaOptions javaOptions = JavaOptions(className: 'Messages');
-    generateJava(javaOptions, root, sink);
+    const JavaGenerator generator = JavaGenerator();
+    generator.generate(javaOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('doit(Reply<List<Long>> callback)'));
     expect(code, contains('List<Long> output ='));
@@ -828,7 +852,8 @@
     );
     final StringBuffer sink = StringBuffer();
     const JavaOptions javaOptions = JavaOptions(className: 'Messages');
-    generateJava(javaOptions, root, sink);
+    const JavaGenerator generator = JavaGenerator();
+    generator.generate(javaOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('doit(Reply<Long> callback)'));
     expect(
@@ -858,7 +883,8 @@
     ], classes: <Class>[], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
     const JavaOptions javaOptions = JavaOptions(className: 'Messages');
-    generateJava(javaOptions, root, sink);
+    const JavaGenerator generator = JavaGenerator();
+    generator.generate(javaOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('class Messages'));
     expect(code, contains('Long add(@NonNull Long x, @NonNull Long y)'));
@@ -889,7 +915,8 @@
     ], classes: <Class>[], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
     const JavaOptions javaOptions = JavaOptions(className: 'Api');
-    generateJava(javaOptions, root, sink);
+    const JavaGenerator generator = JavaGenerator();
+    generator.generate(javaOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('Object xArg = args.get(0)'));
   });
@@ -915,7 +942,8 @@
     ], classes: <Class>[], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
     const JavaOptions javaOptions = JavaOptions(className: 'Messages');
-    generateJava(javaOptions, root, sink);
+    const JavaGenerator generator = JavaGenerator();
+    generator.generate(javaOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('class Messages'));
     expect(code, contains('BasicMessageChannel<Object> channel'));
@@ -947,7 +975,8 @@
     ], classes: <Class>[], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
     const JavaOptions javaOptions = JavaOptions(className: 'Messages');
-    generateJava(javaOptions, root, sink);
+    const JavaGenerator generator = JavaGenerator();
+    generator.generate(javaOptions, root, sink);
     final String code = sink.toString();
     expect(
         code,
@@ -973,7 +1002,8 @@
     );
     final StringBuffer sink = StringBuffer();
     const JavaOptions javaOptions = JavaOptions(className: 'Messages');
-    generateJava(javaOptions, root, sink);
+    const JavaGenerator generator = JavaGenerator();
+    generator.generate(javaOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('@Nullable Long doit();'));
   });
@@ -997,7 +1027,8 @@
     );
     final StringBuffer sink = StringBuffer();
     const JavaOptions javaOptions = JavaOptions(className: 'Messages');
-    generateJava(javaOptions, root, sink);
+    const JavaGenerator generator = JavaGenerator();
+    generator.generate(javaOptions, root, sink);
     final String code = sink.toString();
     // Java doesn't accept nullability annotations in type arguments.
     expect(code, contains('Result<Long>'));
@@ -1025,7 +1056,8 @@
     );
     final StringBuffer sink = StringBuffer();
     const JavaOptions javaOptions = JavaOptions(className: 'Messages');
-    generateJava(javaOptions, root, sink);
+    const JavaGenerator generator = JavaGenerator();
+    generator.generate(javaOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('  void doit(@Nullable Long foo);'));
   });
@@ -1052,7 +1084,8 @@
     );
     final StringBuffer sink = StringBuffer();
     const JavaOptions javaOptions = JavaOptions(className: 'Messages');
-    generateJava(javaOptions, root, sink);
+    const JavaGenerator generator = JavaGenerator();
+    generator.generate(javaOptions, root, sink);
     final String code = sink.toString();
     expect(
         code,
@@ -1083,7 +1116,8 @@
     );
     final StringBuffer sink = StringBuffer();
     const JavaOptions javaOptions = JavaOptions(className: 'Messages');
-    generateJava(javaOptions, root, sink);
+    const JavaGenerator generator = JavaGenerator();
+    generator.generate(javaOptions, root, sink);
     final String code = sink.toString();
     expect(
         code,
@@ -1108,7 +1142,8 @@
     final StringBuffer sink = StringBuffer();
     const JavaOptions javaOptions =
         JavaOptions(className: 'Messages', useGeneratedAnnotation: true);
-    generateJava(javaOptions, root, sink);
+    const JavaGenerator generator = JavaGenerator();
+    generator.generate(javaOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('@javax.annotation.Generated("dev.flutter.pigeon")'));
   });
@@ -1125,7 +1160,8 @@
     );
     final StringBuffer sink = StringBuffer();
     const JavaOptions javaOptions = JavaOptions(className: 'Messages');
-    generateJava(javaOptions, root, sink);
+    const JavaGenerator generator = JavaGenerator();
+    generator.generate(javaOptions, root, sink);
     final String code = sink.toString();
     expect(code,
         isNot(contains('@javax.annotation.Generated("dev.flutter.pigeon")')));
@@ -1207,7 +1243,8 @@
     );
     final StringBuffer sink = StringBuffer();
     const JavaOptions javaOptions = JavaOptions(className: 'Messages');
-    generateJava(javaOptions, root, sink);
+    const JavaGenerator generator = JavaGenerator();
+    generator.generate(javaOptions, root, sink);
     final String code = sink.toString();
     for (final String comment in comments) {
       // This regex finds the comment only between the open and close comment block
@@ -1247,7 +1284,8 @@
     );
     final StringBuffer sink = StringBuffer();
     const JavaOptions javaOptions = JavaOptions(className: 'Messages');
-    generateJava(javaOptions, root, sink);
+    const JavaGenerator generator = JavaGenerator();
+    generator.generate(javaOptions, root, sink);
     final String code = sink.toString();
     expect(code, isNot(contains(' extends StandardMessageCodec')));
     expect(code, contains('StandardMessageCodec'));
@@ -1291,7 +1329,8 @@
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
     const JavaOptions javaOptions = JavaOptions(className: 'Messages');
-    generateJava(javaOptions, root, sink);
+    const JavaGenerator generator = JavaGenerator();
+    generator.generate(javaOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains(' extends StandardMessageCodec'));
   });
diff --git a/packages/pigeon/test/kotlin_generator_test.dart b/packages/pigeon/test/kotlin_generator_test.dart
index 4653410..aa4ec3e 100644
--- a/packages/pigeon/test/kotlin_generator_test.dart
+++ b/packages/pigeon/test/kotlin_generator_test.dart
@@ -27,7 +27,8 @@
     );
     final StringBuffer sink = StringBuffer();
     const KotlinOptions kotlinOptions = KotlinOptions();
-    generateKotlin(kotlinOptions, root, sink);
+    const KotlinGenerator generator = KotlinGenerator();
+    generator.generate(kotlinOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('data class Foobar ('));
     expect(code, contains('val field1: Long? = null'));
@@ -50,7 +51,8 @@
     );
     final StringBuffer sink = StringBuffer();
     const KotlinOptions kotlinOptions = KotlinOptions();
-    generateKotlin(kotlinOptions, root, sink);
+    const KotlinGenerator generator = KotlinGenerator();
+    generator.generate(kotlinOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('enum class Foobar(val raw: Int) {'));
     expect(code, contains('ONE(0)'));
@@ -78,7 +80,8 @@
     ]);
     final StringBuffer sink = StringBuffer();
     const KotlinOptions kotlinOptions = KotlinOptions();
-    generateKotlin(kotlinOptions, root, sink);
+    const KotlinGenerator generator = KotlinGenerator();
+    generator.generate(kotlinOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('enum class Foo(val raw: Int) {'));
     expect(code, contains('val fooArg = Foo.ofRaw(args[0] as Int)'));
@@ -124,7 +127,8 @@
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
     const KotlinOptions kotlinOptions = KotlinOptions();
-    generateKotlin(kotlinOptions, root, sink);
+    const KotlinGenerator generator = KotlinGenerator();
+    generator.generate(kotlinOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('interface Api'));
     expect(code, contains('fun doSomething(input: Input): Output'));
@@ -213,7 +217,8 @@
     final StringBuffer sink = StringBuffer();
 
     const KotlinOptions kotlinOptions = KotlinOptions();
-    generateKotlin(kotlinOptions, root, sink);
+    const KotlinGenerator generator = KotlinGenerator();
+    generator.generate(kotlinOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('val aBool: Boolean? = null'));
     expect(code, contains('val aInt: Long? = null'));
@@ -265,7 +270,8 @@
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
     const KotlinOptions kotlinOptions = KotlinOptions();
-    generateKotlin(kotlinOptions, root, sink);
+    const KotlinGenerator generator = KotlinGenerator();
+    generator.generate(kotlinOptions, root, sink);
     final String code = sink.toString();
     expect(code,
         contains('class Api(private val binaryMessenger: BinaryMessenger)'));
@@ -302,7 +308,8 @@
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
     const KotlinOptions kotlinOptions = KotlinOptions();
-    generateKotlin(kotlinOptions, root, sink);
+    const KotlinGenerator generator = KotlinGenerator();
+    generator.generate(kotlinOptions, root, sink);
     final String code = sink.toString();
     expect(code, isNot(matches('.*doSomething(.*) ->')));
     expect(code, matches('doSomething(.*)'));
@@ -338,7 +345,8 @@
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
     const KotlinOptions kotlinOptions = KotlinOptions();
-    generateKotlin(kotlinOptions, root, sink);
+    const KotlinGenerator generator = KotlinGenerator();
+    generator.generate(kotlinOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('callback: () -> Unit'));
     expect(code, contains('callback()'));
@@ -367,7 +375,8 @@
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
     const KotlinOptions kotlinOptions = KotlinOptions();
-    generateKotlin(kotlinOptions, root, sink);
+    const KotlinGenerator generator = KotlinGenerator();
+    generator.generate(kotlinOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('fun doSomething(): Output'));
     expect(code, contains('wrapped = listOf<Any?>(api.doSomething())'));
@@ -398,7 +407,8 @@
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
     const KotlinOptions kotlinOptions = KotlinOptions();
-    generateKotlin(kotlinOptions, root, sink);
+    const KotlinGenerator generator = KotlinGenerator();
+    generator.generate(kotlinOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('fun doSomething(callback: (Output) -> Unit)'));
     expect(code, contains('channel.send(null)'));
@@ -418,7 +428,8 @@
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
     const KotlinOptions kotlinOptions = KotlinOptions();
-    generateKotlin(kotlinOptions, root, sink);
+    const KotlinGenerator generator = KotlinGenerator();
+    generator.generate(kotlinOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('data class Foobar'));
     expect(code, contains('val field1: List<Any?>? = null'));
@@ -438,7 +449,8 @@
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
     const KotlinOptions kotlinOptions = KotlinOptions();
-    generateKotlin(kotlinOptions, root, sink);
+    const KotlinGenerator generator = KotlinGenerator();
+    generator.generate(kotlinOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('data class Foobar'));
     expect(code, contains('val field1: Map<Any, Any?>? = null'));
@@ -476,7 +488,8 @@
     );
     final StringBuffer sink = StringBuffer();
     const KotlinOptions kotlinOptions = KotlinOptions();
-    generateKotlin(kotlinOptions, root, sink);
+    const KotlinGenerator generator = KotlinGenerator();
+    generator.generate(kotlinOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('data class Outer'));
     expect(code, contains('data class Nested'));
@@ -529,7 +542,8 @@
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
     const KotlinOptions kotlinOptions = KotlinOptions();
-    generateKotlin(kotlinOptions, root, sink);
+    const KotlinGenerator generator = KotlinGenerator();
+    generator.generate(kotlinOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('interface Api'));
     expect(code, contains('api.doSomething(argArg) {'));
@@ -577,7 +591,8 @@
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
     const KotlinOptions kotlinOptions = KotlinOptions();
-    generateKotlin(kotlinOptions, root, sink);
+    const KotlinGenerator generator = KotlinGenerator();
+    generator.generate(kotlinOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('class Api'));
     expect(code, matches('fun doSomething.*Input.*callback.*Output.*Unit'));
@@ -610,7 +625,8 @@
     );
     final StringBuffer sink = StringBuffer();
     const KotlinOptions kotlinOptions = KotlinOptions();
-    generateKotlin(kotlinOptions, root, sink);
+    const KotlinGenerator generator = KotlinGenerator();
+    generator.generate(kotlinOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('enum class Enum1(val raw: Int)'));
     expect(code, contains('ONE(0)'));
@@ -627,7 +643,8 @@
     final KotlinOptions kotlinOptions = KotlinOptions(
       copyrightHeader: makeIterable('hello world'),
     );
-    generateKotlin(kotlinOptions, root, sink);
+    const KotlinGenerator generator = KotlinGenerator();
+    generator.generate(kotlinOptions, root, sink);
     final String code = sink.toString();
     expect(code, startsWith('// hello world'));
   });
@@ -654,7 +671,8 @@
     );
     final StringBuffer sink = StringBuffer();
     const KotlinOptions kotlinOptions = KotlinOptions();
-    generateKotlin(kotlinOptions, root, sink);
+    const KotlinGenerator generator = KotlinGenerator();
+    generator.generate(kotlinOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('data class Foobar'));
     expect(code, contains('val field1: List<Long?>'));
@@ -683,7 +701,8 @@
     );
     final StringBuffer sink = StringBuffer();
     const KotlinOptions kotlinOptions = KotlinOptions();
-    generateKotlin(kotlinOptions, root, sink);
+    const KotlinGenerator generator = KotlinGenerator();
+    generator.generate(kotlinOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('data class Foobar'));
     expect(code, contains('val field1: Map<String?, String?>'));
@@ -714,7 +733,8 @@
     );
     final StringBuffer sink = StringBuffer();
     const KotlinOptions kotlinOptions = KotlinOptions();
-    generateKotlin(kotlinOptions, root, sink);
+    const KotlinGenerator generator = KotlinGenerator();
+    generator.generate(kotlinOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('fun doit(arg: List<Long?>'));
   });
@@ -744,7 +764,8 @@
     );
     final StringBuffer sink = StringBuffer();
     const KotlinOptions kotlinOptions = KotlinOptions();
-    generateKotlin(kotlinOptions, root, sink);
+    const KotlinGenerator generator = KotlinGenerator();
+    generator.generate(kotlinOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('fun doit(argArg: List<Long?>'));
   });
@@ -769,7 +790,8 @@
     );
     final StringBuffer sink = StringBuffer();
     const KotlinOptions kotlinOptions = KotlinOptions();
-    generateKotlin(kotlinOptions, root, sink);
+    const KotlinGenerator generator = KotlinGenerator();
+    generator.generate(kotlinOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('fun doit(): List<Long?>'));
     expect(code, contains('wrapped = listOf<Any?>(api.doit())'));
@@ -796,7 +818,8 @@
     );
     final StringBuffer sink = StringBuffer();
     const KotlinOptions kotlinOptions = KotlinOptions();
-    generateKotlin(kotlinOptions, root, sink);
+    const KotlinGenerator generator = KotlinGenerator();
+    generator.generate(kotlinOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('fun doit(callback: (List<Long?>) -> Unit'));
     expect(code, contains('val result = it as List<Long?>'));
@@ -824,7 +847,8 @@
     ], classes: <Class>[], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
     const KotlinOptions kotlinOptions = KotlinOptions();
-    generateKotlin(kotlinOptions, root, sink);
+    const KotlinGenerator generator = KotlinGenerator();
+    generator.generate(kotlinOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('fun add(x: Long, y: Long): Long'));
     expect(code, contains('val args = message as List<Any?>'));
@@ -861,7 +885,8 @@
     ], classes: <Class>[], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
     const KotlinOptions kotlinOptions = KotlinOptions();
-    generateKotlin(kotlinOptions, root, sink);
+    const KotlinGenerator generator = KotlinGenerator();
+    generator.generate(kotlinOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('val channel = BasicMessageChannel'));
     expect(code, contains('val result = it as Long'));
@@ -889,7 +914,8 @@
     );
     final StringBuffer sink = StringBuffer();
     const KotlinOptions kotlinOptions = KotlinOptions();
-    generateKotlin(kotlinOptions, root, sink);
+    const KotlinGenerator generator = KotlinGenerator();
+    generator.generate(kotlinOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('fun doit(): Long?'));
   });
@@ -913,7 +939,8 @@
     );
     final StringBuffer sink = StringBuffer();
     const KotlinOptions kotlinOptions = KotlinOptions();
-    generateKotlin(kotlinOptions, root, sink);
+    const KotlinGenerator generator = KotlinGenerator();
+    generator.generate(kotlinOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('fun doit(callback: (Long?) -> Unit'));
   });
@@ -940,7 +967,8 @@
     );
     final StringBuffer sink = StringBuffer();
     const KotlinOptions kotlinOptions = KotlinOptions();
-    generateKotlin(kotlinOptions, root, sink);
+    const KotlinGenerator generator = KotlinGenerator();
+    generator.generate(kotlinOptions, root, sink);
     final String code = sink.toString();
     expect(
         code,
@@ -970,7 +998,8 @@
     );
     final StringBuffer sink = StringBuffer();
     const KotlinOptions kotlinOptions = KotlinOptions();
-    generateKotlin(kotlinOptions, root, sink);
+    const KotlinGenerator generator = KotlinGenerator();
+    generator.generate(kotlinOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('fun doit(fooArg: Long?, callback: () -> Unit'));
   });
@@ -1005,7 +1034,8 @@
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
     const KotlinOptions kotlinOptions = KotlinOptions();
-    generateKotlin(kotlinOptions, root, sink);
+    const KotlinGenerator generator = KotlinGenerator();
+    generator.generate(kotlinOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('val input: String\n'));
   });
@@ -1086,7 +1116,8 @@
     );
     final StringBuffer sink = StringBuffer();
     const KotlinOptions kotlinOptions = KotlinOptions();
-    generateKotlin(kotlinOptions, root, sink);
+    const KotlinGenerator generator = KotlinGenerator();
+    generator.generate(kotlinOptions, root, sink);
     final String code = sink.toString();
     for (final String comment in comments) {
       // This regex finds the comment only between the open and close comment block
@@ -1126,7 +1157,8 @@
     );
     final StringBuffer sink = StringBuffer();
     const KotlinOptions kotlinOptions = KotlinOptions();
-    generateKotlin(kotlinOptions, root, sink);
+    const KotlinGenerator generator = KotlinGenerator();
+    generator.generate(kotlinOptions, root, sink);
     final String code = sink.toString();
     expect(code, isNot(contains(' : StandardMessageCodec() ')));
     expect(code, contains('StandardMessageCodec'));
@@ -1170,7 +1202,8 @@
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
     const KotlinOptions kotlinOptions = KotlinOptions();
-    generateKotlin(kotlinOptions, root, sink);
+    const KotlinGenerator generator = KotlinGenerator();
+    generator.generate(kotlinOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains(' : StandardMessageCodec() '));
   });
diff --git a/packages/pigeon/test/objc_generator_test.dart b/packages/pigeon/test/objc_generator_test.dart
index aeb2db6..64f92c8 100644
--- a/packages/pigeon/test/objc_generator_test.dart
+++ b/packages/pigeon/test/objc_generator_test.dart
@@ -3,6 +3,7 @@
 // found in the LICENSE file.
 
 import 'package:pigeon/ast.dart';
+import 'package:pigeon/generator_tools.dart';
 import 'package:pigeon/objc_generator.dart';
 import 'package:pigeon/pigeon_lib.dart';
 import 'package:test/test.dart';
@@ -17,7 +18,13 @@
       ]),
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
-    generateObjcHeader(const ObjcOptions(), root, sink);
+    const ObjcGenerator generator = ObjcGenerator();
+    final OutputFileOptions<ObjcOptions> generatorOptions =
+        OutputFileOptions<ObjcOptions>(
+      fileType: FileType.header,
+      languageOptions: const ObjcOptions(),
+    );
+    generator.generate(generatorOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('@interface Foobar'));
     expect(code, matches('@property.*NSString.*field1'));
@@ -32,8 +39,13 @@
       ]),
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
-    generateObjcSource(
-        const ObjcOptions(headerIncludePath: 'foo.h'), root, sink);
+    const ObjcGenerator generator = ObjcGenerator();
+    final OutputFileOptions<ObjcOptions> generatorOptions =
+        OutputFileOptions<ObjcOptions>(
+      fileType: FileType.source,
+      languageOptions: const ObjcOptions(headerIncludePath: 'foo.h'),
+    );
+    generator.generate(generatorOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('#import "foo.h"'));
     expect(code, contains('@implementation Foobar'));
@@ -50,7 +62,13 @@
       )
     ]);
     final StringBuffer sink = StringBuffer();
-    generateObjcHeader(const ObjcOptions(), root, sink);
+    const ObjcGenerator generator = ObjcGenerator();
+    final OutputFileOptions<ObjcOptions> generatorOptions =
+        OutputFileOptions<ObjcOptions>(
+      fileType: FileType.header,
+      languageOptions: const ObjcOptions(),
+    );
+    generator.generate(generatorOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('typedef NS_ENUM(NSUInteger, Enum1) {'));
     expect(code, contains('  Enum1One = 0,'));
@@ -68,7 +86,13 @@
       )
     ]);
     final StringBuffer sink = StringBuffer();
-    generateObjcHeader(const ObjcOptions(prefix: 'PREFIX'), root, sink);
+    const ObjcGenerator generator = ObjcGenerator();
+    final OutputFileOptions<ObjcOptions> generatorOptions =
+        OutputFileOptions<ObjcOptions>(
+      fileType: FileType.header,
+      languageOptions: const ObjcOptions(prefix: 'PREFIX'),
+    );
+    generator.generate(generatorOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('typedef NS_ENUM(NSUInteger, PREFIXEnum1) {'));
     expect(code, contains('  PREFIXEnum1One = 0,'));
@@ -104,8 +128,13 @@
       ],
     );
     final StringBuffer sink = StringBuffer();
-    generateObjcSource(
-        const ObjcOptions(headerIncludePath: 'foo.h'), root, sink);
+    const ObjcGenerator generator = ObjcGenerator();
+    final OutputFileOptions<ObjcOptions> generatorOptions =
+        OutputFileOptions<ObjcOptions>(
+      fileType: FileType.source,
+      languageOptions: const ObjcOptions(headerIncludePath: 'foo.h'),
+    );
+    generator.generate(generatorOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('#import "foo.h"'));
     expect(code, contains('@implementation Foobar'));
@@ -138,13 +167,25 @@
     const ObjcOptions options =
         ObjcOptions(headerIncludePath: 'foo.h', prefix: 'AC');
     {
-      generateObjcHeader(options, root, sink);
+      const ObjcGenerator generator = ObjcGenerator();
+      final OutputFileOptions<ObjcOptions> generatorOptions =
+          OutputFileOptions<ObjcOptions>(
+        fileType: FileType.header,
+        languageOptions: options,
+      );
+      generator.generate(generatorOptions, root, sink);
       final String code = sink.toString();
       expect(code, contains('typedef NS_ENUM(NSUInteger, ACFoo)'));
       expect(code, contains(':(ACFoo)foo error:'));
     }
     {
-      generateObjcSource(options, root, sink);
+      const ObjcGenerator generator = ObjcGenerator();
+      final OutputFileOptions<ObjcOptions> generatorOptions =
+          OutputFileOptions<ObjcOptions>(
+        fileType: FileType.source,
+        languageOptions: options,
+      );
+      generator.generate(generatorOptions, root, sink);
       final String code = sink.toString();
       expect(
           code,
@@ -207,8 +248,13 @@
       ],
     );
     final StringBuffer sink = StringBuffer();
-    generateObjcHeader(
-        const ObjcOptions(headerIncludePath: 'foo.h'), root, sink);
+    const ObjcGenerator generator = ObjcGenerator();
+    final OutputFileOptions<ObjcOptions> generatorOptions =
+        OutputFileOptions<ObjcOptions>(
+      fileType: FileType.header,
+      languageOptions: const ObjcOptions(headerIncludePath: 'foo.h'),
+    );
+    generator.generate(generatorOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('@property(nonatomic, assign) Enum1 enum1'));
   });
@@ -240,7 +286,13 @@
       ])
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
-    generateObjcHeader(const ObjcOptions(), root, sink);
+    const ObjcGenerator generator = ObjcGenerator();
+    final OutputFileOptions<ObjcOptions> generatorOptions =
+        OutputFileOptions<ObjcOptions>(
+      fileType: FileType.header,
+      languageOptions: const ObjcOptions(),
+    );
+    generator.generate(generatorOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('@interface Input'));
     expect(code, contains('@interface Output'));
@@ -279,8 +331,13 @@
       ])
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
-    generateObjcSource(
-        const ObjcOptions(headerIncludePath: 'foo.h'), root, sink);
+    const ObjcGenerator generator = ObjcGenerator();
+    final OutputFileOptions<ObjcOptions> generatorOptions =
+        OutputFileOptions<ObjcOptions>(
+      fileType: FileType.source,
+      languageOptions: const ObjcOptions(headerIncludePath: 'foo.h'),
+    );
+    generator.generate(generatorOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('#import "foo.h"'));
     expect(code, contains('@implementation Input'));
@@ -327,8 +384,13 @@
     ], enums: <Enum>[]);
 
     final StringBuffer sink = StringBuffer();
-    generateObjcHeader(
-        const ObjcOptions(headerIncludePath: 'foo.h'), root, sink);
+    const ObjcGenerator generator = ObjcGenerator();
+    final OutputFileOptions<ObjcOptions> generatorOptions =
+        OutputFileOptions<ObjcOptions>(
+      fileType: FileType.header,
+      languageOptions: const ObjcOptions(headerIncludePath: 'foo.h'),
+    );
+    generator.generate(generatorOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('@interface Foobar'));
     expect(code, contains('@class FlutterStandardTypedData;'));
@@ -356,8 +418,13 @@
     ], enums: <Enum>[]);
 
     final StringBuffer sink = StringBuffer();
-    generateObjcSource(
-        const ObjcOptions(headerIncludePath: 'foo.h'), root, sink);
+    const ObjcGenerator generator = ObjcGenerator();
+    final OutputFileOptions<ObjcOptions> generatorOptions =
+        OutputFileOptions<ObjcOptions>(
+      fileType: FileType.source,
+      languageOptions: const ObjcOptions(headerIncludePath: 'foo.h'),
+    );
+    generator.generate(generatorOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('@implementation Foobar'));
     expect(code,
@@ -378,8 +445,13 @@
       ])
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
-    generateObjcHeader(
-        const ObjcOptions(headerIncludePath: 'foo.h'), root, sink);
+    const ObjcGenerator generator = ObjcGenerator();
+    final OutputFileOptions<ObjcOptions> generatorOptions =
+        OutputFileOptions<ObjcOptions>(
+      fileType: FileType.header,
+      languageOptions: const ObjcOptions(headerIncludePath: 'foo.h'),
+    );
+    generator.generate(generatorOptions, root, sink);
     final String code = sink.toString();
     expect(code,
         contains('@property(nonatomic, strong, nullable) Input * nested;'));
@@ -399,8 +471,13 @@
       ])
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
-    generateObjcSource(
-        const ObjcOptions(headerIncludePath: 'foo.h'), root, sink);
+    const ObjcGenerator generator = ObjcGenerator();
+    final OutputFileOptions<ObjcOptions> generatorOptions =
+        OutputFileOptions<ObjcOptions>(
+      fileType: FileType.source,
+      languageOptions: const ObjcOptions(headerIncludePath: 'foo.h'),
+    );
+    generator.generate(generatorOptions, root, sink);
     final String code = sink.toString();
     expect(
         code,
@@ -419,7 +496,13 @@
       ]),
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
-    generateObjcHeader(const ObjcOptions(prefix: 'ABC'), root, sink);
+    const ObjcGenerator generator = ObjcGenerator();
+    final OutputFileOptions<ObjcOptions> generatorOptions =
+        OutputFileOptions<ObjcOptions>(
+      fileType: FileType.header,
+      languageOptions: const ObjcOptions(prefix: 'ABC'),
+    );
+    generator.generate(generatorOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('@interface ABCFoobar'));
   });
@@ -433,7 +516,13 @@
       ]),
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
-    generateObjcSource(const ObjcOptions(prefix: 'ABC'), root, sink);
+    const ObjcGenerator generator = ObjcGenerator();
+    final OutputFileOptions<ObjcOptions> generatorOptions =
+        OutputFileOptions<ObjcOptions>(
+      fileType: FileType.source,
+      languageOptions: const ObjcOptions(prefix: 'ABC'),
+    );
+    generator.generate(generatorOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('@implementation ABCFoobar'));
   });
@@ -467,7 +556,13 @@
       ])
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
-    generateObjcHeader(const ObjcOptions(prefix: 'ABC'), root, sink);
+    const ObjcGenerator generator = ObjcGenerator();
+    final OutputFileOptions<ObjcOptions> generatorOptions =
+        OutputFileOptions<ObjcOptions>(
+      fileType: FileType.header,
+      languageOptions: const ObjcOptions(prefix: 'ABC'),
+    );
+    generator.generate(generatorOptions, root, sink);
     final String code = sink.toString();
     expect(code, matches('property.*ABCInput'));
     expect(code, matches('ABCNested.*doSomething.*ABCInput'));
@@ -503,7 +598,13 @@
       ])
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
-    generateObjcSource(const ObjcOptions(prefix: 'ABC'), root, sink);
+    const ObjcGenerator generator = ObjcGenerator();
+    final OutputFileOptions<ObjcOptions> generatorOptions =
+        OutputFileOptions<ObjcOptions>(
+      fileType: FileType.source,
+      languageOptions: const ObjcOptions(prefix: 'ABC'),
+    );
+    generator.generate(generatorOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('ABCInput fromList'));
     expect(code, matches(r'ABCInput.*=.*args.*0.*\;'));
@@ -539,8 +640,13 @@
       ])
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
-    generateObjcHeader(
-        const ObjcOptions(headerIncludePath: 'foo.h'), root, sink);
+    const ObjcGenerator generator = ObjcGenerator();
+    final OutputFileOptions<ObjcOptions> generatorOptions =
+        OutputFileOptions<ObjcOptions>(
+      fileType: FileType.header,
+      languageOptions: const ObjcOptions(headerIncludePath: 'foo.h'),
+    );
+    generator.generate(generatorOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('@interface Api : NSObject'));
     expect(
@@ -579,8 +685,13 @@
       ])
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
-    generateObjcSource(
-        const ObjcOptions(headerIncludePath: 'foo.h'), root, sink);
+    const ObjcGenerator generator = ObjcGenerator();
+    final OutputFileOptions<ObjcOptions> generatorOptions =
+        OutputFileOptions<ObjcOptions>(
+      fileType: FileType.source,
+      languageOptions: const ObjcOptions(headerIncludePath: 'foo.h'),
+    );
+    generator.generate(generatorOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('@implementation Api'));
     expect(code, matches('void.*doSomething.*Input.*Output.*{'));
@@ -609,10 +720,14 @@
       ]),
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
-    generateObjcHeader(
-        const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
-        root,
-        sink);
+    const ObjcGenerator generator = ObjcGenerator();
+    final OutputFileOptions<ObjcOptions> generatorOptions =
+        OutputFileOptions<ObjcOptions>(
+      fileType: FileType.header,
+      languageOptions:
+          const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
+    );
+    generator.generate(generatorOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('(void)doSomething:'));
   });
@@ -640,10 +755,14 @@
       ]),
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
-    generateObjcSource(
-        const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
-        root,
-        sink);
+    const ObjcGenerator generator = ObjcGenerator();
+    final OutputFileOptions<ObjcOptions> generatorOptions =
+        OutputFileOptions<ObjcOptions>(
+      fileType: FileType.source,
+      languageOptions:
+          const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
+    );
+    generator.generate(generatorOptions, root, sink);
     final String code = sink.toString();
     expect(code, isNot(matches('=.*doSomething')));
     expect(code, matches('[.*doSomething:.*]'));
@@ -673,10 +792,14 @@
       ]),
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
-    generateObjcHeader(
-        const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
-        root,
-        sink);
+    const ObjcGenerator generator = ObjcGenerator();
+    final OutputFileOptions<ObjcOptions> generatorOptions =
+        OutputFileOptions<ObjcOptions>(
+      fileType: FileType.header,
+      languageOptions:
+          const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
+    );
+    generator.generate(generatorOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('completion:(void(^)(NSError *_Nullable))'));
   });
@@ -704,10 +827,14 @@
       ]),
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
-    generateObjcSource(
-        const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
-        root,
-        sink);
+    const ObjcGenerator generator = ObjcGenerator();
+    final OutputFileOptions<ObjcOptions> generatorOptions =
+        OutputFileOptions<ObjcOptions>(
+      fileType: FileType.source,
+      languageOptions:
+          const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
+    );
+    generator.generate(generatorOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('completion:(void(^)(NSError *_Nullable))'));
     expect(code, contains('completion(nil)'));
@@ -730,10 +857,14 @@
       ]),
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
-    generateObjcHeader(
-        const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
-        root,
-        sink);
+    const ObjcGenerator generator = ObjcGenerator();
+    final OutputFileOptions<ObjcOptions> generatorOptions =
+        OutputFileOptions<ObjcOptions>(
+      fileType: FileType.header,
+      languageOptions:
+          const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
+    );
+    generator.generate(generatorOptions, root, sink);
     final String code = sink.toString();
     expect(code, matches('ABCOutput.*doSomethingWithError:[(]FlutterError'));
   });
@@ -755,10 +886,14 @@
       ]),
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
-    generateObjcSource(
-        const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
-        root,
-        sink);
+    const ObjcGenerator generator = ObjcGenerator();
+    final OutputFileOptions<ObjcOptions> generatorOptions =
+        OutputFileOptions<ObjcOptions>(
+      fileType: FileType.source,
+      languageOptions:
+          const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
+    );
+    generator.generate(generatorOptions, root, sink);
     final String code = sink.toString();
     expect(code, matches('output.*=.*api doSomethingWithError:&error'));
   });
@@ -780,10 +915,14 @@
       ]),
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
-    generateObjcHeader(
-        const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
-        root,
-        sink);
+    const ObjcGenerator generator = ObjcGenerator();
+    final OutputFileOptions<ObjcOptions> generatorOptions =
+        OutputFileOptions<ObjcOptions>(
+      fileType: FileType.header,
+      languageOptions:
+          const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
+    );
+    generator.generate(generatorOptions, root, sink);
     final String code = sink.toString();
     expect(
         code,
@@ -808,10 +947,14 @@
       ]),
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
-    generateObjcSource(
-        const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
-        root,
-        sink);
+    const ObjcGenerator generator = ObjcGenerator();
+    final OutputFileOptions<ObjcOptions> generatorOptions =
+        OutputFileOptions<ObjcOptions>(
+      fileType: FileType.source,
+      languageOptions:
+          const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
+    );
+    generator.generate(generatorOptions, root, sink);
     final String code = sink.toString();
     expect(
         code,
@@ -829,7 +972,13 @@
       ]),
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
-    generateObjcHeader(const ObjcOptions(), root, sink);
+    const ObjcGenerator generator = ObjcGenerator();
+    final OutputFileOptions<ObjcOptions> generatorOptions =
+        OutputFileOptions<ObjcOptions>(
+      fileType: FileType.header,
+      languageOptions: const ObjcOptions(),
+    );
+    generator.generate(generatorOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('@interface Foobar'));
     expect(code, matches('@property.*NSArray.*field1'));
@@ -844,7 +993,13 @@
       ]),
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
-    generateObjcHeader(const ObjcOptions(), root, sink);
+    const ObjcGenerator generator = ObjcGenerator();
+    final OutputFileOptions<ObjcOptions> generatorOptions =
+        OutputFileOptions<ObjcOptions>(
+      fileType: FileType.header,
+      languageOptions: const ObjcOptions(),
+    );
+    generator.generate(generatorOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('@interface Foobar'));
     expect(code, matches('@property.*NSDictionary.*field1'));
@@ -865,7 +1020,13 @@
       ]),
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
-    generateObjcHeader(const ObjcOptions(), root, sink);
+    const ObjcGenerator generator = ObjcGenerator();
+    final OutputFileOptions<ObjcOptions> generatorOptions =
+        OutputFileOptions<ObjcOptions>(
+      fileType: FileType.header,
+      languageOptions: const ObjcOptions(),
+    );
+    generator.generate(generatorOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('@interface Foobar'));
     expect(
@@ -894,7 +1055,13 @@
       ])
     ], classes: <Class>[], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
-    generateObjcHeader(const ObjcOptions(), root, sink);
+    const ObjcGenerator generator = ObjcGenerator();
+    final OutputFileOptions<ObjcOptions> generatorOptions =
+        OutputFileOptions<ObjcOptions>(
+      fileType: FileType.header,
+      languageOptions: const ObjcOptions(),
+    );
+    generator.generate(generatorOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('(NSDictionary<NSString *, id> *)foo'));
   });
@@ -928,10 +1095,14 @@
       ]),
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
-    generateObjcHeader(
-        const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
-        root,
-        sink);
+    const ObjcGenerator generator = ObjcGenerator();
+    final OutputFileOptions<ObjcOptions> generatorOptions =
+        OutputFileOptions<ObjcOptions>(
+      fileType: FileType.header,
+      languageOptions:
+          const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
+    );
+    generator.generate(generatorOptions, root, sink);
     final String code = sink.toString();
     expect(
         code,
@@ -969,10 +1140,14 @@
       ]),
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
-    generateObjcHeader(
-        const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
-        root,
-        sink);
+    const ObjcGenerator generator = ObjcGenerator();
+    final OutputFileOptions<ObjcOptions> generatorOptions =
+        OutputFileOptions<ObjcOptions>(
+      fileType: FileType.header,
+      languageOptions:
+          const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
+    );
+    generator.generate(generatorOptions, root, sink);
     final String code = sink.toString();
     expect(
         code,
@@ -998,10 +1173,14 @@
       ]),
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
-    generateObjcHeader(
-        const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
-        root,
-        sink);
+    const ObjcGenerator generator = ObjcGenerator();
+    final OutputFileOptions<ObjcOptions> generatorOptions =
+        OutputFileOptions<ObjcOptions>(
+      fileType: FileType.header,
+      languageOptions:
+          const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
+    );
+    generator.generate(generatorOptions, root, sink);
     final String code = sink.toString();
     expect(
         code,
@@ -1020,10 +1199,14 @@
       ])
     ], classes: <Class>[], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
-    generateObjcHeader(
-        const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
-        root,
-        sink);
+    const ObjcGenerator generator = ObjcGenerator();
+    final OutputFileOptions<ObjcOptions> generatorOptions =
+        OutputFileOptions<ObjcOptions>(
+      fileType: FileType.header,
+      languageOptions:
+          const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
+    );
+    generator.generate(generatorOptions, root, sink);
     final String code = sink.toString();
     expect(
         code,
@@ -1061,10 +1244,14 @@
       ]),
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
-    generateObjcSource(
-        const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
-        root,
-        sink);
+    const ObjcGenerator generator = ObjcGenerator();
+    final OutputFileOptions<ObjcOptions> generatorOptions =
+        OutputFileOptions<ObjcOptions>(
+      fileType: FileType.source,
+      languageOptions:
+          const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
+    );
+    generator.generate(generatorOptions, root, sink);
     final String code = sink.toString();
     expect(
         code,
@@ -1101,10 +1288,14 @@
       ]),
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
-    generateObjcSource(
-        const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
-        root,
-        sink);
+    const ObjcGenerator generator = ObjcGenerator();
+    final OutputFileOptions<ObjcOptions> generatorOptions =
+        OutputFileOptions<ObjcOptions>(
+      fileType: FileType.source,
+      languageOptions:
+          const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
+    );
+    generator.generate(generatorOptions, root, sink);
     final String code = sink.toString();
     expect(
         code,
@@ -1123,10 +1314,14 @@
       ])
     ], classes: <Class>[], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
-    generateObjcSource(
-        const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
-        root,
-        sink);
+    const ObjcGenerator generator = ObjcGenerator();
+    final OutputFileOptions<ObjcOptions> generatorOptions =
+        OutputFileOptions<ObjcOptions>(
+      fileType: FileType.source,
+      languageOptions:
+          const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
+    );
+    generator.generate(generatorOptions, root, sink);
     final String code = sink.toString();
     expect(
         code,
@@ -1152,10 +1347,14 @@
       ]),
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
-    generateObjcSource(
-        const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
-        root,
-        sink);
+    const ObjcGenerator generator = ObjcGenerator();
+    final OutputFileOptions<ObjcOptions> generatorOptions =
+        OutputFileOptions<ObjcOptions>(
+      fileType: FileType.source,
+      languageOptions:
+          const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
+    );
+    generator.generate(generatorOptions, root, sink);
     final String code = sink.toString();
     expect(
         code,
@@ -1170,14 +1369,16 @@
   test('source copyright', () {
     final Root root = Root(apis: <Api>[], classes: <Class>[], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
-    generateObjcSource(
-      ObjcOptions(
+    const ObjcGenerator generator = ObjcGenerator();
+    final OutputFileOptions<ObjcOptions> generatorOptions =
+        OutputFileOptions<ObjcOptions>(
+      fileType: FileType.source,
+      languageOptions: ObjcOptions(
           headerIncludePath: 'foo.h',
           prefix: 'ABC',
           copyrightHeader: makeIterable('hello world')),
-      root,
-      sink,
     );
+    generator.generate(generatorOptions, root, sink);
     final String code = sink.toString();
     expect(code, startsWith('// hello world'));
   });
@@ -1185,14 +1386,16 @@
   test('header copyright', () {
     final Root root = Root(apis: <Api>[], classes: <Class>[], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
-    generateObjcHeader(
-      ObjcOptions(
+    const ObjcGenerator generator = ObjcGenerator();
+    final OutputFileOptions<ObjcOptions> generatorOptions =
+        OutputFileOptions<ObjcOptions>(
+      fileType: FileType.header,
+      languageOptions: ObjcOptions(
           headerIncludePath: 'foo.h',
           prefix: 'ABC',
           copyrightHeader: makeIterable('hello world')),
-      root,
-      sink,
     );
+    generator.generate(generatorOptions, root, sink);
     final String code = sink.toString();
     expect(code, startsWith('// hello world'));
   });
@@ -1217,10 +1420,14 @@
       enums: <Enum>[],
     );
     final StringBuffer sink = StringBuffer();
-    generateObjcHeader(
-        const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
-        root,
-        sink);
+    const ObjcGenerator generator = ObjcGenerator();
+    final OutputFileOptions<ObjcOptions> generatorOptions =
+        OutputFileOptions<ObjcOptions>(
+      fileType: FileType.header,
+      languageOptions:
+          const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
+    );
+    generator.generate(generatorOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('NSArray<NSNumber *> * field1'));
   });
@@ -1249,19 +1456,27 @@
     );
     {
       final StringBuffer sink = StringBuffer();
-      generateObjcHeader(
-          const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
-          root,
-          sink);
+      const ObjcGenerator generator = ObjcGenerator();
+      final OutputFileOptions<ObjcOptions> generatorOptions =
+          OutputFileOptions<ObjcOptions>(
+        fileType: FileType.header,
+        languageOptions:
+            const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
+      );
+      generator.generate(generatorOptions, root, sink);
       final String code = sink.toString();
       expect(code, contains('doitArg:(NSArray<NSNumber *> *)arg'));
     }
     {
       final StringBuffer sink = StringBuffer();
-      generateObjcSource(
-          const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
-          root,
-          sink);
+      const ObjcGenerator generator = ObjcGenerator();
+      final OutputFileOptions<ObjcOptions> generatorOptions =
+          OutputFileOptions<ObjcOptions>(
+        fileType: FileType.source,
+        languageOptions:
+            const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
+      );
+      generator.generate(generatorOptions, root, sink);
       final String code = sink.toString();
       expect(
           code,
@@ -1294,19 +1509,27 @@
     );
     {
       final StringBuffer sink = StringBuffer();
-      generateObjcHeader(
-          const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
-          root,
-          sink);
+      const ObjcGenerator generator = ObjcGenerator();
+      final OutputFileOptions<ObjcOptions> generatorOptions =
+          OutputFileOptions<ObjcOptions>(
+        fileType: FileType.header,
+        languageOptions:
+            const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
+      );
+      generator.generate(generatorOptions, root, sink);
       final String code = sink.toString();
       expect(code, contains('doitArg:(NSArray<NSNumber *> *)arg'));
     }
     {
       final StringBuffer sink = StringBuffer();
-      generateObjcSource(
-          const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
-          root,
-          sink);
+      const ObjcGenerator generator = ObjcGenerator();
+      final OutputFileOptions<ObjcOptions> generatorOptions =
+          OutputFileOptions<ObjcOptions>(
+        fileType: FileType.source,
+        languageOptions:
+            const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
+      );
+      generator.generate(generatorOptions, root, sink);
       final String code = sink.toString();
       expect(code, contains('doitArg:(NSArray<NSNumber *> *)arg'));
     }
@@ -1342,10 +1565,14 @@
     );
     {
       final StringBuffer sink = StringBuffer();
-      generateObjcHeader(
-          const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
-          root,
-          sink);
+      const ObjcGenerator generator = ObjcGenerator();
+      final OutputFileOptions<ObjcOptions> generatorOptions =
+          OutputFileOptions<ObjcOptions>(
+        fileType: FileType.header,
+        languageOptions:
+            const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
+      );
+      generator.generate(generatorOptions, root, sink);
       final String code = sink.toString();
       expect(code, contains('doitArg:(NSArray<NSArray<NSNumber *> *> *)arg'));
     }
@@ -1371,20 +1598,28 @@
     );
     {
       final StringBuffer sink = StringBuffer();
-      generateObjcHeader(
-          const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
-          root,
-          sink);
+      const ObjcGenerator generator = ObjcGenerator();
+      final OutputFileOptions<ObjcOptions> generatorOptions =
+          OutputFileOptions<ObjcOptions>(
+        fileType: FileType.header,
+        languageOptions:
+            const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
+      );
+      generator.generate(generatorOptions, root, sink);
       final String code = sink.toString();
       expect(
           code, contains('- (nullable NSArray<NSNumber *> *)doitWithError:'));
     }
     {
       final StringBuffer sink = StringBuffer();
-      generateObjcSource(
-          const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
-          root,
-          sink);
+      const ObjcGenerator generator = ObjcGenerator();
+      final OutputFileOptions<ObjcOptions> generatorOptions =
+          OutputFileOptions<ObjcOptions>(
+        fileType: FileType.source,
+        languageOptions:
+            const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
+      );
+      generator.generate(generatorOptions, root, sink);
       final String code = sink.toString();
       expect(code, contains('NSArray<NSNumber *> *output ='));
     }
@@ -1410,20 +1645,28 @@
     );
     {
       final StringBuffer sink = StringBuffer();
-      generateObjcHeader(
-          const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
-          root,
-          sink);
+      const ObjcGenerator generator = ObjcGenerator();
+      final OutputFileOptions<ObjcOptions> generatorOptions =
+          OutputFileOptions<ObjcOptions>(
+        fileType: FileType.header,
+        languageOptions:
+            const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
+      );
+      generator.generate(generatorOptions, root, sink);
       final String code = sink.toString();
       expect(
           code, contains('doitWithCompletion:(void(^)(NSArray<NSNumber *> *'));
     }
     {
       final StringBuffer sink = StringBuffer();
-      generateObjcSource(
-          const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
-          root,
-          sink);
+      const ObjcGenerator generator = ObjcGenerator();
+      final OutputFileOptions<ObjcOptions> generatorOptions =
+          OutputFileOptions<ObjcOptions>(
+        fileType: FileType.source,
+        languageOptions:
+            const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
+      );
+      generator.generate(generatorOptions, root, sink);
       final String code = sink.toString();
       expect(
           code, contains('doitWithCompletion:(void(^)(NSArray<NSNumber *> *'));
@@ -1451,10 +1694,14 @@
     ], classes: <Class>[], enums: <Enum>[]);
     {
       final StringBuffer sink = StringBuffer();
-      generateObjcHeader(
-          const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
-          root,
-          sink);
+      const ObjcGenerator generator = ObjcGenerator();
+      final OutputFileOptions<ObjcOptions> generatorOptions =
+          OutputFileOptions<ObjcOptions>(
+        fileType: FileType.header,
+        languageOptions:
+            const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
+      );
+      generator.generate(generatorOptions, root, sink);
       final String code = sink.toString();
       expect(
           code,
@@ -1463,10 +1710,14 @@
     }
     {
       final StringBuffer sink = StringBuffer();
-      generateObjcSource(
-          const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
-          root,
-          sink);
+      const ObjcGenerator generator = ObjcGenerator();
+      final OutputFileOptions<ObjcOptions> generatorOptions =
+          OutputFileOptions<ObjcOptions>(
+        fileType: FileType.source,
+        languageOptions:
+            const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
+      );
+      generator.generate(generatorOptions, root, sink);
       final String code = sink.toString();
       expect(code, contains('NSArray *args = message;'));
       expect(code,
@@ -1500,10 +1751,14 @@
     ], classes: <Class>[], enums: <Enum>[]);
     {
       final StringBuffer sink = StringBuffer();
-      generateObjcHeader(
-          const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
-          root,
-          sink);
+      const ObjcGenerator generator = ObjcGenerator();
+      final OutputFileOptions<ObjcOptions> generatorOptions =
+          OutputFileOptions<ObjcOptions>(
+        fileType: FileType.header,
+        languageOptions:
+            const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
+      );
+      generator.generate(generatorOptions, root, sink);
       final String code = sink.toString();
       expect(
           code,
@@ -1512,10 +1767,14 @@
     }
     {
       final StringBuffer sink = StringBuffer();
-      generateObjcSource(
-          const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
-          root,
-          sink);
+      const ObjcGenerator generator = ObjcGenerator();
+      final OutputFileOptions<ObjcOptions> generatorOptions =
+          OutputFileOptions<ObjcOptions>(
+        fileType: FileType.source,
+        languageOptions:
+            const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
+      );
+      generator.generate(generatorOptions, root, sink);
       final String code = sink.toString();
       expect(code, contains('NSArray *args = message;'));
       expect(code,
@@ -1547,10 +1806,14 @@
     ], classes: <Class>[], enums: <Enum>[]);
     {
       final StringBuffer sink = StringBuffer();
-      generateObjcHeader(
-          const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
-          root,
-          sink);
+      const ObjcGenerator generator = ObjcGenerator();
+      final OutputFileOptions<ObjcOptions> generatorOptions =
+          OutputFileOptions<ObjcOptions>(
+        fileType: FileType.header,
+        languageOptions:
+            const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
+      );
+      generator.generate(generatorOptions, root, sink);
       final String code = sink.toString();
       expect(
           code,
@@ -1559,10 +1822,14 @@
     }
     {
       final StringBuffer sink = StringBuffer();
-      generateObjcSource(
-          const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
-          root,
-          sink);
+      const ObjcGenerator generator = ObjcGenerator();
+      final OutputFileOptions<ObjcOptions> generatorOptions =
+          OutputFileOptions<ObjcOptions>(
+        fileType: FileType.source,
+        languageOptions:
+            const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
+      );
+      generator.generate(generatorOptions, root, sink);
       final String code = sink.toString();
       expect(
           code,
@@ -1605,19 +1872,27 @@
     final Root divideRoot = getDivideRoot(ApiLocation.host);
     {
       final StringBuffer sink = StringBuffer();
-      generateObjcHeader(
-          const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
-          divideRoot,
-          sink);
+      const ObjcGenerator generator = ObjcGenerator();
+      final OutputFileOptions<ObjcOptions> generatorOptions =
+          OutputFileOptions<ObjcOptions>(
+        fileType: FileType.header,
+        languageOptions:
+            const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
+      );
+      generator.generate(generatorOptions, divideRoot, sink);
       final String code = sink.toString();
       expect(code, matches('divideValue:.*by:.*error.*;'));
     }
     {
       final StringBuffer sink = StringBuffer();
-      generateObjcSource(
-          const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
-          divideRoot,
-          sink);
+      const ObjcGenerator generator = ObjcGenerator();
+      final OutputFileOptions<ObjcOptions> generatorOptions =
+          OutputFileOptions<ObjcOptions>(
+        fileType: FileType.source,
+        languageOptions:
+            const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
+      );
+      generator.generate(generatorOptions, divideRoot, sink);
       final String code = sink.toString();
       expect(code, matches('divideValue:.*by:.*error.*;'));
     }
@@ -1627,21 +1902,27 @@
     final Root divideRoot = getDivideRoot(ApiLocation.flutter);
     {
       final StringBuffer sink = StringBuffer();
-      generateObjcHeader(
-        const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
-        divideRoot,
-        sink,
+      const ObjcGenerator generator = ObjcGenerator();
+      final OutputFileOptions<ObjcOptions> generatorOptions =
+          OutputFileOptions<ObjcOptions>(
+        fileType: FileType.header,
+        languageOptions:
+            const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
       );
+      generator.generate(generatorOptions, divideRoot, sink);
       final String code = sink.toString();
       expect(code, matches('divideValue:.*by:.*completion.*;'));
     }
     {
       final StringBuffer sink = StringBuffer();
-      generateObjcSource(
-        const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
-        divideRoot,
-        sink,
+      const ObjcGenerator generator = ObjcGenerator();
+      final OutputFileOptions<ObjcOptions> generatorOptions =
+          OutputFileOptions<ObjcOptions>(
+        fileType: FileType.source,
+        languageOptions:
+            const ObjcOptions(headerIncludePath: 'foo.h', prefix: 'ABC'),
       );
+      generator.generate(generatorOptions, divideRoot, sink);
       final String code = sink.toString();
       expect(code, matches('divideValue:.*by:.*completion.*{'));
     }
@@ -1656,7 +1937,13 @@
       ]),
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
-    generateObjcHeader(const ObjcOptions(), root, sink);
+    const ObjcGenerator generator = ObjcGenerator();
+    final OutputFileOptions<ObjcOptions> generatorOptions =
+        OutputFileOptions<ObjcOptions>(
+      fileType: FileType.header,
+      languageOptions: const ObjcOptions(),
+    );
+    generator.generate(generatorOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('@interface Foobar'));
     expect(code, contains('@property(nonatomic, copy) NSString * field1'));
@@ -1679,7 +1966,13 @@
       enums: <Enum>[],
     );
     final StringBuffer sink = StringBuffer();
-    generateObjcHeader(const ObjcOptions(), root, sink);
+    const ObjcGenerator generator = ObjcGenerator();
+    final OutputFileOptions<ObjcOptions> generatorOptions =
+        OutputFileOptions<ObjcOptions>(
+      fileType: FileType.header,
+      languageOptions: const ObjcOptions(),
+    );
+    generator.generate(generatorOptions, root, sink);
     final String code = sink.toString();
     expect(
         code,
@@ -1704,7 +1997,13 @@
       enums: <Enum>[],
     );
     final StringBuffer sink = StringBuffer();
-    generateObjcSource(const ObjcOptions(), root, sink);
+    const ObjcGenerator generator = ObjcGenerator();
+    final OutputFileOptions<ObjcOptions> generatorOptions =
+        OutputFileOptions<ObjcOptions>(
+      fileType: FileType.source,
+      languageOptions: const ObjcOptions(),
+    );
+    generator.generate(generatorOptions, root, sink);
     final String code = sink.toString();
     expect(code, matches(r'doitWithCompletion.*NSNumber \*_Nullable'));
   });
@@ -1726,7 +2025,13 @@
       enums: <Enum>[],
     );
     final StringBuffer sink = StringBuffer();
-    generateObjcHeader(const ObjcOptions(), root, sink);
+    const ObjcGenerator generator = ObjcGenerator();
+    final OutputFileOptions<ObjcOptions> generatorOptions =
+        OutputFileOptions<ObjcOptions>(
+      fileType: FileType.header,
+      languageOptions: const ObjcOptions(),
+    );
+    generator.generate(generatorOptions, root, sink);
     final String code = sink.toString();
     expect(code, matches(r'nullable NSNumber.*doitWithError'));
   });
@@ -1753,13 +2058,25 @@
     );
     {
       final StringBuffer sink = StringBuffer();
-      generateObjcHeader(const ObjcOptions(), root, sink);
+      const ObjcGenerator generator = ObjcGenerator();
+      final OutputFileOptions<ObjcOptions> generatorOptions =
+          OutputFileOptions<ObjcOptions>(
+        fileType: FileType.header,
+        languageOptions: const ObjcOptions(),
+      );
+      generator.generate(generatorOptions, root, sink);
       final String code = sink.toString();
       expect(code, contains('doitFoo:(nullable NSNumber *)foo'));
     }
     {
       final StringBuffer sink = StringBuffer();
-      generateObjcSource(const ObjcOptions(), root, sink);
+      const ObjcGenerator generator = ObjcGenerator();
+      final OutputFileOptions<ObjcOptions> generatorOptions =
+          OutputFileOptions<ObjcOptions>(
+        fileType: FileType.source,
+        languageOptions: const ObjcOptions(),
+      );
+      generator.generate(generatorOptions, root, sink);
       final String code = sink.toString();
       expect(code,
           contains('NSNumber *arg_foo = GetNullableObjectAtIndex(args, 0);'));
@@ -1788,13 +2105,25 @@
     );
     {
       final StringBuffer sink = StringBuffer();
-      generateObjcHeader(const ObjcOptions(), root, sink);
+      const ObjcGenerator generator = ObjcGenerator();
+      final OutputFileOptions<ObjcOptions> generatorOptions =
+          OutputFileOptions<ObjcOptions>(
+        fileType: FileType.header,
+        languageOptions: const ObjcOptions(),
+      );
+      generator.generate(generatorOptions, root, sink);
       final String code = sink.toString();
       expect(code, contains('doitFoo:(nullable NSNumber *)foo'));
     }
     {
       final StringBuffer sink = StringBuffer();
-      generateObjcSource(const ObjcOptions(), root, sink);
+      const ObjcGenerator generator = ObjcGenerator();
+      final OutputFileOptions<ObjcOptions> generatorOptions =
+          OutputFileOptions<ObjcOptions>(
+        fileType: FileType.source,
+        languageOptions: const ObjcOptions(),
+      );
+      generator.generate(generatorOptions, root, sink);
       final String code = sink.toString();
       expect(code, contains('- (void)doitFoo:(nullable NSNumber *)arg_foo'));
     }
@@ -1818,7 +2147,13 @@
       enums: <Enum>[],
     );
     final StringBuffer sink = StringBuffer();
-    generateObjcSource(const ObjcOptions(), root, sink);
+    const ObjcGenerator generator = ObjcGenerator();
+    final OutputFileOptions<ObjcOptions> generatorOptions =
+        OutputFileOptions<ObjcOptions>(
+      fileType: FileType.source,
+      languageOptions: const ObjcOptions(),
+    );
+    generator.generate(generatorOptions, root, sink);
     final String code = sink.toString();
     expect(
         code,
@@ -1902,7 +2237,13 @@
       ],
     );
     final StringBuffer sink = StringBuffer();
-    generateObjcHeader(const ObjcOptions(), root, sink);
+    const ObjcGenerator generator = ObjcGenerator();
+    final OutputFileOptions<ObjcOptions> generatorOptions =
+        OutputFileOptions<ObjcOptions>(
+      fileType: FileType.header,
+      languageOptions: const ObjcOptions(),
+    );
+    generator.generate(generatorOptions, root, sink);
     final String code = sink.toString();
     for (final String comment in comments) {
       expect(code, contains('///$comment'));
@@ -1937,7 +2278,13 @@
       enums: <Enum>[],
     );
     final StringBuffer sink = StringBuffer();
-    generateObjcSource(const ObjcOptions(), root, sink);
+    const ObjcGenerator generator = ObjcGenerator();
+    final OutputFileOptions<ObjcOptions> generatorOptions =
+        OutputFileOptions<ObjcOptions>(
+      fileType: FileType.source,
+      languageOptions: const ObjcOptions(),
+    );
+    generator.generate(generatorOptions, root, sink);
     final String code = sink.toString();
     expect(code, isNot(contains(' : FlutterStandardReader')));
   });
@@ -1979,7 +2326,13 @@
       ])
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
-    generateObjcSource(const ObjcOptions(), root, sink);
+    const ObjcGenerator generator = ObjcGenerator();
+    final OutputFileOptions<ObjcOptions> generatorOptions =
+        OutputFileOptions<ObjcOptions>(
+      fileType: FileType.source,
+      languageOptions: const ObjcOptions(),
+    );
+    generator.generate(generatorOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains(' : FlutterStandardReader'));
   });
diff --git a/packages/pigeon/test/pigeon_lib_test.dart b/packages/pigeon/test/pigeon_lib_test.dart
index 64c4af7..3d5c706 100644
--- a/packages/pigeon/test/pigeon_lib_test.dart
+++ b/packages/pigeon/test/pigeon_lib_test.dart
@@ -938,10 +938,10 @@
       dartTestOut: 'stdout',
       dartOut: 'stdout',
     );
-    final DartTestGeneratorAdapter dartGeneratorAdapter =
+    final DartTestGeneratorAdapter dartTestGeneratorAdapter =
         DartTestGeneratorAdapter();
     final StringBuffer buffer = StringBuffer();
-    dartGeneratorAdapter.generate(buffer, options, root, FileType.source);
+    dartTestGeneratorAdapter.generate(buffer, options, root, FileType.source);
     expect(buffer.toString(), startsWith('// Copyright 2013'));
   });
 
diff --git a/packages/pigeon/test/swift_generator_test.dart b/packages/pigeon/test/swift_generator_test.dart
index 74c8aad..dfd2dfb 100644
--- a/packages/pigeon/test/swift_generator_test.dart
+++ b/packages/pigeon/test/swift_generator_test.dart
@@ -26,7 +26,8 @@
     );
     final StringBuffer sink = StringBuffer();
     const SwiftOptions swiftOptions = SwiftOptions();
-    generateSwift(swiftOptions, root, sink);
+    const SwiftGenerator generator = SwiftGenerator();
+    generator.generate(swiftOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('struct Foobar'));
     expect(code, contains('var field1: Int32? = nil'));
@@ -49,7 +50,8 @@
     );
     final StringBuffer sink = StringBuffer();
     const SwiftOptions swiftOptions = SwiftOptions();
-    generateSwift(swiftOptions, root, sink);
+    const SwiftGenerator generator = SwiftGenerator();
+    generator.generate(swiftOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('enum Foobar: Int'));
     expect(code, contains('  case one = 0'));
@@ -77,7 +79,8 @@
     ]);
     final StringBuffer sink = StringBuffer();
     const SwiftOptions swiftOptions = SwiftOptions();
-    generateSwift(swiftOptions, root, sink);
+    const SwiftGenerator generator = SwiftGenerator();
+    generator.generate(swiftOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('enum Foo: Int'));
     expect(code, contains('let fooArg = Foo(rawValue: args[0] as! Int)!'));
@@ -120,7 +123,8 @@
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
     const SwiftOptions swiftOptions = SwiftOptions();
-    generateSwift(swiftOptions, root, sink);
+    const SwiftGenerator generator = SwiftGenerator();
+    generator.generate(swiftOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('protocol Api'));
     expect(code, matches('func doSomething.*Input.*Output'));
@@ -183,7 +187,8 @@
 
     final StringBuffer sink = StringBuffer();
     const SwiftOptions swiftOptions = SwiftOptions();
-    generateSwift(swiftOptions, root, sink);
+    const SwiftGenerator generator = SwiftGenerator();
+    generator.generate(swiftOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('var aBool: Bool? = nil'));
     expect(code, contains('var aInt: Int32? = nil'));
@@ -232,7 +237,8 @@
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
     const SwiftOptions swiftOptions = SwiftOptions();
-    generateSwift(swiftOptions, root, sink);
+    const SwiftGenerator generator = SwiftGenerator();
+    generator.generate(swiftOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('class Api'));
     expect(code, contains('init(binaryMessenger: FlutterBinaryMessenger)'));
@@ -267,7 +273,8 @@
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
     const SwiftOptions swiftOptions = SwiftOptions();
-    generateSwift(swiftOptions, root, sink);
+    const SwiftGenerator generator = SwiftGenerator();
+    generator.generate(swiftOptions, root, sink);
     final String code = sink.toString();
     expect(code, isNot(matches('.*doSomething(.*) ->')));
     expect(code, matches('doSomething(.*)'));
@@ -301,7 +308,8 @@
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
     const SwiftOptions swiftOptions = SwiftOptions();
-    generateSwift(swiftOptions, root, sink);
+    const SwiftGenerator generator = SwiftGenerator();
+    generator.generate(swiftOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('completion: @escaping () -> Void'));
     expect(code, contains('completion()'));
@@ -329,7 +337,8 @@
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
     const SwiftOptions swiftOptions = SwiftOptions();
-    generateSwift(swiftOptions, root, sink);
+    const SwiftGenerator generator = SwiftGenerator();
+    generator.generate(swiftOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('func doSomething() -> Output'));
     expect(code, contains('let result = api.doSomething()'));
@@ -358,7 +367,8 @@
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
     const SwiftOptions swiftOptions = SwiftOptions();
-    generateSwift(swiftOptions, root, sink);
+    const SwiftGenerator generator = SwiftGenerator();
+    generator.generate(swiftOptions, root, sink);
     final String code = sink.toString();
     expect(code,
         contains('func doSomething(completion: @escaping (Output) -> Void)'));
@@ -378,7 +388,8 @@
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
     const SwiftOptions swiftOptions = SwiftOptions();
-    generateSwift(swiftOptions, root, sink);
+    const SwiftGenerator generator = SwiftGenerator();
+    generator.generate(swiftOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('struct Foobar'));
     expect(code, contains('var field1: [Any?]? = nil'));
@@ -397,7 +408,8 @@
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
     const SwiftOptions swiftOptions = SwiftOptions();
-    generateSwift(swiftOptions, root, sink);
+    const SwiftGenerator generator = SwiftGenerator();
+    generator.generate(swiftOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('struct Foobar'));
     expect(code, contains('var field1: [AnyHashable: Any?]? = nil'));
@@ -433,7 +445,8 @@
     );
     final StringBuffer sink = StringBuffer();
     const SwiftOptions swiftOptions = SwiftOptions();
-    generateSwift(swiftOptions, root, sink);
+    const SwiftGenerator generator = SwiftGenerator();
+    generator.generate(swiftOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('struct Outer'));
     expect(code, contains('struct Nested'));
@@ -481,7 +494,8 @@
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
     const SwiftOptions swiftOptions = SwiftOptions();
-    generateSwift(swiftOptions, root, sink);
+    const SwiftGenerator generator = SwiftGenerator();
+    generator.generate(swiftOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('protocol Api'));
     expect(code, contains('api.doSomething(arg: argArg) { result in'));
@@ -526,7 +540,8 @@
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
     const SwiftOptions swiftOptions = SwiftOptions();
-    generateSwift(swiftOptions, root, sink);
+    const SwiftGenerator generator = SwiftGenerator();
+    generator.generate(swiftOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('class Api'));
     expect(code, matches('func doSomething.*Input.*completion.*Output.*Void'));
@@ -558,7 +573,8 @@
     );
     final StringBuffer sink = StringBuffer();
     const SwiftOptions swiftOptions = SwiftOptions();
-    generateSwift(swiftOptions, root, sink);
+    const SwiftGenerator generator = SwiftGenerator();
+    generator.generate(swiftOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('enum Enum1: Int'));
     expect(code, contains('case one = 0'));
@@ -575,7 +591,8 @@
     final SwiftOptions swiftOptions = SwiftOptions(
       copyrightHeader: makeIterable('hello world'),
     );
-    generateSwift(swiftOptions, root, sink);
+    const SwiftGenerator generator = SwiftGenerator();
+    generator.generate(swiftOptions, root, sink);
     final String code = sink.toString();
     expect(code, startsWith('// hello world'));
   });
@@ -601,7 +618,8 @@
     );
     final StringBuffer sink = StringBuffer();
     const SwiftOptions swiftOptions = SwiftOptions();
-    generateSwift(swiftOptions, root, sink);
+    const SwiftGenerator generator = SwiftGenerator();
+    generator.generate(swiftOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('struct Foobar'));
     expect(code, contains('var field1: [Int32?]'));
@@ -629,7 +647,8 @@
     );
     final StringBuffer sink = StringBuffer();
     const SwiftOptions swiftOptions = SwiftOptions();
-    generateSwift(swiftOptions, root, sink);
+    const SwiftGenerator generator = SwiftGenerator();
+    generator.generate(swiftOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('struct Foobar'));
     expect(code, contains('var field1: [String?: String?]'));
@@ -659,7 +678,8 @@
     );
     final StringBuffer sink = StringBuffer();
     const SwiftOptions swiftOptions = SwiftOptions();
-    generateSwift(swiftOptions, root, sink);
+    const SwiftGenerator generator = SwiftGenerator();
+    generator.generate(swiftOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('func doit(arg: [Int32?]'));
   });
@@ -688,7 +708,8 @@
     );
     final StringBuffer sink = StringBuffer();
     const SwiftOptions swiftOptions = SwiftOptions();
-    generateSwift(swiftOptions, root, sink);
+    const SwiftGenerator generator = SwiftGenerator();
+    generator.generate(swiftOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('func doit(arg argArg: [Int32?]'));
   });
@@ -713,7 +734,8 @@
     );
     final StringBuffer sink = StringBuffer();
     const SwiftOptions swiftOptions = SwiftOptions();
-    generateSwift(swiftOptions, root, sink);
+    const SwiftGenerator generator = SwiftGenerator();
+    generator.generate(swiftOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('func doit() -> [Int32?]'));
     expect(code, contains('let result = api.doit()'));
@@ -740,7 +762,8 @@
     );
     final StringBuffer sink = StringBuffer();
     const SwiftOptions swiftOptions = SwiftOptions();
-    generateSwift(swiftOptions, root, sink);
+    const SwiftGenerator generator = SwiftGenerator();
+    generator.generate(swiftOptions, root, sink);
     final String code = sink.toString();
     expect(
         code, contains('func doit(completion: @escaping ([Int32?]) -> Void'));
@@ -769,7 +792,8 @@
     ], classes: <Class>[], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
     const SwiftOptions swiftOptions = SwiftOptions();
-    generateSwift(swiftOptions, root, sink);
+    const SwiftGenerator generator = SwiftGenerator();
+    generator.generate(swiftOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('func add(x: Int32, y: Int32) -> Int32'));
     expect(code, contains('let args = message as! [Any?]'));
@@ -800,7 +824,8 @@
     ], classes: <Class>[], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
     const SwiftOptions swiftOptions = SwiftOptions();
-    generateSwift(swiftOptions, root, sink);
+    const SwiftGenerator generator = SwiftGenerator();
+    generator.generate(swiftOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('let channel = FlutterBasicMessageChannel'));
     expect(code, contains('let result = response as! Int32'));
@@ -830,7 +855,8 @@
     );
     final StringBuffer sink = StringBuffer();
     const SwiftOptions swiftOptions = SwiftOptions();
-    generateSwift(swiftOptions, root, sink);
+    const SwiftGenerator generator = SwiftGenerator();
+    generator.generate(swiftOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('func doit() -> Int32?'));
   });
@@ -854,7 +880,8 @@
     );
     final StringBuffer sink = StringBuffer();
     const SwiftOptions swiftOptions = SwiftOptions();
-    generateSwift(swiftOptions, root, sink);
+    const SwiftGenerator generator = SwiftGenerator();
+    generator.generate(swiftOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('func doit(completion: @escaping (Int32?) -> Void'));
   });
@@ -881,7 +908,8 @@
     );
     final StringBuffer sink = StringBuffer();
     const SwiftOptions swiftOptions = SwiftOptions();
-    generateSwift(swiftOptions, root, sink);
+    const SwiftGenerator generator = SwiftGenerator();
+    generator.generate(swiftOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('let fooArg = args[0] as? Int32'));
   });
@@ -908,7 +936,8 @@
     );
     final StringBuffer sink = StringBuffer();
     const SwiftOptions swiftOptions = SwiftOptions();
-    generateSwift(swiftOptions, root, sink);
+    const SwiftGenerator generator = SwiftGenerator();
+    generator.generate(swiftOptions, root, sink);
     final String code = sink.toString();
     expect(
         code,
@@ -944,7 +973,8 @@
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
     const SwiftOptions swiftOptions = SwiftOptions();
-    generateSwift(swiftOptions, root, sink);
+    const SwiftGenerator generator = SwiftGenerator();
+    generator.generate(swiftOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains('var input: String\n'));
   });
@@ -1025,7 +1055,8 @@
     );
     final StringBuffer sink = StringBuffer();
     const SwiftOptions swiftOptions = SwiftOptions();
-    generateSwift(swiftOptions, root, sink);
+    const SwiftGenerator generator = SwiftGenerator();
+    generator.generate(swiftOptions, root, sink);
     final String code = sink.toString();
     for (final String comment in comments) {
       expect(code, contains('///$comment'));
@@ -1061,7 +1092,8 @@
     );
     final StringBuffer sink = StringBuffer();
     const SwiftOptions swiftOptions = SwiftOptions();
-    generateSwift(swiftOptions, root, sink);
+    const SwiftGenerator generator = SwiftGenerator();
+    generator.generate(swiftOptions, root, sink);
     final String code = sink.toString();
     expect(code, isNot(contains(': FlutterStandardReader ')));
   });
@@ -1104,7 +1136,8 @@
     ], enums: <Enum>[]);
     final StringBuffer sink = StringBuffer();
     const SwiftOptions swiftOptions = SwiftOptions();
-    generateSwift(swiftOptions, root, sink);
+    const SwiftGenerator generator = SwiftGenerator();
+    generator.generate(swiftOptions, root, sink);
     final String code = sink.toString();
     expect(code, contains(': FlutterStandardReader '));
   });