Unverified Commit 8d923bf9 authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

flutter_web_plugins cleanup and documentation (#67164)

parent d4112424
...@@ -4,5 +4,18 @@ ...@@ -4,5 +4,18 @@
// @dart = 2.8 // @dart = 2.8
/// The platform channels and plugin registry implementations for
/// the web implementations of Flutter plugins.
///
/// This library provides the [Registrar] class, which is used in the
/// `registerWith` method that is itself called by the code generated
/// by the `flutter` tool for web applications.
///
/// See also:
///
/// * [How to Write a Flutter Web Plugin](https://medium.com/flutter/how-to-write-a-flutter-web-plugin-5e26c689ea1), a Medium article
/// describing how the [url_launcher] package was created using [flutter_web_plugins].
library flutter_web_plugins;
export 'src/plugin_event_channel.dart'; export 'src/plugin_event_channel.dart';
export 'src/plugin_registry.dart'; export 'src/plugin_registry.dart';
...@@ -21,15 +21,32 @@ import 'plugin_registry.dart'; ...@@ -21,15 +21,32 @@ import 'plugin_registry.dart';
/// [StandardMethodCodec] is used. If no [binaryMessenger] is provided, then /// [StandardMethodCodec] is used. If no [binaryMessenger] is provided, then
/// [pluginBinaryMessenger], which sends messages to the framework-side, /// [pluginBinaryMessenger], which sends messages to the framework-side,
/// is used. /// is used.
///
/// Channels created using this class implement two methods for
/// subscribing to the event stream. The methods use the encoding of
/// the specified [codec].
///
/// The first method is `listen`. When called, it begins forwarding
/// messages to the framework side when they are added to the
/// [controller]. This triggers the [onListen] callback on the
/// [controller].
///
/// The other method is `cancel`. When called, it stops forwarding
/// events to the framework. This triggers the [onCancel] callback on
/// the [controller].
///
/// Events added to the [controller] when the framework is not
/// subscribed are silently discarded.
class PluginEventChannel<T> { class PluginEventChannel<T> {
/// Creates a new plugin event channel. /// Creates a new plugin event channel.
///
/// The [name] and [codec] arguments must not be null.
const PluginEventChannel( const PluginEventChannel(
this.name, [ this.name, [
this.codec = const StandardMethodCodec(), this.codec = const StandardMethodCodec(),
BinaryMessenger binaryMessenger, this.binaryMessenger,
]) : assert(name != null), ]) : assert(name != null),
assert(codec != null), assert(codec != null);
_binaryMessenger = binaryMessenger;
/// The logical channel on which communication happens. /// The logical channel on which communication happens.
/// ///
...@@ -43,28 +60,52 @@ class PluginEventChannel<T> { ...@@ -43,28 +60,52 @@ class PluginEventChannel<T> {
/// The messenger used by this channel to send platform messages. /// The messenger used by this channel to send platform messages.
/// ///
/// This must not be null. If not provided, defaults to /// When this is null, the [pluginBinaryMessenger] is used instead,
/// [pluginBinaryMessenger], which sends messages from the platform-side /// which sends messages from the platform-side to the
/// to the framework-side. /// framework-side.
BinaryMessenger get binaryMessenger => final BinaryMessenger binaryMessenger;
_binaryMessenger ?? pluginBinaryMessenger;
final BinaryMessenger _binaryMessenger; /// Use [setController] instead.
///
/// Set the stream controller for this event channel. /// This setter is deprecated because it has no corresponding getter,
/// and providing a getter would require making this class non-const.
@Deprecated(
'Replace calls to the "controller" setter with calls to the "setController" method. '
'This feature was deprecated after v1.23.0-7.0.pre.'
)
set controller(StreamController<T> controller) { set controller(StreamController<T> controller) {
setController(controller);
}
/// Changes the stream controller for this event channel.
///
/// Setting the controller to null disconnects from the channel (setting
/// the message handler on the [binaryMessenger] to null).
void setController(StreamController<T> controller) {
final BinaryMessenger messenger = binaryMessenger ?? pluginBinaryMessenger;
if (controller == null) {
messenger.setMessageHandler(name, null);
} else {
// The handler object is kept alive via its handle() method
// keeping a reference to itself. Ideally we would keep a
// reference to it so that there was a clear ownership model,
// but that would require making this class non-const. Having
// this class be const is convenient since it allows references
// to be obtained by using the constructor rather than having
// to literally pass references around.
final _EventChannelHandler<T> handler = _EventChannelHandler<T>( final _EventChannelHandler<T> handler = _EventChannelHandler<T>(
name, name,
codec, codec,
controller, controller,
binaryMessenger, messenger,
); );
binaryMessenger.setMessageHandler( messenger.setMessageHandler(name, handler.handle);
name, controller == null ? null : handler.handle); }
} }
} }
class _EventChannelHandler<T> { class _EventChannelHandler<T> {
_EventChannelHandler(this.name, this.codec, this.controller, this.messenger); _EventChannelHandler(this.name, this.codec, this.controller, this.messenger) : assert(messenger != null);
final String name; final String name;
final MethodCodec codec; final MethodCodec codec;
...@@ -77,14 +118,15 @@ class _EventChannelHandler<T> { ...@@ -77,14 +118,15 @@ class _EventChannelHandler<T> {
final MethodCall call = codec.decodeMethodCall(message); final MethodCall call = codec.decodeMethodCall(message);
switch (call.method) { switch (call.method) {
case 'listen': case 'listen':
assert(call.arguments == null);
return _listen(); return _listen();
case 'cancel': case 'cancel':
assert(call.arguments == null);
return _cancel(); return _cancel();
} }
return null; return null;
} }
// TODO(hterkelsen): Support arguments.
Future<ByteData> _listen() async { Future<ByteData> _listen() async {
if (subscription != null) { if (subscription != null) {
await subscription.cancel(); await subscription.cancel();
...@@ -92,18 +134,17 @@ class _EventChannelHandler<T> { ...@@ -92,18 +134,17 @@ class _EventChannelHandler<T> {
subscription = controller.stream.listen((dynamic event) { subscription = controller.stream.listen((dynamic event) {
messenger.send(name, codec.encodeSuccessEnvelope(event)); messenger.send(name, codec.encodeSuccessEnvelope(event));
}, onError: (dynamic error) { }, onError: (dynamic error) {
messenger.send(name, messenger.send(name, codec.encodeErrorEnvelope(code: 'error', message: '$error'));
codec.encodeErrorEnvelope(code: 'error', message: error.toString()));
}); });
return codec.encodeSuccessEnvelope(null); return codec.encodeSuccessEnvelope(null);
} }
// TODO(hterkelsen): Support arguments.
Future<ByteData> _cancel() async { Future<ByteData> _cancel() async {
if (subscription == null) { if (subscription == null) {
return codec.encodeErrorEnvelope( return codec.encodeErrorEnvelope(
code: 'error', message: 'No active stream to cancel.'); code: 'error',
message: 'No active subscription to cancel.',
);
} }
await subscription.cancel(); await subscription.cancel();
subscription = null; subscription = null;
......
...@@ -13,8 +13,13 @@ import 'package:flutter/services.dart'; ...@@ -13,8 +13,13 @@ import 'package:flutter/services.dart';
typedef _MessageHandler = Future<ByteData> Function(ByteData); typedef _MessageHandler = Future<ByteData> Function(ByteData);
/// This class registers web platform plugins. /// This class registers web platform plugins.
///
/// An instance of this class is available as [webPluginRegistry].
class PluginRegistry { class PluginRegistry {
/// Creates a plugin registry. /// Creates a plugin registry.
///
/// The argument selects the [BinaryMessenger] to use. An
/// appropriate value would be [pluginBinaryMessenger].
PluginRegistry(this._binaryMessenger); PluginRegistry(this._binaryMessenger);
final BinaryMessenger _binaryMessenger; final BinaryMessenger _binaryMessenger;
...@@ -25,6 +30,17 @@ class PluginRegistry { ...@@ -25,6 +30,17 @@ class PluginRegistry {
/// Registers this plugin handler with the engine, so that unrecognized /// Registers this plugin handler with the engine, so that unrecognized
/// platform messages are forwarded to the registry, where they can be /// platform messages are forwarded to the registry, where they can be
/// correctly dispatched to one of the registered plugins. /// correctly dispatched to one of the registered plugins.
///
/// Code generated by the `flutter` tool automatically calls this method
/// for the global [webPluginRegistry] at startup.
///
/// Only one [PluginRegistry] can be registered at a time. Calling this
/// method a second time silently unregisters the first [PluginRegistry]
/// and replaces it with the new one.
///
/// This method uses a function called `webOnlySetPluginHandler` in
/// the [dart:ui] library. That function is only available when
/// compiling for the web.
void registerMessageHandler() { void registerMessageHandler() {
// The function below is only defined in the Web dart:ui. // The function below is only defined in the Web dart:ui.
// ignore: undefined_function // ignore: undefined_function
...@@ -46,22 +62,26 @@ class Registrar { ...@@ -46,22 +62,26 @@ class Registrar {
/// Use this [BinaryMessenger] when creating platform channels in order for /// Use this [BinaryMessenger] when creating platform channels in order for
/// them to receive messages from the platform side. For example: /// them to receive messages from the platform side. For example:
/// ///
/// /// ```dart
/// class MyPlugin { /// class MyPlugin {
/// static void registerWith(Registrar registrar) { /// static void registerWith(Registrar registrar) {
/// final MethodChannel channel = MethodChannel( /// final MethodChannel channel = MethodChannel(
/// 'com.my_plugin/my_plugin', /// 'com.my_plugin/my_plugin',
/// const StandardMethodCodec(), /// const StandardMethodCodec(),
/// registrar.messenger); /// registrar.messenger,
/// );
/// final MyPlugin instance = MyPlugin(); /// final MyPlugin instance = MyPlugin();
/// channel.setMethodCallHandler(instance.handleMethodCall); /// channel.setMethodCallHandler(instance.handleMethodCall);
/// } /// }
/// ... /// // ...
/// } /// }
/// ```
final BinaryMessenger messenger; final BinaryMessenger messenger;
} }
/// The default plugin registry for the web. /// The default plugin registry for the web.
///
/// Uses [pluginBinaryMessenger] as the [BinaryMessenger].
final PluginRegistry webPluginRegistry = PluginRegistry(pluginBinaryMessenger); final PluginRegistry webPluginRegistry = PluginRegistry(pluginBinaryMessenger);
/// A [BinaryMessenger] which does the inverse of the default framework /// A [BinaryMessenger] which does the inverse of the default framework
...@@ -75,23 +95,23 @@ class _PlatformBinaryMessenger extends BinaryMessenger { ...@@ -75,23 +95,23 @@ class _PlatformBinaryMessenger extends BinaryMessenger {
/// Receives a platform message from the framework. /// Receives a platform message from the framework.
@override @override
Future<void> handlePlatformMessage(String channel, ByteData data, Future<void> handlePlatformMessage(
ui.PlatformMessageResponseCallback callback) async { String channel,
ByteData data,
ui.PlatformMessageResponseCallback callback,
) async {
ByteData response; ByteData response;
try { try {
final MessageHandler handler = _handlers[channel]; final MessageHandler handler = _handlers[channel];
if (handler != null) { if (handler != null) {
response = await handler(data); response = await handler(data);
} else {
ui.channelBuffers.push(channel, data, callback);
callback = null;
} }
} catch (exception, stack) { } catch (exception, stack) {
FlutterError.reportError(FlutterErrorDetails( FlutterError.reportError(FlutterErrorDetails(
exception: exception, exception: exception,
stack: stack, stack: stack,
library: 'flutter web shell', library: 'flutter web plugins',
context: ErrorDescription('during a plugin platform message call'), context: ErrorDescription('during a framework-to-plugin message'),
)); ));
} finally { } finally {
if (callback != null) { if (callback != null) {
...@@ -111,7 +131,7 @@ class _PlatformBinaryMessenger extends BinaryMessenger { ...@@ -111,7 +131,7 @@ class _PlatformBinaryMessenger extends BinaryMessenger {
FlutterError.reportError(FlutterErrorDetails( FlutterError.reportError(FlutterErrorDetails(
exception: exception, exception: exception,
stack: stack, stack: stack,
library: 'flutter web shell', library: 'flutter web plugins',
context: ErrorDescription('during a plugin-to-framework message'), context: ErrorDescription('during a plugin-to-framework message'),
)); ));
} }
...@@ -125,9 +145,6 @@ class _PlatformBinaryMessenger extends BinaryMessenger { ...@@ -125,9 +145,6 @@ class _PlatformBinaryMessenger extends BinaryMessenger {
_handlers.remove(channel); _handlers.remove(channel);
else else
_handlers[channel] = handler; _handlers[channel] = handler;
ui.channelBuffers.drain(channel, (ByteData data, ui.PlatformMessageResponseCallback callback) async {
await handlePlatformMessage(channel, data, callback);
});
} }
@override @override
...@@ -135,17 +152,24 @@ class _PlatformBinaryMessenger extends BinaryMessenger { ...@@ -135,17 +152,24 @@ class _PlatformBinaryMessenger extends BinaryMessenger {
@override @override
void setMockMessageHandler( void setMockMessageHandler(
String channel, Future<ByteData> Function(ByteData message) handler) { String channel,
Future<ByteData> Function(ByteData message) handler,
) {
throw FlutterError( throw FlutterError(
'Setting mock handlers is not supported on the platform side.'); 'Setting mock handlers is not supported on the platform side.',
);
} }
@override @override
bool checkMockMessageHandler(String channel, MessageHandler handler) { bool checkMockMessageHandler(String channel, MessageHandler handler) {
throw FlutterError( throw FlutterError(
'Setting mock handlers is not supported on the platform side.'); 'Setting mock handlers is not supported on the platform side.',
);
} }
} }
/// The default [BinaryMessenger] for Flutter Web plugins. /// The default [BinaryMessenger] for Flutter web plugins.
///
/// This is the value used for [webPluginRegistry]'s [PluginRegistry]
/// constructor argument.
final BinaryMessenger pluginBinaryMessenger = _PlatformBinaryMessenger(); final BinaryMessenger pluginBinaryMessenger = _PlatformBinaryMessenger();
...@@ -23,7 +23,7 @@ void main() { ...@@ -23,7 +23,7 @@ void main() {
webPluginRegistry.registerMessageHandler(); webPluginRegistry.registerMessageHandler();
}); });
test('can send events to an $EventChannel', () async { test('can send events to an $EventChannel (deprecated API)', () async {
const EventChannel listeningChannel = EventChannel('test'); const EventChannel listeningChannel = EventChannel('test');
const PluginEventChannel<String> sendingChannel = const PluginEventChannel<String> sendingChannel =
PluginEventChannel<String>('test'); PluginEventChannel<String>('test');
...@@ -39,7 +39,23 @@ void main() { ...@@ -39,7 +39,23 @@ void main() {
await controller.close(); await controller.close();
}); });
test('can send errors to an $EventChannel', () async { test('can send events to an $EventChannel', () async {
const EventChannel listeningChannel = EventChannel('test');
const PluginEventChannel<String> sendingChannel =
PluginEventChannel<String>('test');
final StreamController<String> controller = StreamController<String>();
sendingChannel.setController(controller);
expect(listeningChannel.receiveBroadcastStream(),
emitsInOrder(<String>['hello', 'world']));
controller.add('hello');
controller.add('world');
await controller.close();
});
test('can send errors to an $EventChannel (deprecated API)', () async {
const EventChannel listeningChannel = EventChannel('test2'); const EventChannel listeningChannel = EventChannel('test2');
const PluginEventChannel<String> sendingChannel = const PluginEventChannel<String> sendingChannel =
PluginEventChannel<String>('test2'); PluginEventChannel<String>('test2');
...@@ -56,7 +72,24 @@ void main() { ...@@ -56,7 +72,24 @@ void main() {
await controller.close(); await controller.close();
}); });
test('receives a listen event', () async { test('can send errors to an $EventChannel', () async {
const EventChannel listeningChannel = EventChannel('test2');
const PluginEventChannel<String> sendingChannel =
PluginEventChannel<String>('test2');
final StreamController<String> controller = StreamController<String>();
sendingChannel.setController(controller);
expect(
listeningChannel.receiveBroadcastStream(),
emitsError(predicate<dynamic>((dynamic e) =>
e is PlatformException && e.message == 'Test error')));
controller.addError('Test error');
await controller.close();
});
test('receives a listen event (deprecated API)', () async {
const EventChannel listeningChannel = EventChannel('test3'); const EventChannel listeningChannel = EventChannel('test3');
const PluginEventChannel<String> sendingChannel = const PluginEventChannel<String> sendingChannel =
PluginEventChannel<String>('test3'); PluginEventChannel<String>('test3');
...@@ -72,7 +105,23 @@ void main() { ...@@ -72,7 +105,23 @@ void main() {
await controller.close(); await controller.close();
}); });
test('receives a cancel event', () async { test('receives a listen event', () async {
const EventChannel listeningChannel = EventChannel('test3');
const PluginEventChannel<String> sendingChannel =
PluginEventChannel<String>('test3');
final StreamController<String> controller = StreamController<String>(
onListen: expectAsync0<void>(() {}, count: 1));
sendingChannel.setController(controller);
expect(listeningChannel.receiveBroadcastStream(),
emitsInOrder(<String>['hello']));
controller.add('hello');
await controller.close();
});
test('receives a cancel event (deprecated API)', () async {
const EventChannel listeningChannel = EventChannel('test4'); const EventChannel listeningChannel = EventChannel('test4');
const PluginEventChannel<String> sendingChannel = const PluginEventChannel<String> sendingChannel =
PluginEventChannel<String>('test4'); PluginEventChannel<String>('test4');
...@@ -92,5 +141,26 @@ void main() { ...@@ -92,5 +141,26 @@ void main() {
controller.add('hello'); controller.add('hello');
}); });
test('receives a cancel event', () async {
const EventChannel listeningChannel = EventChannel('test4');
const PluginEventChannel<String> sendingChannel =
PluginEventChannel<String>('test4');
final StreamController<String> controller =
StreamController<String>(onCancel: expectAsync0<void>(() {}));
sendingChannel.setController(controller);
final Stream<dynamic> eventStream =
listeningChannel.receiveBroadcastStream();
StreamSubscription<dynamic> subscription;
subscription =
eventStream.listen(expectAsync1<void, dynamic>((dynamic x) {
expect(x, equals('hello'));
subscription.cancel();
}));
controller.add('hello');
});
}); });
} }
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