I’ve just figured out Flutter error handling for method channels communicating with native code and wanted to capture the technique. This is something that I missed despite a fair amount of reading on plugin integration, so others might have missed it as well. There’s a very good post, Flutter Platform Channels, where I got much of this information, definitely worth a read. There are some details that it doesn’t cover, particularly about testing, and it’s not just focused on error handling, so I thought I’d boost the signal on that.
Error structure
An error communicated across a method channel is defined as three properties:
- Code: A string error code used in error handling code to differentiate the behaviour.
- Message: A string intended for human consumption, gives information about the error.
- Details: A string, for slightly more technical human consumption, that provides more detailed information about the exception.
Dart
In Dart an error is represented as a PlatformException. When invoking a method on a MethodChannel a PlatformException is thrown if the error encoding is detected in a result. The Swift and Kotlin sections that follow show how to send an error encoding as the result of a method invocation. Here’s an example of handling an exception.
MethodChannel channel = MethodChannel( 'my_channel_name' );
try {
await channel.invokeMethod( 'myMethod', 10 );
} on PlatformException catch(e) {
if ( e.code == "someKnownError" ) {
// handle it
}
}
In unit tests it’s easy to test error handling conditions by throwing this exception from the channel method handler. Here’s an example:
setUp(() {
channel.setMockMethodCallHandler((MethodCall methodCall) async {
if (methodCall.method == "myMethod") {
int max = methodCall.arguments
if ( max <= 0 ) {
throw PlatformException(
code: "missingOrInvalidArg",
message: "max must be >= 0");
}
// otherwise return the expected result
...
}
});
});
Swift
In Swift an error is sent to the result function as a FlutterError object. The handle method of a Swift plugin is given the function result: FlutterResult as a parameter. If the method invocation is successful then that result method is invoked with whatever a successful return looks like, could just be a string like: result( “some useful data”). If there’s an error while handling a method the same result method is used, but with a Flutter error object as shown here:
FlutterError( code: String, message: String, details: String )
switch call.method {
case "myMethod":
guard let max = call.arguments as? Int else {
result(
FlutterError( code: "invalidArgs",
message: "Missing arg max",
details: "Expected 1 Int arg." ))
return
}
myMethod( max, result )
default:
result( FlutterMethodNotImplemented)
}
Note that there is a constant, FlutterMethodNotImplemented, that should be used for the special case of an unimplemented method. It is shown above in the default handler.
Kotlin
In Kotlin error handling is a bit cleaner because there is an explicit error method on the FlutterResult class.
/**
* Handles an error result.
*
* @param errorCode An error code String.
* @param errorMessage A human-readable error message String, possibly null.
* @param errorDetails Error details, possibly null
*/
@UiThreadvoid error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails);
if (call.method == "myMethod") {
if ( null != call.arguments && call.arguments is Integer ) {
max = call.arguments as Integer
myMethod( max, result )
}
else {
result.error( "invalidArgs", "Missing arg max", "Expected 1 Integer arg."
}
}