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 @@
// @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_registry.dart';
......@@ -21,15 +21,32 @@ import 'plugin_registry.dart';
/// [StandardMethodCodec] is used. If no [binaryMessenger] is provided, then
/// [pluginBinaryMessenger], which sends messages to the framework-side,
/// 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> {
/// Creates a new plugin event channel.
///
/// The [name] and [codec] arguments must not be null.
const PluginEventChannel(
this.name, [
this.codec = const StandardMethodCodec(),
BinaryMessenger binaryMessenger,
this.binaryMessenger,
]) : assert(name != null),
assert(codec != null),
_binaryMessenger = binaryMessenger;
assert(codec != null);
/// The logical channel on which communication happens.
///
......@@ -43,28 +60,52 @@ class PluginEventChannel<T> {
/// The messenger used by this channel to send platform messages.
///
/// This must not be null. If not provided, defaults to
/// [pluginBinaryMessenger], which sends messages from the platform-side
/// to the framework-side.
BinaryMessenger get binaryMessenger =>
_binaryMessenger ?? pluginBinaryMessenger;
final BinaryMessenger _binaryMessenger;
/// Set the stream controller for this event channel.
/// When this is null, the [pluginBinaryMessenger] is used instead,
/// which sends messages from the platform-side to the
/// framework-side.
final BinaryMessenger binaryMessenger;
/// Use [setController] instead.
///
/// 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) {
final _EventChannelHandler<T> handler = _EventChannelHandler<T>(
name,
codec,
controller,
binaryMessenger,
);
binaryMessenger.setMessageHandler(
name, controller == null ? null : handler.handle);
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>(
name,
codec,
controller,
messenger,
);
messenger.setMessageHandler(name, handler.handle);
}
}
}
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 MethodCodec codec;
......@@ -77,14 +118,15 @@ class _EventChannelHandler<T> {
final MethodCall call = codec.decodeMethodCall(message);
switch (call.method) {
case 'listen':
assert(call.arguments == null);
return _listen();
case 'cancel':
assert(call.arguments == null);
return _cancel();
}
return null;
}
// TODO(hterkelsen): Support arguments.
Future<ByteData> _listen() async {
if (subscription != null) {
await subscription.cancel();
......@@ -92,18 +134,17 @@ class _EventChannelHandler<T> {
subscription = controller.stream.listen((dynamic event) {
messenger.send(name, codec.encodeSuccessEnvelope(event));
}, onError: (dynamic error) {
messenger.send(name,
codec.encodeErrorEnvelope(code: 'error', message: error.toString()));
messenger.send(name, codec.encodeErrorEnvelope(code: 'error', message: '$error'));
});
return codec.encodeSuccessEnvelope(null);
}
// TODO(hterkelsen): Support arguments.
Future<ByteData> _cancel() async {
if (subscription == null) {
return codec.encodeErrorEnvelope(
code: 'error', message: 'No active stream to cancel.');
code: 'error',
message: 'No active subscription to cancel.',
);
}
await subscription.cancel();
subscription = null;
......
......@@ -13,8 +13,13 @@ import 'package:flutter/services.dart';
typedef _MessageHandler = Future<ByteData> Function(ByteData);
/// This class registers web platform plugins.
///
/// An instance of this class is available as [webPluginRegistry].
class PluginRegistry {
/// Creates a plugin registry.
///
/// The argument selects the [BinaryMessenger] to use. An
/// appropriate value would be [pluginBinaryMessenger].
PluginRegistry(this._binaryMessenger);
final BinaryMessenger _binaryMessenger;
......@@ -25,6 +30,17 @@ class PluginRegistry {
/// Registers this plugin handler with the engine, so that unrecognized
/// platform messages are forwarded to the registry, where they can be
/// 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() {
// The function below is only defined in the Web dart:ui.
// ignore: undefined_function
......@@ -46,22 +62,26 @@ class Registrar {
/// Use this [BinaryMessenger] when creating platform channels in order for
/// them to receive messages from the platform side. For example:
///
///
/// class MyPlugin {
/// static void registerWith(Registrar registrar) {
/// final MethodChannel channel = MethodChannel(
/// 'com.my_plugin/my_plugin',
/// const StandardMethodCodec(),
/// registrar.messenger);
/// final MyPlugin instance = MyPlugin();
/// channel.setMethodCallHandler(instance.handleMethodCall);
/// }
/// ...
/// }
/// ```dart
/// class MyPlugin {
/// static void registerWith(Registrar registrar) {
/// final MethodChannel channel = MethodChannel(
/// 'com.my_plugin/my_plugin',
/// const StandardMethodCodec(),
/// registrar.messenger,
/// );
/// final MyPlugin instance = MyPlugin();
/// channel.setMethodCallHandler(instance.handleMethodCall);
/// }
/// // ...
/// }
/// ```
final BinaryMessenger messenger;
}
/// The default plugin registry for the web.
///
/// Uses [pluginBinaryMessenger] as the [BinaryMessenger].
final PluginRegistry webPluginRegistry = PluginRegistry(pluginBinaryMessenger);
/// A [BinaryMessenger] which does the inverse of the default framework
......@@ -75,23 +95,23 @@ class _PlatformBinaryMessenger extends BinaryMessenger {
/// Receives a platform message from the framework.
@override
Future<void> handlePlatformMessage(String channel, ByteData data,
ui.PlatformMessageResponseCallback callback) async {
Future<void> handlePlatformMessage(
String channel,
ByteData data,
ui.PlatformMessageResponseCallback callback,
) async {
ByteData response;
try {
final MessageHandler handler = _handlers[channel];
if (handler != null) {
response = await handler(data);
} else {
ui.channelBuffers.push(channel, data, callback);
callback = null;
}
} catch (exception, stack) {
FlutterError.reportError(FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'flutter web shell',
context: ErrorDescription('during a plugin platform message call'),
library: 'flutter web plugins',
context: ErrorDescription('during a framework-to-plugin message'),
));
} finally {
if (callback != null) {
......@@ -111,7 +131,7 @@ class _PlatformBinaryMessenger extends BinaryMessenger {
FlutterError.reportError(FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'flutter web shell',
library: 'flutter web plugins',
context: ErrorDescription('during a plugin-to-framework message'),
));
}
......@@ -125,9 +145,6 @@ class _PlatformBinaryMessenger extends BinaryMessenger {
_handlers.remove(channel);
else
_handlers[channel] = handler;
ui.channelBuffers.drain(channel, (ByteData data, ui.PlatformMessageResponseCallback callback) async {
await handlePlatformMessage(channel, data, callback);
});
}
@override
......@@ -135,17 +152,24 @@ class _PlatformBinaryMessenger extends BinaryMessenger {
@override
void setMockMessageHandler(
String channel, Future<ByteData> Function(ByteData message) handler) {
String channel,
Future<ByteData> Function(ByteData message) handler,
) {
throw FlutterError(
'Setting mock handlers is not supported on the platform side.');
'Setting mock handlers is not supported on the platform side.',
);
}
@override
bool checkMockMessageHandler(String channel, MessageHandler handler) {
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();
......@@ -23,7 +23,7 @@ void main() {
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 PluginEventChannel<String> sendingChannel =
PluginEventChannel<String>('test');
......@@ -39,7 +39,23 @@ void main() {
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 PluginEventChannel<String> sendingChannel =
PluginEventChannel<String>('test2');
......@@ -56,7 +72,24 @@ void main() {
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 PluginEventChannel<String> sendingChannel =
PluginEventChannel<String>('test3');
......@@ -72,7 +105,23 @@ void main() {
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 PluginEventChannel<String> sendingChannel =
PluginEventChannel<String>('test4');
......@@ -92,5 +141,26 @@ void main() {
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