[pigeon] kotlin equality methods (#8887)

Adds `equals` and `hash` methods to Kotlin classes.

More classes may be added to this pr if reviews are late enough.

work towards https://github.com/flutter/flutter/issues/118087
diff --git a/packages/pigeon/CHANGELOG.md b/packages/pigeon/CHANGELOG.md
index c0b1841..da10f9f 100644
--- a/packages/pigeon/CHANGELOG.md
+++ b/packages/pigeon/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 25.2.0
+
+* [kotlin] Adds equality methods to generated data classes.
+
 ## 25.1.0
 
 * [dart] Adds equality methods to generated data classes.
diff --git a/packages/pigeon/example/app/android/app/src/main/kotlin/dev/flutter/pigeon_example_app/EventChannelMessages.g.kt b/packages/pigeon/example/app/android/app/src/main/kotlin/dev/flutter/pigeon_example_app/EventChannelMessages.g.kt
index 0c0512d..519c9e2 100644
--- a/packages/pigeon/example/app/android/app/src/main/kotlin/dev/flutter/pigeon_example_app/EventChannelMessages.g.kt
+++ b/packages/pigeon/example/app/android/app/src/main/kotlin/dev/flutter/pigeon_example_app/EventChannelMessages.g.kt
@@ -31,6 +31,18 @@
         data,
     )
   }
+
+  override fun equals(other: Any?): Boolean {
+    if (other !is IntEvent) {
+      return false
+    }
+    if (this === other) {
+      return true
+    }
+    return data == other.data
+  }
+
+  override fun hashCode(): Int = toList().hashCode()
 }
 
 /** Generated class from Pigeon that represents data sent in messages. */
@@ -47,6 +59,18 @@
         data,
     )
   }
+
+  override fun equals(other: Any?): Boolean {
+    if (other !is StringEvent) {
+      return false
+    }
+    if (this === other) {
+      return true
+    }
+    return data == other.data
+  }
+
+  override fun hashCode(): Int = toList().hashCode()
 }
 
 private open class EventChannelMessagesPigeonCodec : StandardMessageCodec() {
diff --git a/packages/pigeon/example/app/android/app/src/main/kotlin/dev/flutter/pigeon_example_app/Messages.g.kt b/packages/pigeon/example/app/android/app/src/main/kotlin/dev/flutter/pigeon_example_app/Messages.g.kt
index c6b3271..279f731 100644
--- a/packages/pigeon/example/app/android/app/src/main/kotlin/dev/flutter/pigeon_example_app/Messages.g.kt
+++ b/packages/pigeon/example/app/android/app/src/main/kotlin/dev/flutter/pigeon_example_app/Messages.g.kt
@@ -46,6 +46,29 @@
     val details: Any? = null
 ) : Throwable()
 
+private fun deepEqualsMessages(a: Any?, b: Any?): Boolean {
+  if (a is ByteArray && b is ByteArray) {
+    return a.contentEquals(b)
+  }
+  if (a is IntArray && b is IntArray) {
+    return a.contentEquals(b)
+  }
+  if (a is LongArray && b is LongArray) {
+    return a.contentEquals(b)
+  }
+  if (a is DoubleArray && b is DoubleArray) {
+    return a.contentEquals(b)
+  }
+  if (a is Array<*> && b is Array<*>) {
+    return a.size == b.size && a.indices.all { deepEqualsMessages(a[it], b[it]) }
+  }
+  if (a is Map<*, *> && b is Map<*, *>) {
+    return a.size == b.size &&
+        a.keys.all { (b as Map<Any?, Any?>).containsKey(it) && deepEqualsMessages(a[it], b[it]) }
+  }
+  return a == b
+}
+
 enum class Code(val raw: Int) {
   ONE(0),
   TWO(1);
@@ -82,6 +105,21 @@
         data,
     )
   }
+
+  override fun equals(other: Any?): Boolean {
+    if (other !is MessageData) {
+      return false
+    }
+    if (this === other) {
+      return true
+    }
+    return name == other.name &&
+        description == other.description &&
+        code == other.code &&
+        deepEqualsMessages(data, other.data)
+  }
+
+  override fun hashCode(): Int = toList().hashCode()
 }
 
 private open class MessagesPigeonCodec : StandardMessageCodec() {
diff --git a/packages/pigeon/lib/src/cpp/cpp_generator.dart b/packages/pigeon/lib/src/cpp/cpp_generator.dart
index 3db417a..cad25aa 100644
--- a/packages/pigeon/lib/src/cpp/cpp_generator.dart
+++ b/packages/pigeon/lib/src/cpp/cpp_generator.dart
@@ -98,7 +98,7 @@
 /// Options that control how C++ code will be generated.
 ///
 /// For internal use only.
-class InternalCppOptions {
+class InternalCppOptions extends PigeonInternalOptions {
   /// Creates a [InternalCppOptions] object.
   const InternalCppOptions({
     required this.headerIncludePath,
diff --git a/packages/pigeon/lib/src/dart/dart_generator.dart b/packages/pigeon/lib/src/dart/dart_generator.dart
index 1ea9b65..b8897e4 100644
--- a/packages/pigeon/lib/src/dart/dart_generator.dart
+++ b/packages/pigeon/lib/src/dart/dart_generator.dart
@@ -87,7 +87,7 @@
 }
 
 /// Options that control how Dart code will be generated.
-class InternalDartOptions {
+class InternalDartOptions extends PigeonInternalOptions {
   /// Constructor for InternalDartOptions.
   const InternalDartOptions({
     this.copyrightHeader,
@@ -354,12 +354,7 @@
       indent.writeScoped('return ', '', () {
         indent.format(
             classDefinition.fields
-                .map((NamedType field) => field.type.baseName == 'List' ||
-                        field.type.baseName == 'Float64List' ||
-                        field.type.baseName == 'Int32List' ||
-                        field.type.baseName == 'Int64List' ||
-                        field.type.baseName == 'Uint8List' ||
-                        field.type.baseName == 'Map'
+                .map((NamedType field) => isCollectionType(field.type)
                     ? '_deepEquals(${field.name}, other.${field.name})'
                     : '${field.name} == other.${field.name}')
                 .join('\n&& '),
@@ -1089,10 +1084,8 @@
       _writeWrapResponse(generatorOptions, root, indent);
     }
     if (root.classes.isNotEmpty &&
-        root.classes.any((Class dataClass) => dataClass.fields.any(
-            (NamedType field) =>
-                field.type.baseName.startsWith('List') ||
-                field.type.baseName.startsWith('Map')))) {
+        root.classes.any((Class dataClass) => dataClass.fields
+            .any((NamedType field) => isCollectionType(field.type)))) {
       _writeDeepEquals(indent);
     }
   }
diff --git a/packages/pigeon/lib/src/generator.dart b/packages/pigeon/lib/src/generator.dart
index 66961c4..fc0b8a1 100644
--- a/packages/pigeon/lib/src/generator.dart
+++ b/packages/pigeon/lib/src/generator.dart
@@ -6,18 +6,21 @@
 import 'generator_tools.dart';
 
 /// The internal options used by the generator.
-abstract class InternalOptions {}
+abstract class PigeonInternalOptions {
+  /// Constructor.
+  const PigeonInternalOptions();
+}
 
 /// An abstract base class of generators.
 ///
 /// This provides the structure that is common across generators for different languages.
-abstract class Generator<InternalOptions> {
+abstract class Generator<PigeonInternalOptions> {
   /// Constructor.
   const Generator();
 
   /// Generates files for specified language with specified [generatorOptions]
   void generate(
-    InternalOptions generatorOptions,
+    PigeonInternalOptions generatorOptions,
     Root root,
     StringSink sink, {
     required String dartPackageName,
@@ -25,14 +28,14 @@
 }
 
 /// An abstract base class that enforces code generation across platforms.
-abstract class StructuredGenerator<InternalOptions>
-    extends Generator<InternalOptions> {
+abstract class StructuredGenerator<PigeonInternalOptions>
+    extends Generator<PigeonInternalOptions> {
   /// Constructor.
   const StructuredGenerator();
 
   @override
   void generate(
-    InternalOptions generatorOptions,
+    PigeonInternalOptions generatorOptions,
     Root root,
     StringSink sink, {
     required String dartPackageName,
@@ -123,7 +126,7 @@
 
   /// Adds specified headers to [indent].
   void writeFilePrologue(
-    InternalOptions generatorOptions,
+    PigeonInternalOptions generatorOptions,
     Root root,
     Indent indent, {
     required String dartPackageName,
@@ -131,7 +134,7 @@
 
   /// Writes specified imports to [indent].
   void writeFileImports(
-    InternalOptions generatorOptions,
+    PigeonInternalOptions generatorOptions,
     Root root,
     Indent indent, {
     required String dartPackageName,
@@ -141,7 +144,7 @@
   ///
   /// This method is not required, and does not need to be overridden.
   void writeOpenNamespace(
-    InternalOptions generatorOptions,
+    PigeonInternalOptions generatorOptions,
     Root root,
     Indent indent, {
     required String dartPackageName,
@@ -151,7 +154,7 @@
   ///
   /// This method is not required, and does not need to be overridden.
   void writeCloseNamespace(
-    InternalOptions generatorOptions,
+    PigeonInternalOptions generatorOptions,
     Root root,
     Indent indent, {
     required String dartPackageName,
@@ -161,7 +164,7 @@
   ///
   /// This method is not required, and does not need to be overridden.
   void writeGeneralUtilities(
-    InternalOptions generatorOptions,
+    PigeonInternalOptions generatorOptions,
     Root root,
     Indent indent, {
     required String dartPackageName,
@@ -171,7 +174,7 @@
   ///
   /// Can be overridden to add extra code before/after enums.
   void writeEnums(
-    InternalOptions generatorOptions,
+    PigeonInternalOptions generatorOptions,
     Root root,
     Indent indent, {
     required String dartPackageName,
@@ -189,7 +192,7 @@
 
   /// Writes a single Enum to [indent]. This is needed in most generators.
   void writeEnum(
-    InternalOptions generatorOptions,
+    PigeonInternalOptions generatorOptions,
     Root root,
     Indent indent,
     Enum anEnum, {
@@ -200,7 +203,7 @@
   ///
   /// Can be overridden to add extra code before/after apis.
   void writeDataClasses(
-    InternalOptions generatorOptions,
+    PigeonInternalOptions generatorOptions,
     Root root,
     Indent indent, {
     required String dartPackageName,
@@ -218,7 +221,7 @@
 
   /// Writes the custom codec to [indent].
   void writeGeneralCodec(
-    InternalOptions generatorOptions,
+    PigeonInternalOptions generatorOptions,
     Root root,
     Indent indent, {
     required String dartPackageName,
@@ -226,7 +229,7 @@
 
   /// Writes a single data class to [indent].
   void writeDataClass(
-    InternalOptions generatorOptions,
+    PigeonInternalOptions generatorOptions,
     Root root,
     Indent indent,
     Class classDefinition, {
@@ -235,7 +238,7 @@
 
   /// Writes a single class encode method to [indent].
   void writeClassEncode(
-    InternalOptions generatorOptions,
+    PigeonInternalOptions generatorOptions,
     Root root,
     Indent indent,
     Class classDefinition, {
@@ -244,7 +247,7 @@
 
   /// Writes a single class decode method to [indent].
   void writeClassDecode(
-    InternalOptions generatorOptions,
+    PigeonInternalOptions generatorOptions,
     Root root,
     Indent indent,
     Class classDefinition, {
@@ -253,7 +256,7 @@
 
   /// Writes a single class decode method to [indent].
   void writeClassEquality(
-    InternalOptions generatorOptions,
+    PigeonInternalOptions generatorOptions,
     Root root,
     Indent indent,
     Class classDefinition, {
@@ -264,7 +267,7 @@
   ///
   /// Can be overridden to add extra code before/after classes.
   void writeApis(
-    InternalOptions generatorOptions,
+    PigeonInternalOptions generatorOptions,
     Root root,
     Indent indent, {
     required String dartPackageName,
@@ -309,7 +312,7 @@
 
   /// Writes a single Flutter Api to [indent].
   void writeFlutterApi(
-    InternalOptions generatorOptions,
+    PigeonInternalOptions generatorOptions,
     Root root,
     Indent indent,
     AstFlutterApi api, {
@@ -318,7 +321,7 @@
 
   /// Writes a single Host Api to [indent].
   void writeHostApi(
-    InternalOptions generatorOptions,
+    PigeonInternalOptions generatorOptions,
     Root root,
     Indent indent,
     AstHostApi api, {
@@ -327,7 +330,7 @@
 
   /// Writes the implementation of an `InstanceManager` to [indent].
   void writeInstanceManager(
-    InternalOptions generatorOptions,
+    PigeonInternalOptions generatorOptions,
     Root root,
     Indent indent, {
     required String dartPackageName,
@@ -336,7 +339,7 @@
   /// Writes the implementation of the API for the `InstanceManager` to
   /// [indent].
   void writeInstanceManagerApi(
-    InternalOptions generatorOptions,
+    PigeonInternalOptions generatorOptions,
     Root root,
     Indent indent, {
     required String dartPackageName,
@@ -353,14 +356,14 @@
   /// needs to create its own codec (it has methods/fields/constructor that use
   /// a data class) it should extend this codec and not `StandardMessageCodec`.
   void writeProxyApiBaseCodec(
-    InternalOptions generatorOptions,
+    PigeonInternalOptions generatorOptions,
     Root root,
     Indent indent,
   ) {}
 
   /// Writes a single Proxy Api to [indent].
   void writeProxyApi(
-    InternalOptions generatorOptions,
+    PigeonInternalOptions generatorOptions,
     Root root,
     Indent indent,
     AstProxyApi api, {
@@ -369,7 +372,7 @@
 
   /// Writes a single event channel Api to [indent].
   void writeEventChannelApi(
-    InternalOptions generatorOptions,
+    PigeonInternalOptions generatorOptions,
     Root root,
     Indent indent,
     AstEventChannelApi api, {
diff --git a/packages/pigeon/lib/src/generator_tools.dart b/packages/pigeon/lib/src/generator_tools.dart
index dd1b3d7..58cc1ab 100644
--- a/packages/pigeon/lib/src/generator_tools.dart
+++ b/packages/pigeon/lib/src/generator_tools.dart
@@ -14,7 +14,7 @@
 /// The current version of pigeon.
 ///
 /// This must match the version in pubspec.yaml.
-const String pigeonVersion = '25.1.0';
+const String pigeonVersion = '25.2.0';
 
 /// Read all the content from [stdin] to a String.
 String readStdin() {
@@ -861,3 +861,11 @@
     dartPackageName: dartPackageName,
   );
 }
+
+/// Whether the type is a collection.
+bool isCollectionType(TypeDeclaration type) {
+  return !type.isClass &&
+      !type.isEnum &&
+      !type.isProxyApi &&
+      (type.baseName.contains('List') || type.baseName == 'Map');
+}
diff --git a/packages/pigeon/lib/src/gobject/gobject_generator.dart b/packages/pigeon/lib/src/gobject/gobject_generator.dart
index 3469c98..9cb1796 100644
--- a/packages/pigeon/lib/src/gobject/gobject_generator.dart
+++ b/packages/pigeon/lib/src/gobject/gobject_generator.dart
@@ -74,7 +74,7 @@
 }
 
 /// Options that control how GObject code will be generated.
-class InternalGObjectOptions {
+class InternalGObjectOptions extends PigeonInternalOptions {
   /// Creates a [InternalGObjectOptions] object
   const InternalGObjectOptions({
     required this.headerIncludePath,
diff --git a/packages/pigeon/lib/src/java/java_generator.dart b/packages/pigeon/lib/src/java/java_generator.dart
index ec9c37a..26b346d 100644
--- a/packages/pigeon/lib/src/java/java_generator.dart
+++ b/packages/pigeon/lib/src/java/java_generator.dart
@@ -90,7 +90,7 @@
 }
 
 /// Options that control how Java code will be generated.
-class InternalJavaOptions {
+class InternalJavaOptions extends PigeonInternalOptions {
   /// Creates a [InternalJavaOptions] object
   const InternalJavaOptions({
     required this.javaOut,
diff --git a/packages/pigeon/lib/src/kotlin/kotlin_generator.dart b/packages/pigeon/lib/src/kotlin/kotlin_generator.dart
index 31f2983..1368fa3 100644
--- a/packages/pigeon/lib/src/kotlin/kotlin_generator.dart
+++ b/packages/pigeon/lib/src/kotlin/kotlin_generator.dart
@@ -99,7 +99,7 @@
 }
 
 ///
-class InternalKotlinOptions {
+class InternalKotlinOptions extends PigeonInternalOptions {
   /// Creates a [InternalKotlinOptions] object
   const InternalKotlinOptions({
     this.package,
@@ -291,9 +291,46 @@
         classDefinition,
         dartPackageName: dartPackageName,
       );
+      writeClassEquality(
+        generatorOptions,
+        root,
+        indent,
+        classDefinition,
+        dartPackageName: dartPackageName,
+      );
     });
   }
 
+  @override
+  void writeClassEquality(
+    InternalKotlinOptions generatorOptions,
+    Root root,
+    Indent indent,
+    Class classDefinition, {
+    required String dartPackageName,
+  }) {
+    indent.writeScoped('override fun equals(other: Any?): Boolean {', '}', () {
+      indent.writeScoped('if (other !is ${classDefinition.name}) {', '}', () {
+        indent.writeln('return false');
+      });
+      indent.writeScoped('if (this === other) {', '}', () {
+        indent.writeln('return true');
+      });
+      indent.write('return ');
+      indent.format(
+        classDefinition.fields
+            .map((NamedType field) => isCollectionType(field.type)
+                ? 'deepEquals${generatorOptions.fileSpecificClassNameComponent}(${field.name}, other.${field.name})'
+                : '${field.name} == other.${field.name}')
+            .join('\n&& '),
+        leadingSpace: false,
+      );
+    });
+
+    indent.newln();
+    indent.writeln('override fun hashCode(): Int = toList().hashCode()');
+  }
+
   void _writeDataClassSignature(
     Indent indent,
     Class classDefinition, {
@@ -507,7 +544,7 @@
     indent.newln();
     if (root.containsEventChannel) {
       indent.writeln(
-          'val ${generatorOptions.fileSpecificClassNameComponent}$_pigeonMethodChannelCodec = StandardMethodCodec(${generatorOptions.fileSpecificClassNameComponent}$_codecName());');
+          'val ${generatorOptions.fileSpecificClassNameComponent}$_pigeonMethodChannelCodec = StandardMethodCodec(${generatorOptions.fileSpecificClassNameComponent}$_codecName())');
       indent.newln();
     }
   }
@@ -1219,6 +1256,36 @@
     });
   }
 
+  void _writeDeepEquals(InternalKotlinOptions generatorOptions, Indent indent) {
+    indent.format('''
+private fun deepEquals${generatorOptions.fileSpecificClassNameComponent}(a: Any?, b: Any?): Boolean {
+  if (a is ByteArray && b is ByteArray) {
+      return a.contentEquals(b)
+  }
+  if (a is IntArray && b is IntArray) {
+      return a.contentEquals(b)
+  }
+  if (a is LongArray && b is LongArray) {
+      return a.contentEquals(b)
+  }
+  if (a is DoubleArray && b is DoubleArray) {
+      return a.contentEquals(b)
+  }
+  if (a is Array<*> && b is Array<*>) {
+    return a.size == b.size &&
+        a.indices.all{ deepEquals${generatorOptions.fileSpecificClassNameComponent}(a[it], b[it]) }
+  }
+  if (a is Map<*, *> && b is Map<*, *>) {
+    return a.size == b.size && a.keys.all {
+        (b as Map<Any?, Any?>).containsKey(it) &&
+        deepEquals${generatorOptions.fileSpecificClassNameComponent}(a[it], b[it])
+    }
+  }
+  return a == b;
+}
+    ''');
+  }
+
   @override
   void writeGeneralUtilities(
     InternalKotlinOptions generatorOptions,
@@ -1236,6 +1303,11 @@
     if (generatorOptions.includeErrorClass) {
       _writeErrorClass(generatorOptions, indent);
     }
+    if (root.classes.isNotEmpty &&
+        root.classes.any((Class dataClass) => dataClass.fields
+            .any((NamedType field) => isCollectionType(field.type)))) {
+      _writeDeepEquals(generatorOptions, indent);
+    }
   }
 
   static void _writeMethodDeclaration(
diff --git a/packages/pigeon/lib/src/objc/objc_generator.dart b/packages/pigeon/lib/src/objc/objc_generator.dart
index 85b92bd..0a2e913 100644
--- a/packages/pigeon/lib/src/objc/objc_generator.dart
+++ b/packages/pigeon/lib/src/objc/objc_generator.dart
@@ -93,7 +93,7 @@
 }
 
 /// Options that control how Objective-C code will be generated.
-class InternalObjcOptions {
+class InternalObjcOptions extends PigeonInternalOptions {
   /// Parametric constructor for InternalObjcOptions.
   const InternalObjcOptions({
     required this.headerIncludePath,
diff --git a/packages/pigeon/lib/src/swift/swift_generator.dart b/packages/pigeon/lib/src/swift/swift_generator.dart
index 88aa792..c9d2cdc 100644
--- a/packages/pigeon/lib/src/swift/swift_generator.dart
+++ b/packages/pigeon/lib/src/swift/swift_generator.dart
@@ -79,7 +79,7 @@
 }
 
 /// Options that control how Swift code will be generated.
-class InternalSwiftOptions {
+class InternalSwiftOptions extends PigeonInternalOptions {
   /// Creates a [InternalSwiftOptions] object
   const InternalSwiftOptions({
     this.copyrightHeader,
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 467de3c..6871e5f 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
@@ -49,6 +49,29 @@
     val details: Any? = null
 ) : Throwable()
 
+private fun deepEqualsCoreTests(a: Any?, b: Any?): Boolean {
+  if (a is ByteArray && b is ByteArray) {
+    return a.contentEquals(b)
+  }
+  if (a is IntArray && b is IntArray) {
+    return a.contentEquals(b)
+  }
+  if (a is LongArray && b is LongArray) {
+    return a.contentEquals(b)
+  }
+  if (a is DoubleArray && b is DoubleArray) {
+    return a.contentEquals(b)
+  }
+  if (a is Array<*> && b is Array<*>) {
+    return a.size == b.size && a.indices.all { deepEqualsCoreTests(a[it], b[it]) }
+  }
+  if (a is Map<*, *> && b is Map<*, *>) {
+    return a.size == b.size &&
+        a.keys.all { (b as Map<Any?, Any?>).containsKey(it) && deepEqualsCoreTests(a[it], b[it]) }
+  }
+  return a == b
+}
+
 enum class AnEnum(val raw: Int) {
   ONE(0),
   TWO(1),
@@ -87,6 +110,18 @@
         aField,
     )
   }
+
+  override fun equals(other: Any?): Boolean {
+    if (other !is UnusedClass) {
+      return false
+    }
+    if (this === other) {
+      return true
+    }
+    return aField == other.aField
+  }
+
+  override fun hashCode(): Int = toList().hashCode()
 }
 
 /**
@@ -218,6 +253,45 @@
         mapMap,
     )
   }
+
+  override fun equals(other: Any?): Boolean {
+    if (other !is AllTypes) {
+      return false
+    }
+    if (this === other) {
+      return true
+    }
+    return aBool == other.aBool &&
+        anInt == other.anInt &&
+        anInt64 == other.anInt64 &&
+        aDouble == other.aDouble &&
+        deepEqualsCoreTests(aByteArray, other.aByteArray) &&
+        deepEqualsCoreTests(a4ByteArray, other.a4ByteArray) &&
+        deepEqualsCoreTests(a8ByteArray, other.a8ByteArray) &&
+        deepEqualsCoreTests(aFloatArray, other.aFloatArray) &&
+        anEnum == other.anEnum &&
+        anotherEnum == other.anotherEnum &&
+        aString == other.aString &&
+        anObject == other.anObject &&
+        deepEqualsCoreTests(list, other.list) &&
+        deepEqualsCoreTests(stringList, other.stringList) &&
+        deepEqualsCoreTests(intList, other.intList) &&
+        deepEqualsCoreTests(doubleList, other.doubleList) &&
+        deepEqualsCoreTests(boolList, other.boolList) &&
+        deepEqualsCoreTests(enumList, other.enumList) &&
+        deepEqualsCoreTests(objectList, other.objectList) &&
+        deepEqualsCoreTests(listList, other.listList) &&
+        deepEqualsCoreTests(mapList, other.mapList) &&
+        deepEqualsCoreTests(map, other.map) &&
+        deepEqualsCoreTests(stringMap, other.stringMap) &&
+        deepEqualsCoreTests(intMap, other.intMap) &&
+        deepEqualsCoreTests(enumMap, other.enumMap) &&
+        deepEqualsCoreTests(objectMap, other.objectMap) &&
+        deepEqualsCoreTests(listMap, other.listMap) &&
+        deepEqualsCoreTests(mapMap, other.mapMap)
+  }
+
+  override fun hashCode(): Int = toList().hashCode()
 }
 
 /**
@@ -361,6 +435,48 @@
         recursiveClassMap,
     )
   }
+
+  override fun equals(other: Any?): Boolean {
+    if (other !is AllNullableTypes) {
+      return false
+    }
+    if (this === other) {
+      return true
+    }
+    return aNullableBool == other.aNullableBool &&
+        aNullableInt == other.aNullableInt &&
+        aNullableInt64 == other.aNullableInt64 &&
+        aNullableDouble == other.aNullableDouble &&
+        deepEqualsCoreTests(aNullableByteArray, other.aNullableByteArray) &&
+        deepEqualsCoreTests(aNullable4ByteArray, other.aNullable4ByteArray) &&
+        deepEqualsCoreTests(aNullable8ByteArray, other.aNullable8ByteArray) &&
+        deepEqualsCoreTests(aNullableFloatArray, other.aNullableFloatArray) &&
+        aNullableEnum == other.aNullableEnum &&
+        anotherNullableEnum == other.anotherNullableEnum &&
+        aNullableString == other.aNullableString &&
+        aNullableObject == other.aNullableObject &&
+        allNullableTypes == other.allNullableTypes &&
+        deepEqualsCoreTests(list, other.list) &&
+        deepEqualsCoreTests(stringList, other.stringList) &&
+        deepEqualsCoreTests(intList, other.intList) &&
+        deepEqualsCoreTests(doubleList, other.doubleList) &&
+        deepEqualsCoreTests(boolList, other.boolList) &&
+        deepEqualsCoreTests(enumList, other.enumList) &&
+        deepEqualsCoreTests(objectList, other.objectList) &&
+        deepEqualsCoreTests(listList, other.listList) &&
+        deepEqualsCoreTests(mapList, other.mapList) &&
+        deepEqualsCoreTests(recursiveClassList, other.recursiveClassList) &&
+        deepEqualsCoreTests(map, other.map) &&
+        deepEqualsCoreTests(stringMap, other.stringMap) &&
+        deepEqualsCoreTests(intMap, other.intMap) &&
+        deepEqualsCoreTests(enumMap, other.enumMap) &&
+        deepEqualsCoreTests(objectMap, other.objectMap) &&
+        deepEqualsCoreTests(listMap, other.listMap) &&
+        deepEqualsCoreTests(mapMap, other.mapMap) &&
+        deepEqualsCoreTests(recursiveClassMap, other.recursiveClassMap)
+  }
+
+  override fun hashCode(): Int = toList().hashCode()
 }
 
 /**
@@ -493,6 +609,45 @@
         mapMap,
     )
   }
+
+  override fun equals(other: Any?): Boolean {
+    if (other !is AllNullableTypesWithoutRecursion) {
+      return false
+    }
+    if (this === other) {
+      return true
+    }
+    return aNullableBool == other.aNullableBool &&
+        aNullableInt == other.aNullableInt &&
+        aNullableInt64 == other.aNullableInt64 &&
+        aNullableDouble == other.aNullableDouble &&
+        deepEqualsCoreTests(aNullableByteArray, other.aNullableByteArray) &&
+        deepEqualsCoreTests(aNullable4ByteArray, other.aNullable4ByteArray) &&
+        deepEqualsCoreTests(aNullable8ByteArray, other.aNullable8ByteArray) &&
+        deepEqualsCoreTests(aNullableFloatArray, other.aNullableFloatArray) &&
+        aNullableEnum == other.aNullableEnum &&
+        anotherNullableEnum == other.anotherNullableEnum &&
+        aNullableString == other.aNullableString &&
+        aNullableObject == other.aNullableObject &&
+        deepEqualsCoreTests(list, other.list) &&
+        deepEqualsCoreTests(stringList, other.stringList) &&
+        deepEqualsCoreTests(intList, other.intList) &&
+        deepEqualsCoreTests(doubleList, other.doubleList) &&
+        deepEqualsCoreTests(boolList, other.boolList) &&
+        deepEqualsCoreTests(enumList, other.enumList) &&
+        deepEqualsCoreTests(objectList, other.objectList) &&
+        deepEqualsCoreTests(listList, other.listList) &&
+        deepEqualsCoreTests(mapList, other.mapList) &&
+        deepEqualsCoreTests(map, other.map) &&
+        deepEqualsCoreTests(stringMap, other.stringMap) &&
+        deepEqualsCoreTests(intMap, other.intMap) &&
+        deepEqualsCoreTests(enumMap, other.enumMap) &&
+        deepEqualsCoreTests(objectMap, other.objectMap) &&
+        deepEqualsCoreTests(listMap, other.listMap) &&
+        deepEqualsCoreTests(mapMap, other.mapMap)
+  }
+
+  override fun hashCode(): Int = toList().hashCode()
 }
 
 /**
@@ -544,6 +699,24 @@
         nullableClassMap,
     )
   }
+
+  override fun equals(other: Any?): Boolean {
+    if (other !is AllClassesWrapper) {
+      return false
+    }
+    if (this === other) {
+      return true
+    }
+    return allNullableTypes == other.allNullableTypes &&
+        allNullableTypesWithoutRecursion == other.allNullableTypesWithoutRecursion &&
+        allTypes == other.allTypes &&
+        deepEqualsCoreTests(classList, other.classList) &&
+        deepEqualsCoreTests(nullableClassList, other.nullableClassList) &&
+        deepEqualsCoreTests(classMap, other.classMap) &&
+        deepEqualsCoreTests(nullableClassMap, other.nullableClassMap)
+  }
+
+  override fun hashCode(): Int = toList().hashCode()
 }
 
 /**
@@ -564,6 +737,18 @@
         testList,
     )
   }
+
+  override fun equals(other: Any?): Boolean {
+    if (other !is TestMessage) {
+      return false
+    }
+    if (this === other) {
+      return true
+    }
+    return deepEqualsCoreTests(testList, other.testList)
+  }
+
+  override fun hashCode(): Int = toList().hashCode()
 }
 
 private open class CoreTestsPigeonCodec : StandardMessageCodec() {
diff --git a/packages/pigeon/platform_tests/test_plugin/android/src/main/kotlin/com/example/test_plugin/EventChannelTests.gen.kt b/packages/pigeon/platform_tests/test_plugin/android/src/main/kotlin/com/example/test_plugin/EventChannelTests.gen.kt
index 1f24c42..6d39a54 100644
--- a/packages/pigeon/platform_tests/test_plugin/android/src/main/kotlin/com/example/test_plugin/EventChannelTests.gen.kt
+++ b/packages/pigeon/platform_tests/test_plugin/android/src/main/kotlin/com/example/test_plugin/EventChannelTests.gen.kt
@@ -28,6 +28,31 @@
     val details: Any? = null
 ) : Throwable()
 
+private fun deepEqualsEventChannelTests(a: Any?, b: Any?): Boolean {
+  if (a is ByteArray && b is ByteArray) {
+    return a.contentEquals(b)
+  }
+  if (a is IntArray && b is IntArray) {
+    return a.contentEquals(b)
+  }
+  if (a is LongArray && b is LongArray) {
+    return a.contentEquals(b)
+  }
+  if (a is DoubleArray && b is DoubleArray) {
+    return a.contentEquals(b)
+  }
+  if (a is Array<*> && b is Array<*>) {
+    return a.size == b.size && a.indices.all { deepEqualsEventChannelTests(a[it], b[it]) }
+  }
+  if (a is Map<*, *> && b is Map<*, *>) {
+    return a.size == b.size &&
+        a.keys.all {
+          (b as Map<Any?, Any?>).containsKey(it) && deepEqualsEventChannelTests(a[it], b[it])
+        }
+  }
+  return a == b
+}
+
 enum class EventEnum(val raw: Int) {
   ONE(0),
   TWO(1),
@@ -193,6 +218,48 @@
         recursiveClassMap,
     )
   }
+
+  override fun equals(other: Any?): Boolean {
+    if (other !is EventAllNullableTypes) {
+      return false
+    }
+    if (this === other) {
+      return true
+    }
+    return aNullableBool == other.aNullableBool &&
+        aNullableInt == other.aNullableInt &&
+        aNullableInt64 == other.aNullableInt64 &&
+        aNullableDouble == other.aNullableDouble &&
+        deepEqualsEventChannelTests(aNullableByteArray, other.aNullableByteArray) &&
+        deepEqualsEventChannelTests(aNullable4ByteArray, other.aNullable4ByteArray) &&
+        deepEqualsEventChannelTests(aNullable8ByteArray, other.aNullable8ByteArray) &&
+        deepEqualsEventChannelTests(aNullableFloatArray, other.aNullableFloatArray) &&
+        aNullableEnum == other.aNullableEnum &&
+        anotherNullableEnum == other.anotherNullableEnum &&
+        aNullableString == other.aNullableString &&
+        aNullableObject == other.aNullableObject &&
+        allNullableTypes == other.allNullableTypes &&
+        deepEqualsEventChannelTests(list, other.list) &&
+        deepEqualsEventChannelTests(stringList, other.stringList) &&
+        deepEqualsEventChannelTests(intList, other.intList) &&
+        deepEqualsEventChannelTests(doubleList, other.doubleList) &&
+        deepEqualsEventChannelTests(boolList, other.boolList) &&
+        deepEqualsEventChannelTests(enumList, other.enumList) &&
+        deepEqualsEventChannelTests(objectList, other.objectList) &&
+        deepEqualsEventChannelTests(listList, other.listList) &&
+        deepEqualsEventChannelTests(mapList, other.mapList) &&
+        deepEqualsEventChannelTests(recursiveClassList, other.recursiveClassList) &&
+        deepEqualsEventChannelTests(map, other.map) &&
+        deepEqualsEventChannelTests(stringMap, other.stringMap) &&
+        deepEqualsEventChannelTests(intMap, other.intMap) &&
+        deepEqualsEventChannelTests(enumMap, other.enumMap) &&
+        deepEqualsEventChannelTests(objectMap, other.objectMap) &&
+        deepEqualsEventChannelTests(listMap, other.listMap) &&
+        deepEqualsEventChannelTests(mapMap, other.mapMap) &&
+        deepEqualsEventChannelTests(recursiveClassMap, other.recursiveClassMap)
+  }
+
+  override fun hashCode(): Int = toList().hashCode()
 }
 
 /**
@@ -214,6 +281,18 @@
         value,
     )
   }
+
+  override fun equals(other: Any?): Boolean {
+    if (other !is IntEvent) {
+      return false
+    }
+    if (this === other) {
+      return true
+    }
+    return value == other.value
+  }
+
+  override fun hashCode(): Int = toList().hashCode()
 }
 
 /** Generated class from Pigeon that represents data sent in messages. */
@@ -230,6 +309,18 @@
         value,
     )
   }
+
+  override fun equals(other: Any?): Boolean {
+    if (other !is StringEvent) {
+      return false
+    }
+    if (this === other) {
+      return true
+    }
+    return value == other.value
+  }
+
+  override fun hashCode(): Int = toList().hashCode()
 }
 
 /** Generated class from Pigeon that represents data sent in messages. */
@@ -246,6 +337,18 @@
         value,
     )
   }
+
+  override fun equals(other: Any?): Boolean {
+    if (other !is BoolEvent) {
+      return false
+    }
+    if (this === other) {
+      return true
+    }
+    return value == other.value
+  }
+
+  override fun hashCode(): Int = toList().hashCode()
 }
 
 /** Generated class from Pigeon that represents data sent in messages. */
@@ -262,6 +365,18 @@
         value,
     )
   }
+
+  override fun equals(other: Any?): Boolean {
+    if (other !is DoubleEvent) {
+      return false
+    }
+    if (this === other) {
+      return true
+    }
+    return value == other.value
+  }
+
+  override fun hashCode(): Int = toList().hashCode()
 }
 
 /** Generated class from Pigeon that represents data sent in messages. */
@@ -278,6 +393,18 @@
         value,
     )
   }
+
+  override fun equals(other: Any?): Boolean {
+    if (other !is ObjectsEvent) {
+      return false
+    }
+    if (this === other) {
+      return true
+    }
+    return value == other.value
+  }
+
+  override fun hashCode(): Int = toList().hashCode()
 }
 
 /** Generated class from Pigeon that represents data sent in messages. */
@@ -294,6 +421,18 @@
         value,
     )
   }
+
+  override fun equals(other: Any?): Boolean {
+    if (other !is EnumEvent) {
+      return false
+    }
+    if (this === other) {
+      return true
+    }
+    return value == other.value
+  }
+
+  override fun hashCode(): Int = toList().hashCode()
 }
 
 /** Generated class from Pigeon that represents data sent in messages. */
@@ -310,6 +449,18 @@
         value,
     )
   }
+
+  override fun equals(other: Any?): Boolean {
+    if (other !is ClassEvent) {
+      return false
+    }
+    if (this === other) {
+      return true
+    }
+    return value == other.value
+  }
+
+  override fun hashCode(): Int = toList().hashCode()
 }
 
 private open class EventChannelTestsPigeonCodec : StandardMessageCodec() {
diff --git a/packages/pigeon/platform_tests/test_plugin/android/src/test/kotlin/com/example/test_plugin/AllDatatypesTest.kt b/packages/pigeon/platform_tests/test_plugin/android/src/test/kotlin/com/example/test_plugin/AllDatatypesTest.kt
index aa42535..a630b27 100644
--- a/packages/pigeon/platform_tests/test_plugin/android/src/test/kotlin/com/example/test_plugin/AllDatatypesTest.kt
+++ b/packages/pigeon/platform_tests/test_plugin/android/src/test/kotlin/com/example/test_plugin/AllDatatypesTest.kt
@@ -10,70 +10,13 @@
 import java.nio.ByteBuffer
 import java.util.ArrayList
 import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotEquals
 import org.junit.Assert.assertNotNull
 import org.junit.Assert.assertTrue
 import org.junit.Test
 
 internal class AllDatatypesTest {
 
-  fun compareAllTypes(firstTypes: AllTypes?, secondTypes: AllTypes?) {
-    assertEquals(firstTypes == null, secondTypes == null)
-    if (firstTypes == null || secondTypes == null) {
-      return
-    }
-    assertEquals(firstTypes.aBool, secondTypes.aBool)
-    assertEquals(firstTypes.anInt, secondTypes.anInt)
-    assertEquals(firstTypes.anInt64, secondTypes.anInt64)
-    assertEquals(firstTypes.aDouble, secondTypes.aDouble, 0.0)
-    assertEquals(firstTypes.aString, secondTypes.aString)
-    assertTrue(firstTypes.aByteArray.contentEquals(secondTypes.aByteArray))
-    assertTrue(firstTypes.a4ByteArray.contentEquals(secondTypes.a4ByteArray))
-    assertTrue(firstTypes.a8ByteArray.contentEquals(secondTypes.a8ByteArray))
-    assertTrue(firstTypes.aFloatArray.contentEquals(secondTypes.aFloatArray))
-    assertEquals(firstTypes.anEnum, secondTypes.anEnum)
-    assertEquals(firstTypes.anotherEnum, secondTypes.anotherEnum)
-    assertEquals(firstTypes.anObject, secondTypes.anObject)
-    assertEquals(firstTypes.list, secondTypes.list)
-    assertEquals(firstTypes.boolList, secondTypes.boolList)
-    assertEquals(firstTypes.doubleList, secondTypes.doubleList)
-    assertEquals(firstTypes.intList, secondTypes.intList)
-    assertEquals(firstTypes.stringList, secondTypes.stringList)
-    assertEquals(firstTypes.map, secondTypes.map)
-  }
-
-  private fun compareAllNullableTypes(
-      firstTypes: AllNullableTypes?,
-      secondTypes: AllNullableTypes?
-  ) {
-    assertEquals(firstTypes == null, secondTypes == null)
-    if (firstTypes == null || secondTypes == null) {
-      return
-    }
-    assertEquals(firstTypes.aNullableBool, secondTypes.aNullableBool)
-    assertEquals(firstTypes.aNullableInt, secondTypes.aNullableInt)
-    assertEquals(firstTypes.aNullableDouble, secondTypes.aNullableDouble)
-    assertEquals(firstTypes.aNullableString, secondTypes.aNullableString)
-    assertTrue(firstTypes.aNullableByteArray.contentEquals(secondTypes.aNullableByteArray))
-    assertTrue(firstTypes.aNullable4ByteArray.contentEquals(secondTypes.aNullable4ByteArray))
-    assertTrue(firstTypes.aNullable8ByteArray.contentEquals(secondTypes.aNullable8ByteArray))
-    assertTrue(firstTypes.aNullableFloatArray.contentEquals(secondTypes.aNullableFloatArray))
-    assertEquals(firstTypes.aNullableObject, secondTypes.aNullableObject)
-    assertEquals(firstTypes.aNullableEnum, secondTypes.aNullableEnum)
-    assertEquals(firstTypes.anotherNullableEnum, secondTypes.anotherNullableEnum)
-    assertEquals(firstTypes.list, secondTypes.list)
-    assertEquals(firstTypes.boolList, secondTypes.boolList)
-    assertEquals(firstTypes.doubleList, secondTypes.doubleList)
-    assertEquals(firstTypes.intList, secondTypes.intList)
-    assertEquals(firstTypes.stringList, secondTypes.stringList)
-    assertEquals(firstTypes.listList, secondTypes.listList)
-    assertEquals(firstTypes.mapList, secondTypes.mapList)
-    assertEquals(firstTypes.map, secondTypes.map)
-    assertEquals(firstTypes.stringMap, secondTypes.stringMap)
-    assertEquals(firstTypes.intMap, secondTypes.intMap)
-    assertEquals(firstTypes.listMap, secondTypes.listMap)
-    assertEquals(firstTypes.mapMap, secondTypes.mapMap)
-  }
-
   @Test
   fun testNullValues() {
     val everything = AllNullableTypes()
@@ -95,7 +38,7 @@
     var didCall = false
     api.echoAllNullableTypes(everything) { result ->
       didCall = true
-      val output = (result.getOrNull())?.let { compareAllNullableTypes(it, everything) }
+      val output = (result.getOrNull())?.let { it == everything }
       assertNotNull(output)
     }
 
@@ -148,9 +91,111 @@
     var didCall = false
     api.echoAllNullableTypes(everything) {
       didCall = true
-      compareAllNullableTypes(everything, it.getOrNull())
+      assertTrue(everything == it.getOrNull())
     }
 
     assertTrue(didCall)
   }
+
+  private val correctList = listOf<Any?>("a", 2, "three")
+  private val matchingList = correctList.toMutableList()
+  private val differentList = listOf<Any?>("a", 2, "three", 4.0)
+
+  private val correctMap = mapOf<Any, Any?>("a" to 1, "b" to 2, "c" to "three")
+  private val matchingMap = correctMap.toMap()
+  private val differentKeyMap = mapOf<Any, Any?>("a" to 1, "b" to 2, "d" to "three")
+  private val differentValueMap = mapOf<Any, Any?>("a" to 1, "b" to 2, "c" to "five")
+
+  private val correctListInMap = mapOf<Any, Any?>("a" to 1, "b" to 2, "c" to correctList)
+  private val matchingListInMap = mapOf<Any, Any?>("a" to 1, "b" to 2, "c" to matchingList)
+  private val differentListInMap = mapOf<Any, Any?>("a" to 1, "b" to 2, "c" to differentList)
+
+  private val correctMapInList = listOf<Any?>("a", 2, correctMap)
+  private val matchingMapInList = listOf<Any?>("a", 2, matchingMap)
+  private val differentKeyMapInList = listOf<Any?>("a", 2, differentKeyMap)
+  private val differentValueMapInList = listOf<Any?>("a", 2, differentValueMap)
+
+  @Test
+  fun `equality method correctly checks deep equality`() {
+    val generic = AllNullableTypes(list = correctList, map = correctMap)
+    val identical = generic.copy()
+    assertEquals(generic, identical)
+  }
+
+  @Test
+  fun `equality method correctly identifies non-matching classes`() {
+    val generic = AllNullableTypes(list = correctList, map = correctMap)
+    val allNull = AllNullableTypes()
+    assertNotEquals(allNull, generic)
+  }
+
+  @Test
+  fun `equality method correctly identifies non-matching lists in classes`() {
+    val withList = AllNullableTypes(list = correctList)
+    val withDifferentList = AllNullableTypes(list = differentList)
+    assertNotEquals(withList, withDifferentList)
+  }
+
+  @Test
+  fun `equality method correctly identifies matching -but unique- lists in classes`() {
+    val withList = AllNullableTypes(list = correctList)
+    val withDifferentList = AllNullableTypes(list = matchingList)
+    assertEquals(withList, withDifferentList)
+  }
+
+  @Test
+  fun `equality method correctly identifies non-matching keys in maps in classes`() {
+    val withMap = AllNullableTypes(map = correctMap)
+    val withDifferentMap = AllNullableTypes(map = differentKeyMap)
+    assertNotEquals(withMap, withDifferentMap)
+  }
+
+  @Test
+  fun `equality method correctly identifies non-matching values in maps in classes`() {
+    val withMap = AllNullableTypes(map = correctMap)
+    val withDifferentMap = AllNullableTypes(map = differentValueMap)
+    assertNotEquals(withMap, withDifferentMap)
+  }
+
+  @Test
+  fun `equality method correctly identifies matching -but unique- maps in classes`() {
+    val withMap = AllNullableTypes(map = correctMap)
+    val withDifferentMap = AllNullableTypes(map = matchingMap)
+    assertEquals(withMap, withDifferentMap)
+  }
+
+  @Test
+  fun `equality method correctly identifies non-matching lists nested in maps in classes`() {
+    val withListInMap = AllNullableTypes(map = correctListInMap)
+    val withDifferentListInMap = AllNullableTypes(map = differentListInMap)
+    assertNotEquals(withListInMap, withDifferentListInMap)
+  }
+
+  @Test
+  fun `equality method correctly identifies matching -but unique- lists nested in maps in classes`() {
+    val withListInMap = AllNullableTypes(map = correctListInMap)
+    val withDifferentListInMap = AllNullableTypes(map = matchingListInMap)
+    assertEquals(withListInMap, withDifferentListInMap)
+  }
+
+  @Test
+  fun `equality method correctly identifies non-matching keys in maps nested in lists in classes`() {
+    val withMapInList = AllNullableTypes(list = correctMapInList)
+    val withDifferentMapInList = AllNullableTypes(list = differentKeyMapInList)
+    assertNotEquals(withMapInList, withDifferentMapInList)
+  }
+
+  @Test
+  fun `equality method correctly identifies non-matching values in maps nested in lists in classes`() {
+    val withMapInList = AllNullableTypes(list = correctMapInList)
+    val withDifferentMapInList = AllNullableTypes(list = differentValueMapInList)
+    assertNotEquals(withMapInList, withDifferentMapInList)
+  }
+
+  @Test
+  fun `equality method correctly identifies matching -but unique- maps nested in lists in classes`() {
+    val withMapInList = AllNullableTypes(list = correctMapInList)
+    val withDifferentMapInList = AllNullableTypes(list = matchingMapInList)
+    assertEquals(withMapInList, withDifferentMapInList)
+  }
 }
diff --git a/packages/pigeon/pubspec.yaml b/packages/pigeon/pubspec.yaml
index a1e5e43..3459232 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%3A%22p%3A+pigeon%22
-version: 25.1.0 # This must match the version in lib/src/generator_tools.dart
+version: 25.2.0 # This must match the version in lib/src/generator_tools.dart
 
 environment:
   sdk: ^3.4.0