Skip to content

[pigeon] Standardize host api error handling #3234

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 36 commits into from
Mar 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
5a74262
standardize kotlin/java host api error wrapping
xegrox Feb 21, 2023
a2d74b1
changelog
xegrox Feb 21, 2023
97c02cb
tests
xegrox Feb 21, 2023
4ff920e
format
xegrox Feb 21, 2023
1575209
format
xegrox Feb 21, 2023
7a4d396
update test
xegrox Feb 22, 2023
e79fa0d
standardize for swift too
xegrox Feb 22, 2023
b4aca30
Merge branch 'main' into main
xegrox Feb 22, 2023
cdace1c
format
xegrox Feb 22, 2023
dc9c3e0
changelog
xegrox Feb 22, 2023
4294185
standardize for cpp
xegrox Feb 22, 2023
b28fec5
error class for kotlin + tests
xegrox Feb 24, 2023
59bde90
error class for java + tests
xegrox Feb 24, 2023
cf93865
update tests for other platforms
xegrox Feb 24, 2023
6371f12
fix missing try catch java
xegrox Feb 24, 2023
c9a30bb
Merge branch 'main' of https://github.com/flutter/packages
xegrox Feb 24, 2023
8ca9ee5
update version and changelog
xegrox Feb 24, 2023
50acb7a
format
xegrox Feb 24, 2023
84ea8a1
apply review changes
xegrox Feb 25, 2023
73f26df
one error class per file for kotlin
xegrox Feb 25, 2023
cf3d222
update changelog
xegrox Feb 25, 2023
8feaa2c
update changelog
xegrox Feb 25, 2023
7ee1845
Merge branch 'main' into main
xegrox Feb 25, 2023
5d1b57d
rename default error class to `FlutterError`
xegrox Feb 27, 2023
c898a63
format
xegrox Feb 27, 2023
eb154ac
update tests
xegrox Feb 27, 2023
43bfaaf
format
xegrox Feb 27, 2023
d9858ee
review changes
xegrox Mar 12, 2023
1d59674
Merge branch 'main' of https://github.com/flutter/packages
xegrox Mar 12, 2023
742dba0
fix warnings
xegrox Mar 12, 2023
52fe56b
update changelog
xegrox Mar 12, 2023
d50751d
fix cpp
xegrox Mar 12, 2023
7c57e7e
fix cpp
xegrox Mar 12, 2023
ee5d84a
review changes
xegrox Mar 15, 2023
698a29d
Merge branch 'main' into main
xegrox Mar 15, 2023
23e6659
Fix "Objective-C"
stuartmorgan-g Mar 15, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions packages/pigeon/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
## 9.1.0

* [java] Adds a `GeneratedApi.FlutterError` exception for passing custom error details (code, message, details).
* [kotlin] Adds a `FlutterError` exception for passing custom error details (code, message, details).
* [kotlin] Adds an `errorClassName` option in `KotlinOptions` for custom error class names.
* [java] Removes legacy try catch from async apis.
* [java] Removes legacy null check on non-nullable method arguments.
* [cpp] Fixes wrong order of items in `FlutterError`.
* Adds `FlutterError` handling integration tests for all platforms.

## 9.0.7

* [swift] Changes all ints to int64.
Expand Down
59 changes: 57 additions & 2 deletions packages/pigeon/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ abstract class Api2Host {
Generates:

```objc
// Objc
// Objective-C
@protocol Api2Host
-(void)calculate:(nullable Value *)input
completion:(void(^)(Value *_Nullable, FlutterError *_Nullable))completion;
Expand Down Expand Up @@ -145,7 +145,7 @@ public interface Api2Host {

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

Expand Down Expand Up @@ -224,6 +224,61 @@ abstract class Api2Host {
}
```

### Error Handling

#### Kotlin, Java and Swift

All Host API exceptions are translated into Flutter `PlatformException`.
* For synchronous methods, thrown exceptions will be caught and translated.
* For asynchronous methods, there is no default exception handling; errors should be returned via the provided callback.

To pass custom details into `PlatformException` for error handling, use `FlutterError` in your Host API.
For example:

```kotlin
// Kotlin
class MyApi : GeneratedApi {
// For synchronous methods
override fun doSomething() {
throw FlutterError('error_code', 'message', 'details')
}

// For async methods
override fun doSomethingAsync(callback: (Result<Unit>) -> Unit) {
callback(Result.failure(FlutterError('error_code', 'message', 'details'))
}
}
```

#### Objective-C and C++

Likewise, Host API errors can be sent using the provided `FlutterError` class (translated into `PlatformException`).

For synchronous methods:
* Objective-C - Assign the `error` argument to a `FlutterError` reference.
* C++ - Return a `FlutterError` directly (for void methods) or within an `ErrorOr` instance.

For async methods:
* Both - Return a `FlutterError` through the provided callback.

#### Handling the errors

Then you can implement error handling on the Flutter side:

```dart
// Dart
void doSomething() {
try {
myApi.doSomething()
} catch (PlatformException e) {
if (e.code == 'error_code') {
// Perform custom error handling
assert(e.message == 'message')
assert(e.details == 'details')
}
}
}
```

## Feedback

Expand Down
2 changes: 1 addition & 1 deletion packages/pigeon/lib/cpp_generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -839,8 +839,8 @@ EncodableValue ${api.name}::WrapError(std::string_view error_message) {
}
EncodableValue ${api.name}::WrapError(const FlutterError& error) {
\treturn EncodableValue(EncodableList{
\t\tEncodableValue(error.message()),
\t\tEncodableValue(error.code()),
\t\tEncodableValue(error.message()),
\t\terror.details()
\t});
}''');
Expand Down
2 changes: 1 addition & 1 deletion packages/pigeon/lib/generator_tools.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import 'ast.dart';
/// The current version of pigeon.
///
/// This must match the version in pubspec.yaml.
const String pigeonVersion = '9.0.7';
const String pigeonVersion = '9.1.0';

/// Read all the content from [stdin] to a String.
String readStdin() {
Expand Down
165 changes: 95 additions & 70 deletions packages/pigeon/lib/java_generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ class JavaGenerator extends StructuredGenerator<JavaOptions> {
indent.writeln(
'$_docCommentPrefix Generated class from Pigeon.$_docCommentSuffix');
indent.writeln(
'@SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression"})');
'@SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression", "serial"})');
if (generatorOptions.useGeneratedAnnotation ?? false) {
indent.writeln('@javax.annotation.Generated("dev.flutter.pigeon")');
}
Expand Down Expand Up @@ -606,46 +606,36 @@ class JavaGenerator extends StructuredGenerator<JavaOptions> {
: _javaTypeForDartType(method.returnType);
indent.writeln(
'ArrayList<Object> wrapped = new ArrayList<Object>();');
indent.write('try ');
indent.addScoped('{', '}', () {
final List<String> methodArgument = <String>[];
if (method.arguments.isNotEmpty) {
indent.writeln(
'ArrayList<Object> args = (ArrayList<Object>) message;');
indent.writeln('assert args != null;');
enumerate(method.arguments, (int index, NamedType arg) {
// The StandardMessageCodec can give us [Integer, Long] for
// a Dart 'int'. To keep things simple we just use 64bit
// longs in Pigeon with Java.
final bool isInt = arg.type.baseName == 'int';
final String argType =
isInt ? 'Number' : _javaTypeForDartType(arg.type);
final String argName = _getSafeArgumentName(index, arg);
final String argExpression = isInt
? '($argName == null) ? null : $argName.longValue()'
: argName;
String accessor = 'args.get($index)';
if (isEnum(root, arg.type)) {
accessor = _intToEnum(accessor, arg.type.baseName);
} else {
accessor = _cast(accessor, javaType: argType);
}
indent.writeln('$argType $argName = $accessor;');
if (!arg.type.isNullable) {
indent.write('if ($argName == null) ');
indent.addScoped('{', '}', () {
indent.writeln(
'throw new NullPointerException("$argName unexpectedly null.");');
});
}
methodArgument.add(argExpression);
});
}
if (method.isAsynchronous) {
final String resultValue =
method.returnType.isVoid ? 'null' : 'result';
const String resultName = 'resultCallback';
indent.format('''
final List<String> methodArgument = <String>[];
if (method.arguments.isNotEmpty) {
indent.writeln(
'ArrayList<Object> args = (ArrayList<Object>) message;');
enumerate(method.arguments, (int index, NamedType arg) {
// The StandardMessageCodec can give us [Integer, Long] for
// a Dart 'int'. To keep things simple we just use 64bit
// longs in Pigeon with Java.
final bool isInt = arg.type.baseName == 'int';
final String argType =
isInt ? 'Number' : _javaTypeForDartType(arg.type);
final String argName = _getSafeArgumentName(index, arg);
final String argExpression = isInt
? '($argName == null) ? null : $argName.longValue()'
: argName;
String accessor = 'args.get($index)';
if (isEnum(root, arg.type)) {
accessor = _intToEnum(accessor, arg.type.baseName);
} else if (argType != 'Object') {
accessor = _cast(accessor, javaType: argType);
}
indent.writeln('$argType $argName = $accessor;');
methodArgument.add(argExpression);
});
}
if (method.isAsynchronous) {
final String resultValue =
method.returnType.isVoid ? 'null' : 'result';
const String resultName = 'resultCallback';
indent.format('''
Result<$returnType> $resultName =
\t\tnew Result<$returnType>() {
\t\t\tpublic void success($returnType result) {
Expand All @@ -659,31 +649,33 @@ Result<$returnType> $resultName =
\t\t\t}
\t\t};
''');
methodArgument.add(resultName);
}
final String call =
'api.${method.name}(${methodArgument.join(', ')})';
if (method.isAsynchronous) {
indent.writeln('$call;');
} else if (method.returnType.isVoid) {
indent.writeln('$call;');
indent.writeln('wrapped.add(0, null);');
} else {
indent.writeln('$returnType output = $call;');
indent.writeln('wrapped.add(0, output);');
}
}, addTrailingNewline: false);
indent.add(' catch (Error | RuntimeException exception) ');
indent.addScoped('{', '}', () {
indent.writeln(
'ArrayList<Object> wrappedError = wrapError(exception);');
if (method.isAsynchronous) {
indent.writeln('reply.reply(wrappedError);');
} else {
indent.writeln('wrapped = wrappedError;');
}
});
if (!method.isAsynchronous) {
methodArgument.add(resultName);
}
final String call =
'api.${method.name}(${methodArgument.join(', ')})';
if (method.isAsynchronous) {
indent.writeln('$call;');
} else {
indent.write('try ');
indent.addScoped('{', '}', () {
if (method.returnType.isVoid) {
indent.writeln('$call;');
indent.writeln('wrapped.add(0, null);');
} else {
indent.writeln('$returnType output = $call;');
indent.writeln('wrapped.add(0, output);');
}
});
indent.add(' catch (Throwable exception) ');
indent.addScoped('{', '}', () {
indent.writeln(
'ArrayList<Object> wrappedError = wrapError(exception);');
if (method.isAsynchronous) {
indent.writeln('reply.reply(wrappedError);');
} else {
indent.writeln('wrapped = wrappedError;');
}
});
indent.writeln('reply.reply(wrapped);');
}
});
Expand Down Expand Up @@ -765,22 +757,55 @@ Result<$returnType> $resultName =
});
}

void _writeErrorClass(Indent indent) {
indent.writeln(
'/** Error class for passing custom error details to Flutter via a thrown PlatformException. */');
indent.write('public static class FlutterError extends RuntimeException ');
indent.addScoped('{', '}', () {
indent.newln();
indent.writeln('/** The error code. */');
indent.writeln('public final String code;');
indent.newln();
indent.writeln(
'/** The error details. Must be a datatype supported by the api codec. */');
indent.writeln('public final Object details;');
indent.newln();
indent.writeln(
'public FlutterError(@NonNull String code, @Nullable String message, @Nullable Object details) ');
indent.writeScoped('{', '}', () {
indent.writeln('super(message);');
indent.writeln('this.code = code;');
indent.writeln('this.details = details;');
});
});
}

void _writeWrapError(Indent indent) {
indent.format('''
@NonNull
private static ArrayList<Object> wrapError(@NonNull Throwable exception) {
\tArrayList<Object> errorList = new ArrayList<Object>(3);
\terrorList.add(exception.toString());
\terrorList.add(exception.getClass().getSimpleName());
\terrorList.add(
\t\t"Cause: " + exception.getCause() + ", Stacktrace: " + Log.getStackTraceString(exception));
\tif (exception instanceof FlutterError) {
\t\tFlutterError error = (FlutterError) exception;
\t\terrorList.add(error.code);
\t\terrorList.add(error.getMessage());
\t\terrorList.add(error.details);
\t} else {
\t\terrorList.add(exception.toString());
\t\terrorList.add(exception.getClass().getSimpleName());
\t\terrorList.add(
\t\t\t"Cause: " + exception.getCause() + ", Stacktrace: " + Log.getStackTraceString(exception));
\t}
\treturn errorList;
}''');
}

@override
void writeGeneralUtilities(
JavaOptions generatorOptions, Root root, Indent indent) {
indent.newln();
_writeErrorClass(indent);
indent.newln();
_writeWrapError(indent);
}

Expand Down
Loading