Commit 471c97df authored by Mikkel Nygaard Ravn's avatar Mikkel Nygaard Ravn Committed by GitHub

Platform channel API cleanup (#9048)

parent 0e43e581
......@@ -37,9 +37,12 @@ public class MainActivity extends FlutterActivity {
} else {
response.error("UNAVAILABLE", "Battery level not available.", null);
}
} else {
response.notImplemented();
}
}
});
}
);
}
private int getBatteryLevel() {
......
......@@ -13,8 +13,7 @@
(FlutterViewController*)self.window.rootViewController;
FlutterMethodChannel* batteryChannel = [FlutterMethodChannel
methodChannelWithName:@"battery"
binaryMessenger:controller
codec:[FlutterStandardMethodCodec sharedInstance]];
binaryMessenger:controller];
[batteryChannel setMethodCallHandler:^(FlutterMethodCall* call,
FlutterResultReceiver result) {
if ([@"getBatteryLevel" isEqualToString:call.method]) {
......@@ -25,15 +24,12 @@
message:@"Battery info unavailable"
details:nil]);
} else {
result([NSNumber numberWithInt:(int)(device.batteryLevel * 100)]);
result(@((int)(device.batteryLevel * 100)));
}
} else {
result([FlutterError errorWithCode:@"UNKNOWN_METHOD"
message:@"Unknown battery method called"
details:nil]);
result(FlutterMethodNotImplemented);
}
}];
return YES;
}
@end
......@@ -105,6 +105,8 @@ class MethodCall {
///
/// * [PlatformMethodChannel], which use [MethodCodec]s for communication
/// between Flutter and platform plugins.
/// * [PlatformEventChannel], which use [MethodCodec]s for communication
/// between Flutter and platform plugins.
abstract class MethodCodec {
/// Encodes the specified [methodCall] in binary.
ByteData encodeMethodCall(MethodCall methodCall);
......@@ -139,7 +141,7 @@ abstract class MethodCodec {
/// * [PlatformMethodChannel.invokeMethod], which completes the returned future
/// with a [PlatformException], if invoking the platform plugin method
/// results in an error envelope.
/// * [PlatformMethodChannel.receiveBroadcastStream], which emits
/// * [PlatformEventChannel.receiveBroadcastStream], which emits
/// [PlatformException]s as error events, whenever an event received from the
/// platform plugin is wrapped in an error envelope.
class PlatformException implements Exception {
......@@ -167,8 +169,22 @@ class PlatformException implements Exception {
String toString() => 'PlatformException($code, $message, $details)';
}
/// Thrown to indicate that a platform interaction failed to find the plugin.
/// Thrown to indicate that a platform interaction failed to find a handling
/// plugin.
///
/// See also:
///
/// * [PlatformMethodChannel.invokeMethod], which completes the returned future
/// with a [MissingPluginException], if no plugin handler for the method call
/// was found.
class MissingPluginException implements Exception {
/// Creates a [MissingPluginException] with an optional human-readable
/// error message.
MissingPluginException([this.message]);
/// A human-readable error message, possibly `null`.
final String message;
@override
String toString() => 'MissingPluginException';
String toString() => 'MissingPluginException($message)';
}
......@@ -120,8 +120,6 @@ class JSONMethodCodec implements MethodCodec {
@override
dynamic decodeEnvelope(ByteData envelope) {
if (envelope == null)
throw new MissingPluginException();
final dynamic decoded = const JSONMessageCodec().decodeMessage(envelope);
if (decoded is! List)
throw new FormatException('Expected envelope List, got $decoded');
......@@ -461,8 +459,6 @@ class StandardMethodCodec implements MethodCodec {
@override
dynamic decodeEnvelope(ByteData envelope) {
if (envelope == null)
throw new MissingPluginException();
// First byte is zero in success case, and non-zero otherwise.
if (envelope.lengthInBytes == 0)
throw const FormatException('Expected envelope, got nothing');
......
......@@ -90,7 +90,7 @@ class PlatformMessageChannel<T> {
}
/// A named channel for communicating with platform plugins using asynchronous
/// method calls and event streams.
/// method calls.
///
/// Method calls are encoded into binary before being sent, and binary results
/// received are decoded into Dart values. The [MethodCodec] used must be
......@@ -125,12 +125,16 @@ class PlatformMethodChannel {
/// * a result (possibly `null`), on successful invocation;
/// * a [PlatformException], if the invocation failed in the platform plugin;
/// * a [FormatException], if encoding or decoding failed.
/// * a [MissingPluginException], if the method has not been implemented.
Future<dynamic> invokeMethod(String method, [dynamic arguments]) async {
assert(method != null);
return codec.decodeEnvelope(await PlatformMessages.sendBinary(
final dynamic result = await PlatformMessages.sendBinary(
name,
codec.encodeMethodCall(new MethodCall(method, arguments)),
));
);
if (result == null)
throw new MissingPluginException("No implementation found for method $method on channel $name");
return codec.decodeEnvelope(result);
}
/// Sets a callback for receiving method calls on this channel.
......@@ -143,7 +147,9 @@ class PlatformMethodChannel {
/// is sent back to the platform plugin caller wrapped in a success envelope
/// as defined by the [codec] of this channel. If the future completes with
/// a [PlatformException], the fields of that exception will be used to
/// populate an error envelope which is sent back instead.
/// populate an error envelope which is sent back instead. If the future
/// completes with a [MissingPluginException], an empty reply is sent
/// similarly to what happens if no method call handler has been set.
void setMethodCallHandler(Future<dynamic> handler(MethodCall call)) {
if (handler == null) {
PlatformMessages.setBinaryMessageHandler(name, null);
......@@ -158,6 +164,8 @@ class PlatformMethodChannel {
} on PlatformException catch (e) {
return codec.encodeErrorEnvelope(
code: e.code, message: e.message, details: e.details);
} on MissingPluginException {
return null;
}
},
);
......@@ -170,9 +178,10 @@ class PlatformMethodChannel {
/// this channel, if any. To remove the mock handler, pass `null` as the
/// `handler` argument.
///
/// If the future returned by the handler completes with a result, that value
/// is used as the result of the method invocation. If the future completes
/// with a [PlatformException], that will be thrown instead.
/// Later calls to [invokeMethod] will result in a successful result,
/// a [PlatformException] or a [MissingPluginException], determined by how
/// the future returned by the mock callback completes. The [codec] of this
/// channel is used to encode and decode values and errors.
///
/// This is intended for testing. Method calls intercepted in this manner are
/// not sent to platform plugins.
......@@ -190,11 +199,61 @@ class PlatformMethodChannel {
} on PlatformException catch (e) {
return codec.encodeErrorEnvelope(
code: e.code, message: e.message, details: e.details);
} on MissingPluginException {
return null;
}
},
);
}
}
}
/// A [PlatformMethodChannel] that ignores missing platform plugins.
///
/// When [invokeMethod] fails to find the platform plugin, it returns null
/// instead of throwing an exception.
class OptionalPlatformMethodChannel extends PlatformMethodChannel {
/// Creates a [PlatformMethodChannel] that ignores missing platform plugins.
const OptionalPlatformMethodChannel(String name, [MethodCodec codec = const StandardMethodCodec()])
: super(name, codec);
@override
Future<dynamic> invokeMethod(String method, [dynamic arguments]) async {
try {
return await super.invokeMethod(method, arguments);
} on MissingPluginException {
return null;
}
}
}
/// A named channel for communicating with platform plugins using event streams.
///
/// Stream setup requests are encoded into binary before being sent,
/// and binary events received are decoded into Dart values. The [MethodCodec]
/// used must be compatible with the one used by the platform plugin. This can
/// be achieved by creating a FlutterEventChannel counterpart of this channel on
/// the platform side. The Dart type of events sent and received is `dynamic`,
/// but only values supported by the specified [MethodCodec] can be used.
///
/// The identity of the channel is given by its name, so other uses of that name
/// with may interfere with this channel's communication.
///
/// See: <https://flutter.io/platform-channels/>
class PlatformEventChannel {
/// Creates a [PlatformEventChannel] with the specified [name].
///
/// The [codec] used will be [StandardMethodCodec], unless otherwise
/// specified.
///
/// Neither [name] nor [codec] may be `null`.
const PlatformEventChannel(this.name, [this.codec = const StandardMethodCodec()]);
/// The logical channel on which communication happens, not `null`.
final String name;
/// The message codec used by this channel, not `null`.
final MethodCodec codec;
/// Sets up a broadcast stream for receiving events on this channel.
///
......@@ -206,15 +265,15 @@ class PlatformMethodChannel {
/// received from the platform plugin;
/// * an error event containing a [FormatException] for each event received
/// where decoding fails;
/// * an error event containing a [PlatformException] or [FormatException]
/// whenever stream setup fails (stream setup is done only when listener
/// count changes from 0 to 1).
/// * an error event containing a [PlatformException],
/// [MissingPluginException], or [FormatException] whenever stream setup fails
/// (stream setup is done only when listener count changes from 0 to 1).
///
/// Notes for platform plugin implementers:
///
/// Plugins must expose methods named `listen` and `cancel` suitable for
/// invocations by [invokeMethod]. Both methods are invoked with the specified
/// [arguments].
/// invocations by [PlatformMethodChannel.invokeMethod]. Both methods are
/// invoked with the specified [arguments].
///
/// Following the semantics of broadcast streams, `listen` will be called as
/// the first listener registers with the returned stream, and `cancel` when
......@@ -222,6 +281,7 @@ class PlatformMethodChannel {
/// indefinitely. Platform plugins should consume no stream-related resources
/// while listener count is zero.
Stream<dynamic> receiveBroadcastStream([dynamic arguments]) {
final PlatformMethodChannel methodChannel = new PlatformMethodChannel(name, codec);
StreamController<dynamic> controller;
controller = new StreamController<dynamic>.broadcast(
onListen: () async {
......@@ -239,7 +299,7 @@ class PlatformMethodChannel {
}
);
try {
await invokeMethod('listen', arguments);
await methodChannel.invokeMethod('listen', arguments);
} catch (e) {
PlatformMessages.setBinaryMessageHandler(name, null);
controller.addError(e);
......@@ -247,7 +307,7 @@ class PlatformMethodChannel {
}, onCancel: () async {
PlatformMessages.setBinaryMessageHandler(name, null);
try {
await invokeMethod('cancel', arguments);
await methodChannel.invokeMethod('cancel', arguments);
} catch (exception, stack) {
FlutterError.reportError(new FlutterErrorDetails(
exception: exception,
......@@ -261,22 +321,3 @@ class PlatformMethodChannel {
return controller.stream;
}
}
/// A [PlatformMethodChannel] that ignores missing platform plugins.
///
/// When [invokeMethod] fails to find the platform plugin, it returns null
/// instead of throwing an exception.
class OptionalPlatformMethodChannel extends PlatformMethodChannel {
/// Creates a [PlatformMethodChannel] that ignores missing platform plugins.
const OptionalPlatformMethodChannel(String name, [MethodCodec codec = const StandardMethodCodec()])
: super(name, codec);
@override
Future<dynamic> invokeMethod(String method, [dynamic arguments]) async {
try {
return await super.invokeMethod(method, arguments);
} on MissingPluginException {
return null;
}
}
}
......@@ -261,6 +261,8 @@ class _TextInputClientHandler {
case 'TextInputClient.performAction':
_currentConnection._client.performAction(_toTextInputAction(args[1]));
break;
default:
throw new MissingPluginException();
}
}
......
......@@ -37,10 +37,10 @@ void main() {
group('PlatformMethodChannel', () {
const MessageCodec<dynamic> jsonMessage = const JSONMessageCodec();
const MethodCodec jsonMethod = const JSONMethodCodec();
const PlatformMethodChannel channel = const PlatformMethodChannel('ch', jsonMethod);
const PlatformMethodChannel channel = const PlatformMethodChannel('ch7', jsonMethod);
test('can invoke method and get result', () async {
PlatformMessages.setMockBinaryMessageHandler(
'ch',
'ch7',
(ByteData message) async {
final Map<dynamic, dynamic> methodCall = jsonMessage.decodeMessage(message);
if (methodCall['method'] == 'sayHello')
......@@ -54,11 +54,11 @@ void main() {
});
test('can invoke method and get error', () async {
PlatformMessages.setMockBinaryMessageHandler(
'ch',
'ch7',
(ByteData message) async {
return jsonMessage.encodeMessage(<dynamic>[
'unknown',
'Method not understood',
'bad',
'Something happened',
<String, dynamic>{'a': 42, 'b': 3.14},
]);
},
......@@ -67,11 +67,33 @@ void main() {
await channel.invokeMethod('sayHello', 'hello');
fail('Exception expected');
} on PlatformException catch(e) {
expect(e.code, equals('unknown'));
expect(e.message, equals('Method not understood'));
expect(e.code, equals('bad'));
expect(e.message, equals('Something happened'));
expect(e.details, equals(<String, dynamic>{'a': 42, 'b': 3.14}));
} catch(e) {
fail('PlatformException expected');
}
});
test('can invoke unimplemented method', () async {
PlatformMessages.setMockBinaryMessageHandler(
'ch7',
(ByteData message) async => null,
);
try {
await channel.invokeMethod('sayHello', 'hello');
fail('Exception expected');
} on MissingPluginException catch(e) {
expect(e.message, contains('sayHello'));
expect(e.message, contains('ch7'));
} catch(e) {
fail('MissingPluginException expected');
}
});
});
group('PlatformEventChannel', () {
const MessageCodec<dynamic> jsonMessage = const JSONMessageCodec();
const MethodCodec jsonMethod = const JSONMethodCodec();
const PlatformEventChannel channel = const PlatformEventChannel('ch', jsonMethod);
test('can receive event stream', () async {
void emitEvent(dynamic event) {
PlatformMessages.handlePlatformMessage(
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment