Allow sending platform messages from plugins to the framework and implement EventChannel (#40638) * Allow sending messages from the platform to the framework * Add ability to send messages from the plugin to the platform. Also adds [PluginEventChannel] which is the plugin counterpart to EventChannel. Fixes https://github.com/flutter/flutter/issues/39981 * fix analyzer errors * Enhance doc comments * Remove dead code in test
diff --git a/packages/flutter_web_plugins/lib/flutter_web_plugins.dart b/packages/flutter_web_plugins/lib/flutter_web_plugins.dart index 626e74c..fdf0627 100644 --- a/packages/flutter_web_plugins/lib/flutter_web_plugins.dart +++ b/packages/flutter_web_plugins/lib/flutter_web_plugins.dart
@@ -2,4 +2,5 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +export 'src/plugin_event_channel.dart'; export 'src/plugin_registry.dart';
diff --git a/packages/flutter_web_plugins/lib/src/plugin_event_channel.dart b/packages/flutter_web_plugins/lib/src/plugin_event_channel.dart new file mode 100644 index 0000000..68645b2 --- /dev/null +++ b/packages/flutter_web_plugins/lib/src/plugin_event_channel.dart
@@ -0,0 +1,110 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/services.dart'; + +import 'plugin_registry.dart'; + +/// A named channel for sending events to the framework-side using streams. +/// +/// This is the platform-side equivalent of [EventChannel]. Whereas +/// [EventChannel] receives a stream of events from platform plugins, this +/// channel sends a stream of events to the handler listening on the +/// framework-side. +/// +/// The channel [name] must not be null. If no [codec] is provided, then +/// [StandardMethodCodec] is used. If no [binaryMessenger] is provided, then +/// [pluginBinaryMessenger], which sends messages to the framework-side, +/// is used. +class PluginEventChannel<T> { + /// Creates a new plugin event channel. + const PluginEventChannel( + this.name, [ + this.codec = const StandardMethodCodec(), + BinaryMessenger binaryMessenger, + ]) : assert(name != null), + assert(codec != null), + _binaryMessenger = binaryMessenger; + + /// The logical channel on which communication happens. + /// + /// This must not be null. + final String name; + + /// The message codec used by this channel. + /// + /// This must not be null. This defaults to [StandardMethodCodec]. + final MethodCodec codec; + + /// 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. + set controller(StreamController<T> controller) { + final _EventChannelHandler<T> handler = _EventChannelHandler<T>( + name, + codec, + controller, + binaryMessenger, + ); + binaryMessenger.setMessageHandler( + name, controller == null ? null : handler.handle); + } +} + +class _EventChannelHandler<T> { + _EventChannelHandler(this.name, this.codec, this.controller, this.messenger); + + final String name; + final MethodCodec codec; + final StreamController<T> controller; + final BinaryMessenger messenger; + + StreamSubscription<T> subscription; + + Future<ByteData> handle(ByteData message) { + final MethodCall call = codec.decodeMethodCall(message); + switch (call.method) { + case 'listen': + return _listen(); + case 'cancel': + return _cancel(); + } + return null; + } + + // TODO(hterkelsen): Support arguments. + Future<ByteData> _listen() async { + if (subscription != null) { + await subscription.cancel(); + } + 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())); + }); + + 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.'); + } + await subscription.cancel(); + subscription = null; + return codec.encodeSuccessEnvelope(null); + } +}
diff --git a/packages/flutter_web_plugins/lib/src/plugin_registry.dart b/packages/flutter_web_plugins/lib/src/plugin_registry.dart index df79e4c..a48d50c 100644 --- a/packages/flutter_web_plugins/lib/src/plugin_registry.dart +++ b/packages/flutter_web_plugins/lib/src/plugin_registry.dart
@@ -96,8 +96,20 @@ /// Sends a platform message from the platform side back to the framework. @override Future<ByteData> send(String channel, ByteData message) { - throw FlutterError( - 'Cannot send messages from the platform side to the framework.'); + final Completer<ByteData> completer = Completer<ByteData>(); + ui.window.onPlatformMessage(channel, message, (ByteData reply) { + try { + completer.complete(reply); + } catch (exception, stack) { + FlutterError.reportError(FlutterErrorDetails( + exception: exception, + stack: stack, + library: 'flutter web shell', + context: ErrorDescription('during a plugin-to-framework message'), + )); + } + }); + return completer.future; } @override
diff --git a/packages/flutter_web_plugins/test/plugin_event_channel_test.dart b/packages/flutter_web_plugins/test/plugin_event_channel_test.dart new file mode 100644 index 0000000..074954a --- /dev/null +++ b/packages/flutter_web_plugins/test/plugin_event_channel_test.dart
@@ -0,0 +1,90 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@TestOn('chrome') // Uses web-only Flutter SDK + +import 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; + +void main() { + group('Plugin Event Channel', () { + setUp(() { + TestWidgetsFlutterBinding.ensureInitialized(); + webPluginRegistry.registerMessageHandler(); + }); + + 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.controller = controller; + + expect(listeningChannel.receiveBroadcastStream(), + emitsInOrder(<String>['hello', 'world'])); + + controller.add('hello'); + controller.add('world'); + await controller.close(); + }); + + 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.controller = 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', () 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.controller = controller; + + expect(listeningChannel.receiveBroadcastStream(), + emitsInOrder(<String>['hello'])); + + controller.add('hello'); + await controller.close(); + }); + + 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.controller = 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'); + }); + }); +}
diff --git a/packages/flutter_web_plugins/test/plugin_registry_test.dart b/packages/flutter_web_plugins/test/plugin_registry_test.dart index b9ef72f..9d779ce 100644 --- a/packages/flutter_web_plugins/test/plugin_registry_test.dart +++ b/packages/flutter_web_plugins/test/plugin_registry_test.dart
@@ -31,27 +31,43 @@ setUp(() { TestWidgetsFlutterBinding.ensureInitialized(); webPluginRegistry.registerMessageHandler(); - }); - - test('Can register a plugin', () { - TestPlugin.calledMethods.clear(); - final Registrar registrar = webPluginRegistry.registrarFor(TestPlugin); TestPlugin.registerWith(registrar); + }); + + test('can register a plugin', () { + TestPlugin.calledMethods.clear(); const MethodChannel frameworkChannel = MethodChannel('test_plugin', StandardMethodCodec()); frameworkChannel.invokeMethod<void>('test1'); - expect(TestPlugin.calledMethods, <String>['test1']); + expect(TestPlugin.calledMethods, equals(<String>['test1'])); }); - test('Throws when trying to send a platform message to the framework', () { - expect(() => pluginBinaryMessenger.send('test', ByteData(0)), - throwsFlutterError); + test('can send a message from the plugin to the framework', () async { + const StandardMessageCodec codec = StandardMessageCodec(); + + final List<String> loggedMessages = <String>[]; + ServicesBinding.instance.defaultBinaryMessenger + .setMessageHandler('test_send', (ByteData data) { + loggedMessages.add(codec.decodeMessage(data)); + return null; + }); + + await pluginBinaryMessenger.send( + 'test_send', codec.encodeMessage('hello')); + expect(loggedMessages, equals(<String>['hello'])); + + await pluginBinaryMessenger.send( + 'test_send', codec.encodeMessage('world')); + expect(loggedMessages, equals(<String>['hello', 'world'])); + + ServicesBinding.instance.defaultBinaryMessenger + .setMessageHandler('test_send', null); }); - test('Throws when trying to set a mock handler', () { + test('throws when trying to set a mock handler', () { expect( () => pluginBinaryMessenger.setMockMessageHandler( 'test', (ByteData data) async => ByteData(0)),