Skip to content

Commit 50ad6ee

Browse files
[pigeon] Implement equals for Java data classes (#6992)
Adds implementations of `equals` and `hashCode` to Java data classes. This is frequently useful for native unit tests of plugins using Pigeon (e.g., when using a mock FlutterApi implementation to check that the expected call is being made with the right arguments). Part of flutter/flutter#118087
1 parent 1612774 commit 50ad6ee

File tree

7 files changed

+311
-7
lines changed

7 files changed

+311
-7
lines changed

packages/pigeon/CHANGELOG.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
## NEXT
1+
## 20.0.2
22

3+
* [java] Adds `equals` and `hashCode` support for data classes.
34
* [swift] Fully-qualifies types in Equatable extension test.
45

56
## 20.0.1
@@ -17,7 +18,7 @@
1718

1819
## 19.0.2
1920

20-
* [kotlin] Adds the `@JvmOverloads` to the `HostApi` setUp method. This prevents the calling Java code from having to provide an empty `String` as Kotlin provides it by default
21+
* [kotlin] Adds the `@JvmOverloads` to the `HostApi` setUp method. This prevents the calling Java code from having to provide an empty `String` as Kotlin provides it by default
2122

2223
## 19.0.1
2324

packages/pigeon/example/app/android/app/src/main/java/io/flutter/plugins/Messages.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.util.Collections;
2323
import java.util.List;
2424
import java.util.Map;
25+
import java.util.Objects;
2526

2627
/** Generated class from Pigeon. */
2728
@SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression", "serial"})
@@ -132,6 +133,26 @@ public void setData(@NonNull Map<String, String> setterArg) {
132133
/** Constructor is non-public to enforce null safety; use Builder. */
133134
MessageData() {}
134135

136+
@Override
137+
public boolean equals(Object o) {
138+
if (this == o) {
139+
return true;
140+
}
141+
if (o == null || getClass() != o.getClass()) {
142+
return false;
143+
}
144+
MessageData that = (MessageData) o;
145+
return Objects.equals(name, that.name)
146+
&& Objects.equals(description, that.description)
147+
&& code.equals(that.code)
148+
&& data.equals(that.data);
149+
}
150+
151+
@Override
152+
public int hashCode() {
153+
return Objects.hash(name, description, code, data);
154+
}
155+
135156
public static final class Builder {
136157

137158
private @Nullable String name;

packages/pigeon/lib/generator_tools.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import 'ast.dart';
1313
/// The current version of pigeon.
1414
///
1515
/// This must match the version in pubspec.yaml.
16-
const String pigeonVersion = '20.0.1';
16+
const String pigeonVersion = '20.0.2';
1717

1818
/// Prefix for all local variables in methods.
1919
///

packages/pigeon/lib/java_generator.dart

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ class JavaGenerator extends StructuredGenerator<JavaOptions> {
141141
indent.writeln('import java.util.HashMap;');
142142
indent.writeln('import java.util.List;');
143143
indent.writeln('import java.util.Map;');
144+
indent.writeln('import java.util.Objects;');
144145
indent.newln();
145146
}
146147

@@ -233,6 +234,7 @@ class JavaGenerator extends StructuredGenerator<JavaOptions> {
233234
indent.writeln('${classDefinition.name}() {}');
234235
indent.newln();
235236
}
237+
_writeEquality(indent, classDefinition);
236238

237239
_writeClassBuilder(generatorOptions, root, indent, classDefinition);
238240
writeClassEncode(
@@ -282,6 +284,62 @@ class JavaGenerator extends StructuredGenerator<JavaOptions> {
282284
});
283285
}
284286

287+
void _writeEquality(Indent indent, Class classDefinition) {
288+
// Implement equals(...).
289+
indent.writeln('@Override');
290+
indent.writeScoped('public boolean equals(Object o) {', '}', () {
291+
indent.writeln('if (this == o) { return true; }');
292+
indent.writeln(
293+
'if (o == null || getClass() != o.getClass()) { return false; }');
294+
indent.writeln(
295+
'${classDefinition.name} that = (${classDefinition.name}) o;');
296+
final Iterable<String> checks = classDefinition.fields.map(
297+
(NamedType field) {
298+
// Objects.equals only does pointer equality for array types.
299+
if (_javaTypeIsArray(field.type)) {
300+
return 'Arrays.equals(${field.name}, that.${field.name})';
301+
}
302+
return field.type.isNullable
303+
? 'Objects.equals(${field.name}, that.${field.name})'
304+
: '${field.name}.equals(that.${field.name})';
305+
},
306+
);
307+
indent.writeln('return ${checks.join(' && ')};');
308+
});
309+
indent.newln();
310+
311+
// Implement hashCode().
312+
indent.writeln('@Override');
313+
indent.writeScoped('public int hashCode() {', '}', () {
314+
// As with equalty checks, arrays need special handling.
315+
final Iterable<String> arrayFieldNames = classDefinition.fields
316+
.where((NamedType field) => _javaTypeIsArray(field.type))
317+
.map((NamedType field) => field.name);
318+
final Iterable<String> nonArrayFieldNames = classDefinition.fields
319+
.where((NamedType field) => !_javaTypeIsArray(field.type))
320+
.map((NamedType field) => field.name);
321+
final String nonArrayHashValue = nonArrayFieldNames.isNotEmpty
322+
? 'Objects.hash(${nonArrayFieldNames.join(', ')})'
323+
: '0';
324+
325+
if (arrayFieldNames.isEmpty) {
326+
// Return directly if there are no array variables, to avoid redundant
327+
// variable lint warnings.
328+
indent.writeln('return $nonArrayHashValue;');
329+
} else {
330+
const String resultVar = '${varNamePrefix}result';
331+
indent.writeln('int $resultVar = $nonArrayHashValue;');
332+
// Manually mix in the Arrays.hashCode values.
333+
for (final String name in arrayFieldNames) {
334+
indent.writeln(
335+
'$resultVar = 31 * $resultVar + Arrays.hashCode($name);');
336+
}
337+
indent.writeln('return $resultVar;');
338+
}
339+
});
340+
indent.newln();
341+
}
342+
285343
void _writeClassBuilder(
286344
JavaOptions generatorOptions,
287345
Root root,
@@ -1022,6 +1080,10 @@ String _javaTypeForBuiltinGenericDartType(
10221080
}
10231081
}
10241082

1083+
bool _javaTypeIsArray(TypeDeclaration type) {
1084+
return _javaTypeForBuiltinDartType(type)?.endsWith('[]') ?? false;
1085+
}
1086+
10251087
String? _javaTypeForBuiltinDartType(TypeDeclaration type) {
10261088
const Map<String, String> javaTypeForDartTypeMap = <String, String>{
10271089
'bool': 'Boolean',

0 commit comments

Comments
 (0)