Skip to content

Commit 44ddf2c

Browse files
authored
[pigeon] Standardize host api error handling (#3234)
[pigeon] Standardize host api error handling
1 parent 9c37578 commit 44ddf2c

File tree

31 files changed

+1580
-1273
lines changed

31 files changed

+1580
-1273
lines changed

packages/pigeon/CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
## 9.1.0
2+
3+
* [java] Adds a `GeneratedApi.FlutterError` exception for passing custom error details (code, message, details).
4+
* [kotlin] Adds a `FlutterError` exception for passing custom error details (code, message, details).
5+
* [kotlin] Adds an `errorClassName` option in `KotlinOptions` for custom error class names.
6+
* [java] Removes legacy try catch from async apis.
7+
* [java] Removes legacy null check on non-nullable method arguments.
8+
* [cpp] Fixes wrong order of items in `FlutterError`.
9+
* Adds `FlutterError` handling integration tests for all platforms.
10+
111
## 9.0.7
212

313
* [swift] Changes all ints to int64.

packages/pigeon/README.md

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ abstract class Api2Host {
112112
Generates:
113113

114114
```objc
115-
// Objc
115+
// Objective-C
116116
@protocol Api2Host
117117
-(void)calculate:(nullable Value *)input
118118
completion:(void(^)(Value *_Nullable, FlutterError *_Nullable))completion;
@@ -145,7 +145,7 @@ public interface Api2Host {
145145

146146
/** Generated interface from Pigeon that represents a handler of messages from Flutter.*/
147147
interface Api2Host {
148-
fun calculate(value: Value, callback: (Value) -> Unit)
148+
fun calculate(value: Value, callback: (Result<Value>) -> Unit)
149149
}
150150
```
151151

@@ -224,6 +224,61 @@ abstract class Api2Host {
224224
}
225225
```
226226
227+
### Error Handling
228+
229+
#### Kotlin, Java and Swift
230+
231+
All Host API exceptions are translated into Flutter `PlatformException`.
232+
* For synchronous methods, thrown exceptions will be caught and translated.
233+
* For asynchronous methods, there is no default exception handling; errors should be returned via the provided callback.
234+
235+
To pass custom details into `PlatformException` for error handling, use `FlutterError` in your Host API.
236+
For example:
237+
238+
```kotlin
239+
// Kotlin
240+
class MyApi : GeneratedApi {
241+
// For synchronous methods
242+
override fun doSomething() {
243+
throw FlutterError('error_code', 'message', 'details')
244+
}
245+
246+
// For async methods
247+
override fun doSomethingAsync(callback: (Result<Unit>) -> Unit) {
248+
callback(Result.failure(FlutterError('error_code', 'message', 'details'))
249+
}
250+
}
251+
```
252+
253+
#### Objective-C and C++
254+
255+
Likewise, Host API errors can be sent using the provided `FlutterError` class (translated into `PlatformException`).
256+
257+
For synchronous methods:
258+
* Objective-C - Assign the `error` argument to a `FlutterError` reference.
259+
* C++ - Return a `FlutterError` directly (for void methods) or within an `ErrorOr` instance.
260+
261+
For async methods:
262+
* Both - Return a `FlutterError` through the provided callback.
263+
264+
#### Handling the errors
265+
266+
Then you can implement error handling on the Flutter side:
267+
268+
```dart
269+
// Dart
270+
void doSomething() {
271+
try {
272+
myApi.doSomething()
273+
} catch (PlatformException e) {
274+
if (e.code == 'error_code') {
275+
// Perform custom error handling
276+
assert(e.message == 'message')
277+
assert(e.details == 'details')
278+
}
279+
}
280+
}
281+
```
227282

228283
## Feedback
229284

packages/pigeon/lib/cpp_generator.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -839,8 +839,8 @@ EncodableValue ${api.name}::WrapError(std::string_view error_message) {
839839
}
840840
EncodableValue ${api.name}::WrapError(const FlutterError& error) {
841841
\treturn EncodableValue(EncodableList{
842-
\t\tEncodableValue(error.message()),
843842
\t\tEncodableValue(error.code()),
843+
\t\tEncodableValue(error.message()),
844844
\t\terror.details()
845845
\t});
846846
}''');

packages/pigeon/lib/generator_tools.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import 'ast.dart';
1111
/// The current version of pigeon.
1212
///
1313
/// This must match the version in pubspec.yaml.
14-
const String pigeonVersion = '9.0.7';
14+
const String pigeonVersion = '9.1.0';
1515

1616
/// Read all the content from [stdin] to a String.
1717
String readStdin() {

packages/pigeon/lib/java_generator.dart

Lines changed: 95 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ class JavaGenerator extends StructuredGenerator<JavaOptions> {
132132
indent.writeln(
133133
'$_docCommentPrefix Generated class from Pigeon.$_docCommentSuffix');
134134
indent.writeln(
135-
'@SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression"})');
135+
'@SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression", "serial"})');
136136
if (generatorOptions.useGeneratedAnnotation ?? false) {
137137
indent.writeln('@javax.annotation.Generated("dev.flutter.pigeon")');
138138
}
@@ -606,46 +606,36 @@ class JavaGenerator extends StructuredGenerator<JavaOptions> {
606606
: _javaTypeForDartType(method.returnType);
607607
indent.writeln(
608608
'ArrayList<Object> wrapped = new ArrayList<Object>();');
609-
indent.write('try ');
610-
indent.addScoped('{', '}', () {
611-
final List<String> methodArgument = <String>[];
612-
if (method.arguments.isNotEmpty) {
613-
indent.writeln(
614-
'ArrayList<Object> args = (ArrayList<Object>) message;');
615-
indent.writeln('assert args != null;');
616-
enumerate(method.arguments, (int index, NamedType arg) {
617-
// The StandardMessageCodec can give us [Integer, Long] for
618-
// a Dart 'int'. To keep things simple we just use 64bit
619-
// longs in Pigeon with Java.
620-
final bool isInt = arg.type.baseName == 'int';
621-
final String argType =
622-
isInt ? 'Number' : _javaTypeForDartType(arg.type);
623-
final String argName = _getSafeArgumentName(index, arg);
624-
final String argExpression = isInt
625-
? '($argName == null) ? null : $argName.longValue()'
626-
: argName;
627-
String accessor = 'args.get($index)';
628-
if (isEnum(root, arg.type)) {
629-
accessor = _intToEnum(accessor, arg.type.baseName);
630-
} else {
631-
accessor = _cast(accessor, javaType: argType);
632-
}
633-
indent.writeln('$argType $argName = $accessor;');
634-
if (!arg.type.isNullable) {
635-
indent.write('if ($argName == null) ');
636-
indent.addScoped('{', '}', () {
637-
indent.writeln(
638-
'throw new NullPointerException("$argName unexpectedly null.");');
639-
});
640-
}
641-
methodArgument.add(argExpression);
642-
});
643-
}
644-
if (method.isAsynchronous) {
645-
final String resultValue =
646-
method.returnType.isVoid ? 'null' : 'result';
647-
const String resultName = 'resultCallback';
648-
indent.format('''
609+
final List<String> methodArgument = <String>[];
610+
if (method.arguments.isNotEmpty) {
611+
indent.writeln(
612+
'ArrayList<Object> args = (ArrayList<Object>) message;');
613+
enumerate(method.arguments, (int index, NamedType arg) {
614+
// The StandardMessageCodec can give us [Integer, Long] for
615+
// a Dart 'int'. To keep things simple we just use 64bit
616+
// longs in Pigeon with Java.
617+
final bool isInt = arg.type.baseName == 'int';
618+
final String argType =
619+
isInt ? 'Number' : _javaTypeForDartType(arg.type);
620+
final String argName = _getSafeArgumentName(index, arg);
621+
final String argExpression = isInt
622+
? '($argName == null) ? null : $argName.longValue()'
623+
: argName;
624+
String accessor = 'args.get($index)';
625+
if (isEnum(root, arg.type)) {
626+
accessor = _intToEnum(accessor, arg.type.baseName);
627+
} else if (argType != 'Object') {
628+
accessor = _cast(accessor, javaType: argType);
629+
}
630+
indent.writeln('$argType $argName = $accessor;');
631+
methodArgument.add(argExpression);
632+
});
633+
}
634+
if (method.isAsynchronous) {
635+
final String resultValue =
636+
method.returnType.isVoid ? 'null' : 'result';
637+
const String resultName = 'resultCallback';
638+
indent.format('''
649639
Result<$returnType> $resultName =
650640
\t\tnew Result<$returnType>() {
651641
\t\t\tpublic void success($returnType result) {
@@ -659,31 +649,33 @@ Result<$returnType> $resultName =
659649
\t\t\t}
660650
\t\t};
661651
''');
662-
methodArgument.add(resultName);
663-
}
664-
final String call =
665-
'api.${method.name}(${methodArgument.join(', ')})';
666-
if (method.isAsynchronous) {
667-
indent.writeln('$call;');
668-
} else if (method.returnType.isVoid) {
669-
indent.writeln('$call;');
670-
indent.writeln('wrapped.add(0, null);');
671-
} else {
672-
indent.writeln('$returnType output = $call;');
673-
indent.writeln('wrapped.add(0, output);');
674-
}
675-
}, addTrailingNewline: false);
676-
indent.add(' catch (Error | RuntimeException exception) ');
677-
indent.addScoped('{', '}', () {
678-
indent.writeln(
679-
'ArrayList<Object> wrappedError = wrapError(exception);');
680-
if (method.isAsynchronous) {
681-
indent.writeln('reply.reply(wrappedError);');
682-
} else {
683-
indent.writeln('wrapped = wrappedError;');
684-
}
685-
});
686-
if (!method.isAsynchronous) {
652+
methodArgument.add(resultName);
653+
}
654+
final String call =
655+
'api.${method.name}(${methodArgument.join(', ')})';
656+
if (method.isAsynchronous) {
657+
indent.writeln('$call;');
658+
} else {
659+
indent.write('try ');
660+
indent.addScoped('{', '}', () {
661+
if (method.returnType.isVoid) {
662+
indent.writeln('$call;');
663+
indent.writeln('wrapped.add(0, null);');
664+
} else {
665+
indent.writeln('$returnType output = $call;');
666+
indent.writeln('wrapped.add(0, output);');
667+
}
668+
});
669+
indent.add(' catch (Throwable exception) ');
670+
indent.addScoped('{', '}', () {
671+
indent.writeln(
672+
'ArrayList<Object> wrappedError = wrapError(exception);');
673+
if (method.isAsynchronous) {
674+
indent.writeln('reply.reply(wrappedError);');
675+
} else {
676+
indent.writeln('wrapped = wrappedError;');
677+
}
678+
});
687679
indent.writeln('reply.reply(wrapped);');
688680
}
689681
});
@@ -765,22 +757,55 @@ Result<$returnType> $resultName =
765757
});
766758
}
767759

760+
void _writeErrorClass(Indent indent) {
761+
indent.writeln(
762+
'/** Error class for passing custom error details to Flutter via a thrown PlatformException. */');
763+
indent.write('public static class FlutterError extends RuntimeException ');
764+
indent.addScoped('{', '}', () {
765+
indent.newln();
766+
indent.writeln('/** The error code. */');
767+
indent.writeln('public final String code;');
768+
indent.newln();
769+
indent.writeln(
770+
'/** The error details. Must be a datatype supported by the api codec. */');
771+
indent.writeln('public final Object details;');
772+
indent.newln();
773+
indent.writeln(
774+
'public FlutterError(@NonNull String code, @Nullable String message, @Nullable Object details) ');
775+
indent.writeScoped('{', '}', () {
776+
indent.writeln('super(message);');
777+
indent.writeln('this.code = code;');
778+
indent.writeln('this.details = details;');
779+
});
780+
});
781+
}
782+
768783
void _writeWrapError(Indent indent) {
769784
indent.format('''
770785
@NonNull
771786
private static ArrayList<Object> wrapError(@NonNull Throwable exception) {
772787
\tArrayList<Object> errorList = new ArrayList<Object>(3);
773-
\terrorList.add(exception.toString());
774-
\terrorList.add(exception.getClass().getSimpleName());
775-
\terrorList.add(
776-
\t\t"Cause: " + exception.getCause() + ", Stacktrace: " + Log.getStackTraceString(exception));
788+
\tif (exception instanceof FlutterError) {
789+
\t\tFlutterError error = (FlutterError) exception;
790+
\t\terrorList.add(error.code);
791+
\t\terrorList.add(error.getMessage());
792+
\t\terrorList.add(error.details);
793+
\t} else {
794+
\t\terrorList.add(exception.toString());
795+
\t\terrorList.add(exception.getClass().getSimpleName());
796+
\t\terrorList.add(
797+
\t\t\t"Cause: " + exception.getCause() + ", Stacktrace: " + Log.getStackTraceString(exception));
798+
\t}
777799
\treturn errorList;
778800
}''');
779801
}
780802

781803
@override
782804
void writeGeneralUtilities(
783805
JavaOptions generatorOptions, Root root, Indent indent) {
806+
indent.newln();
807+
_writeErrorClass(indent);
808+
indent.newln();
784809
_writeWrapError(indent);
785810
}
786811

0 commit comments

Comments
 (0)