Unverified Commit 6911a151 authored by Harry Terkelsen's avatar Harry Terkelsen Committed by GitHub

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
parent c94a994d
......@@ -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';
// 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);
}
}
......@@ -96,8 +96,20 @@ class _PlatformBinaryMessenger extends BinaryMessenger {
/// 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
......
// 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');
});
});
}
......@@ -31,27 +31,43 @@ void main() {
setUp(() {
TestWidgetsFlutterBinding.ensureInitialized();
webPluginRegistry.registerMessageHandler();
final Registrar registrar = webPluginRegistry.registrarFor(TestPlugin);
TestPlugin.registerWith(registrar);
});
test('Can register a plugin', () {
test('can register a plugin', () {
TestPlugin.calledMethods.clear();
final Registrar registrar = webPluginRegistry.registrarFor(TestPlugin);
TestPlugin.registerWith(registrar);
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)),
......
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