Unverified Commit 83af6f48 authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

Adds a type parameter to invokeMethod (and additional utility methods) (#26303)

parent dca8d36d
......@@ -26,7 +26,7 @@ Future<String> dataHandler(String message) async {
final Completer<String> completer = Completer<String>();
final int id = int.tryParse(message.split('#')[1]) ?? 0;
Future<void> completeSemantics([Object _]) async {
final dynamic result = await kSemanticsChannel.invokeMethod('getSemanticsNode', <String, dynamic>{
final dynamic result = await kSemanticsChannel.invokeMethod<dynamic>('getSemanticsNode', <String, dynamic>{
'id': id,
});
completer.complete(json.encode(result));
......
......@@ -125,15 +125,15 @@ class PlatformViewState extends State<PlatformViewPage> {
.cast<Map<dynamic, dynamic>>()
.map<Map<String, dynamic>>((Map<dynamic, dynamic> e) =>e.cast<String, dynamic>())
.toList();
await channel.invokeMethod('pipeFlutterViewEvents');
await viewChannel.invokeMethod('pipeTouchEvents');
await channel.invokeMethod<void>('pipeFlutterViewEvents');
await viewChannel.invokeMethod<void>('pipeTouchEvents');
print('replaying ${recordedEvents.length} motion events');
for (Map<String, dynamic> event in recordedEvents.reversed) {
await channel.invokeMethod('synthesizeEvent', event);
await channel.invokeMethod<void>('synthesizeEvent', event);
}
await channel.invokeMethod('stopFlutterViewEvents');
await viewChannel.invokeMethod('stopTouchEvents');
await channel.invokeMethod<void>('stopFlutterViewEvents');
await viewChannel.invokeMethod<void>('stopTouchEvents');
if (flutterViewEvents.length != embeddedViewEvents.length)
return 'Synthesized ${flutterViewEvents.length} events but the embedded view received ${embeddedViewEvents.length} events';
......@@ -160,7 +160,7 @@ class PlatformViewState extends State<PlatformViewPage> {
}
Future<void> saveRecordedEvents(ByteData data, BuildContext context) async {
if (!await channel.invokeMethod('getStoragePermission')) {
if (!await channel.invokeMethod<bool>('getStoragePermission')) {
showMessage(
context, 'External storage permissions are required to save events');
return;
......@@ -190,11 +190,11 @@ class PlatformViewState extends State<PlatformViewPage> {
}
void listenToFlutterViewEvents() {
channel.invokeMethod('pipeFlutterViewEvents');
viewChannel.invokeMethod('pipeTouchEvents');
channel.invokeMethod<void>('pipeFlutterViewEvents');
viewChannel.invokeMethod<void>('pipeTouchEvents');
Timer(const Duration(seconds: 3), () {
channel.invokeMethod('stopFlutterViewEvents');
viewChannel.invokeMethod('stopTouchEvents');
channel.invokeMethod<void>('stopFlutterViewEvents');
viewChannel.invokeMethod<void>('stopTouchEvents');
});
}
......
......@@ -67,7 +67,7 @@ Future<TestStepResult> _methodCallSuccessHandshake(
dynamic result = nothing;
dynamic error = nothing;
try {
result = await channel.invokeMethod('success', arguments);
result = await channel.invokeMethod<dynamic>('success', arguments);
} catch (e) {
error = e;
}
......@@ -95,7 +95,7 @@ Future<TestStepResult> _methodCallErrorHandshake(
dynamic errorDetails = nothing;
dynamic error = nothing;
try {
error = await channel.invokeMethod('error', arguments);
error = await channel.invokeMethod<dynamic>('error', arguments);
} on PlatformException catch (e) {
errorDetails = e.details;
} catch (e) {
......@@ -123,7 +123,7 @@ Future<TestStepResult> _methodCallNotImplementedHandshake(
dynamic result = nothing;
dynamic error = nothing;
try {
error = await channel.invokeMethod('notImplemented');
error = await channel.invokeMethod<dynamic>('notImplemented');
} on MissingPluginException {
result = null;
} catch (e) {
......
......@@ -47,11 +47,11 @@ Widget builds: $_widgetBuilds''';
_summary = 'Producing texture frames at .5x speed...';
_state = FrameState.slow;
_icon = Icons.stop;
channel.invokeMethod('start', _flutterFrameRate ~/ 2);
channel.invokeMethod<void>('start', _flutterFrameRate ~/ 2);
break;
case FrameState.slow:
debugPrint('Stopping .5x speed test...');
await channel.invokeMethod('stop');
await channel.invokeMethod<void>('stop');
await _summarizeStats();
_icon = Icons.fast_forward;
_state = FrameState.afterSlow;
......@@ -62,11 +62,11 @@ Widget builds: $_widgetBuilds''';
_summary = 'Producing texture frames at 2x speed...';
_state = FrameState.fast;
_icon = Icons.stop;
channel.invokeMethod('start', (_flutterFrameRate * 2).toInt());
channel.invokeMethod<void>('start', (_flutterFrameRate * 2).toInt());
break;
case FrameState.fast:
debugPrint('Stopping 2x speed test...');
await channel.invokeMethod('stop');
await channel.invokeMethod<void>('stop');
await _summarizeStats();
_state = FrameState.afterFast;
_icon = Icons.replay;
......
......@@ -18,7 +18,7 @@ class _FlavorState extends State<Flavor> {
@override
void initState() {
super.initState();
const MethodChannel('flavor').invokeMethod('getFlavor').then((Object flavor) {
const MethodChannel('flavor').invokeMethod<String>('getFlavor').then((String flavor) {
setState(() {
_flavor = flavor;
});
......
......@@ -82,10 +82,10 @@ Future<void> main() async {
await controller.tap(find.byTooltip('Back'));
}
print('Finished successfully!');
_kTestChannel.invokeMethod('success');
_kTestChannel.invokeMethod<void>('success');
} catch (error, stack) {
print('Caught error: $error\n$stack');
_kTestChannel.invokeMethod('failure');
_kTestChannel.invokeMethod<void>('failure');
}
}
......
......@@ -34,7 +34,7 @@ class Clipboard {
/// Stores the given clipboard data on the clipboard.
static Future<void> setData(ClipboardData data) async {
await SystemChannels.platform.invokeMethod(
await SystemChannels.platform.invokeMethod<void>(
'Clipboard.setData',
<String, dynamic>{
'text': data.text,
......
......@@ -21,7 +21,7 @@ class HapticFeedback {
/// On Android, this uses the platform haptic feedback API to simulate a
/// response to a long press (`HapticFeedbackConstants.LONG_PRESS`).
static Future<void> vibrate() async {
await SystemChannels.platform.invokeMethod('HapticFeedback.vibrate');
await SystemChannels.platform.invokeMethod<void>('HapticFeedback.vibrate');
}
/// Provides a haptic feedback corresponding a collision impact with a light mass.
......@@ -32,7 +32,7 @@ class HapticFeedback {
///
/// On Android, this uses `HapticFeedbackConstants.VIRTUAL_KEY`.
static Future<void> lightImpact() async {
await SystemChannels.platform.invokeMethod(
await SystemChannels.platform.invokeMethod<void>(
'HapticFeedback.vibrate',
'HapticFeedbackType.lightImpact',
);
......@@ -46,7 +46,7 @@ class HapticFeedback {
///
/// On Android, this uses `HapticFeedbackConstants.KEYBOARD_TAP`.
static Future<void> mediumImpact() async {
await SystemChannels.platform.invokeMethod(
await SystemChannels.platform.invokeMethod<void>(
'HapticFeedback.vibrate',
'HapticFeedbackType.mediumImpact',
);
......@@ -61,7 +61,7 @@ class HapticFeedback {
/// On Android, this uses `HapticFeedbackConstants.CONTEXT_CLICK` on API levels
/// 23 and above. This call has no effects on Android API levels below 23.
static Future<void> heavyImpact() async {
await SystemChannels.platform.invokeMethod(
await SystemChannels.platform.invokeMethod<void>(
'HapticFeedback.vibrate',
'HapticFeedbackType.heavyImpact',
);
......@@ -74,7 +74,7 @@ class HapticFeedback {
///
/// On Android, this uses `HapticFeedbackConstants.CLOCK_TICK`.
static Future<void> selectionClick() async {
await SystemChannels.platform.invokeMethod(
await SystemChannels.platform.invokeMethod<void>(
'HapticFeedback.vibrate',
'HapticFeedbackType.selectionClick',
);
......
......@@ -128,6 +128,12 @@ class MethodChannel {
/// result. The values supported by the default codec and their platform-specific
/// counterparts are documented with [StandardMessageCodec].
///
/// The generic argument `T` of the method can be inferred by the surrounding
/// context, or provided explicitly. If it does not match the returned type of
/// the channel, a [TypeError] will be thrown at runtime. `T` cannot be a class
/// with generics other than `dynamic`. For example, `Map<String, String>`
/// is not supported but `Map<dynamic, dynamic>` or `Map` is.
///
/// Returns a [Future] which completes to one of the following:
///
/// * a result (possibly null), on successful invocation;
......@@ -149,10 +155,9 @@ class MethodChannel {
/// static const MethodChannel _channel = MethodChannel('music');
///
/// static Future<bool> isLicensed() async {
/// // invokeMethod returns a Future<dynamic>, and we cannot pass that for
/// // a Future<bool>, hence the indirection.
/// final bool result = await _channel.invokeMethod('isLicensed');
/// return result;
/// // invokeMethod returns a Future<T> which can be inferred as bool
/// // in this context.
/// return _channel.invokeMethod('isLicensed');
/// }
///
/// static Future<List<Song>> songs() async {
......@@ -160,6 +165,7 @@ class MethodChannel {
/// // List<dynamic> with Map<dynamic, dynamic> entries. Post-processing
/// // code thus cannot assume e.g. List<Map<String, String>> even though
/// // the actual values involved would support such a typed container.
/// // The correct type cannot be inferred with any value of `T`.
/// final List<dynamic> songs = await _channel.invokeMethod('getSongs');
/// return songs.map(Song.fromJson).toList();
/// }
......@@ -168,7 +174,7 @@ class MethodChannel {
/// // Errors occurring on the platform side cause invokeMethod to throw
/// // PlatformExceptions.
/// try {
/// await _channel.invokeMethod('play', <String, dynamic>{
/// return _channel.invokeMethod('play', <String, dynamic>{
/// 'song': song.id,
/// 'volume': volume,
/// });
......@@ -275,21 +281,54 @@ class MethodChannel {
///
/// See also:
///
/// * [invokeListMethod], for automatically returning typed lists.
/// * [invokeMapMethod], for automatically returning typed maps.
/// * [StandardMessageCodec] which defines the payload values supported by
/// [StandardMethodCodec].
/// * [JSONMessageCodec] which defines the payload values supported by
/// [JSONMethodCodec].
/// * <https://docs.flutter.io/javadoc/io/flutter/plugin/common/MethodCall.html>
/// for how to access method call arguments on Android.
Future<dynamic> invokeMethod(String method, [dynamic arguments]) async {
@optionalTypeArgs
Future<T> invokeMethod<T>(String method, [dynamic arguments]) async {
assert(method != null);
final dynamic result = await BinaryMessages.send(
final ByteData result = await BinaryMessages.send(
name,
codec.encodeMethodCall(MethodCall(method, arguments)),
);
if (result == null)
if (result == null) {
throw MissingPluginException('No implementation found for method $method on channel $name');
return codec.decodeEnvelope(result);
}
final T typedResult = codec.decodeEnvelope(result);
return typedResult;
}
/// An implementation of [invokeMethod] that can return typed lists.
///
/// Dart generics are reified, meaning that an untyped List<dynamic>
/// cannot masquerade as a List<T>. Since invokeMethod can only return
/// dynamic maps, we instead create a new typed list using [List.cast].
///
/// See also:
///
/// * [invokeMethod], which this call delegates to.
Future<List<T>> invokeListMethod<T>(String method, [dynamic arguments]) async {
final List<dynamic> result = await invokeMethod<List<dynamic>>(method, arguments);
return result.cast<T>();
}
/// An implementation of [invokeMethod] that can return typed maps.
///
/// Dart generics are reified, meaning that an untyped Map<dynamic, dynamic>
/// cannot masquerade as a Map<K, V>. Since invokeMethod can only return
/// dynamic maps, we instead create a new typed map using [Map.cast].
///
/// See also:
///
/// * [invokeMethod], which this call delegates to.
Future<Map<K, V>> invokeMapMethod<K, V>(String method, [dynamic arguments]) async {
final Map<dynamic, dynamic> result = await invokeMethod<Map<dynamic, dynamic>>(method, arguments);
return result.cast<K, V>();
}
/// Sets a callback for receiving method calls on this channel.
......@@ -366,13 +405,27 @@ class OptionalMethodChannel extends MethodChannel {
: super(name, codec);
@override
Future<dynamic> invokeMethod(String method, [dynamic arguments]) async {
Future<T> invokeMethod<T>(String method, [dynamic arguments]) async {
try {
return await super.invokeMethod(method, arguments);
final T result = await super.invokeMethod<T>(method, arguments);
return result;
} on MissingPluginException {
return null;
}
}
@override
Future<List<T>> invokeListMethod<T>(String method, [dynamic arguments]) async {
final List<dynamic> result = await invokeMethod<List<dynamic>>(method, arguments);
return result.cast<T>();
}
@override
Future<Map<K, V>> invokeMapMethod<K, V>(String method, [dynamic arguments]) async {
final Map<dynamic, dynamic> result = await invokeMethod<Map<dynamic, dynamic>>(method, arguments);
return result.cast<K, V>();
}
}
/// A named channel for communicating with platform plugins using event streams.
......@@ -434,7 +487,7 @@ class EventChannel {
return null;
});
try {
await methodChannel.invokeMethod('listen', arguments);
await methodChannel.invokeMethod<void>('listen', arguments);
} catch (exception, stack) {
FlutterError.reportError(FlutterErrorDetails(
exception: exception,
......@@ -446,7 +499,7 @@ class EventChannel {
}, onCancel: () async {
BinaryMessages.setMessageHandler(name, null);
try {
await methodChannel.invokeMethod('cancel', arguments);
await methodChannel.invokeMethod<void>('cancel', arguments);
} catch (exception, stack) {
FlutterError.reportError(FlutterErrorDetails(
exception: exception,
......
......@@ -129,7 +129,7 @@ class PlatformViewsService {
paramsByteData.lengthInBytes,
);
}
await SystemChannels.platform_views.invokeMethod('create', args);
await SystemChannels.platform_views.invokeMethod<void>('create', args);
return UiKitViewController._(id, layoutDirection);
}
}
......@@ -485,7 +485,7 @@ class AndroidViewController {
/// disposed.
Future<void> dispose() async {
if (_state == _AndroidViewState.creating || _state == _AndroidViewState.created)
await SystemChannels.platform_views.invokeMethod('dispose', id);
await SystemChannels.platform_views.invokeMethod<void>('dispose', id);
_state = _AndroidViewState.disposed;
}
......@@ -504,7 +504,7 @@ class AndroidViewController {
if (_state == _AndroidViewState.waitingForSize)
return _create(size);
await SystemChannels.platform_views.invokeMethod('resize', <String, dynamic> {
await SystemChannels.platform_views.invokeMethod<void>('resize', <String, dynamic> {
'id': id,
'width': size.width,
'height': size.height,
......@@ -526,7 +526,7 @@ class AndroidViewController {
if (_state == _AndroidViewState.waitingForSize)
return;
await SystemChannels.platform_views.invokeMethod('setDirection', <String, dynamic> {
await SystemChannels.platform_views.invokeMethod<void>('setDirection', <String, dynamic> {
'id': id,
'direction': _getAndroidDirection(layoutDirection),
});
......@@ -550,7 +550,7 @@ class AndroidViewController {
/// See documentation of [MotionEvent.obtain](https://developer.android.com/reference/android/view/MotionEvent.html#obtain(long,%20long,%20int,%20float,%20float,%20float,%20float,%20int,%20float,%20float,%20int,%20int))
/// for description of the parameters.
Future<void> sendMotionEvent(AndroidMotionEvent event) async {
await SystemChannels.platform_views.invokeMethod(
await SystemChannels.platform_views.invokeMethod<dynamic>(
'touch',
event._asList(id),
);
......@@ -649,6 +649,6 @@ class UiKitViewController {
/// disposed.
Future<void> dispose() async {
_debugDisposed = true;
await SystemChannels.platform_views.invokeMethod('dispose', id);
await SystemChannels.platform_views.invokeMethod<void>('dispose', id);
}
}
......@@ -238,7 +238,7 @@ class SystemChrome {
/// The empty list causes the application to defer to the operating system
/// default.
static Future<void> setPreferredOrientations(List<DeviceOrientation> orientations) async {
await SystemChannels.platform.invokeMethod(
await SystemChannels.platform.invokeMethod<void>(
'SystemChrome.setPreferredOrientations',
_stringify(orientations),
);
......@@ -250,7 +250,7 @@ class SystemChrome {
/// Any part of the description that is unsupported on the current platform
/// will be ignored.
static Future<void> setApplicationSwitcherDescription(ApplicationSwitcherDescription description) async {
await SystemChannels.platform.invokeMethod(
await SystemChannels.platform.invokeMethod<void>(
'SystemChrome.setApplicationSwitcherDescription',
<String, dynamic>{
'label': description.label,
......@@ -282,7 +282,7 @@ class SystemChrome {
/// or calling this again. Otherwise, the original UI overlay settings will be
/// automatically restored only when the application loses and regains focus.
static Future<void> setEnabledSystemUIOverlays(List<SystemUiOverlay> overlays) async {
await SystemChannels.platform.invokeMethod(
await SystemChannels.platform.invokeMethod<void>(
'SystemChrome.setEnabledSystemUIOverlays',
_stringify(overlays),
);
......@@ -298,7 +298,7 @@ class SystemChrome {
/// On Android, the system UI cannot be changed until 1 second after the previous
/// change. This is to prevent malware from permanently hiding navigation buttons.
static Future<void> restoreSystemUIOverlays() async {
await SystemChannels.platform.invokeMethod(
await SystemChannels.platform.invokeMethod<void>(
'SystemChrome.restoreSystemUIOverlays',
null,
);
......@@ -349,7 +349,7 @@ class SystemChrome {
scheduleMicrotask(() {
assert(_pendingStyle != null);
if (_pendingStyle != _latestStyle) {
SystemChannels.platform.invokeMethod(
SystemChannels.platform.invokeMethod<void>(
'SystemChrome.setSystemUIOverlayStyle',
_pendingStyle._toMap(),
);
......
......@@ -20,6 +20,6 @@ class SystemNavigator {
/// the latter may cause the underlying platform to act as if the application
/// had crashed.
static Future<void> pop() async {
await SystemChannels.platform.invokeMethod('SystemNavigator.pop');
await SystemChannels.platform.invokeMethod<void>('SystemNavigator.pop');
}
}
......@@ -20,7 +20,7 @@ class SystemSound {
/// Play the specified system sound. If that sound is not present on the
/// system, the call is ignored.
static Future<void> play(SystemSoundType type) async {
await SystemChannels.platform.invokeMethod(
await SystemChannels.platform.invokeMethod<void>(
'SystemSound.play',
type.toString(),
);
......
......@@ -626,13 +626,13 @@ class TextInputConnection {
/// Requests that the text input control become visible.
void show() {
assert(attached);
SystemChannels.textInput.invokeMethod('TextInput.show');
SystemChannels.textInput.invokeMethod<void>('TextInput.show');
}
/// Requests that the text input control change its internal state to match the given state.
void setEditingState(TextEditingValue value) {
assert(attached);
SystemChannels.textInput.invokeMethod(
SystemChannels.textInput.invokeMethod<void>(
'TextInput.setEditingState',
value.toJSON(),
);
......@@ -644,7 +644,7 @@ class TextInputConnection {
/// other client attaches to it within this animation frame.
void close() {
if (attached) {
SystemChannels.textInput.invokeMethod('TextInput.clearClient');
SystemChannels.textInput.invokeMethod<void>('TextInput.clearClient');
_clientHandler
.._currentConnection = null
.._scheduleHide();
......@@ -749,7 +749,7 @@ class _TextInputClientHandler {
scheduleMicrotask(() {
_hidePending = false;
if (_currentConnection == null)
SystemChannels.textInput.invokeMethod('TextInput.hide');
SystemChannels.textInput.invokeMethod<void>('TextInput.hide');
});
}
}
......@@ -802,7 +802,7 @@ class TextInput {
assert(_debugEnsureInputActionWorksOnPlatform(configuration.inputAction));
final TextInputConnection connection = TextInputConnection._(client);
_clientHandler._currentConnection = connection;
SystemChannels.textInput.invokeMethod(
SystemChannels.textInput.invokeMethod<void>(
'TextInput.setClient',
<dynamic>[ connection._id, configuration.toJson() ],
);
......
......@@ -43,15 +43,49 @@ void main() {
'ch7',
(ByteData message) async {
final Map<dynamic, dynamic> methodCall = jsonMessage.decodeMessage(message);
if (methodCall['method'] == 'sayHello')
if (methodCall['method'] == 'sayHello') {
return jsonMessage.encodeMessage(<dynamic>['${methodCall['args']} world']);
else
} else {
return jsonMessage.encodeMessage(<dynamic>['unknown', null, null]);
}
},
);
final String result = await channel.invokeMethod('sayHello', 'hello');
expect(result, equals('hello world'));
});
test('can invoke list method and get result', () async {
BinaryMessages.setMockMessageHandler(
'ch7',
(ByteData message) async {
final Map<dynamic, dynamic> methodCall = jsonMessage.decodeMessage(message);
if (methodCall['method'] == 'sayHello') {
return jsonMessage.encodeMessage(<dynamic>[<String>['${methodCall['args']}', 'world']]);
} else {
return jsonMessage.encodeMessage(<dynamic>['unknown', null, null]);
}
},
);
expect(channel.invokeMethod<List<String>>('sayHello', 'hello'), throwsA(isInstanceOf<TypeError>()));
expect(await channel.invokeListMethod<String>('sayHello', 'hello'), <String>['hello', 'world']);
});
test('can invoke map method and get result', () async {
BinaryMessages.setMockMessageHandler(
'ch7',
(ByteData message) async {
final Map<dynamic, dynamic> methodCall = jsonMessage.decodeMessage(message);
if (methodCall['method'] == 'sayHello') {
return jsonMessage.encodeMessage(<dynamic>[<String, String>{'${methodCall['args']}': 'world'}]);
} else {
return jsonMessage.encodeMessage(<dynamic>['unknown', null, null]);
}
},
);
expect(channel.invokeMethod<Map<String, String>>('sayHello', 'hello'), throwsA(isInstanceOf<TypeError>()));
expect(await channel.invokeMapMethod<String, String>('sayHello', 'hello'), <String, String>{'hello': 'world'});
});
test('can invoke method and get error', () async {
BinaryMessages.setMockMessageHandler(
'ch7',
......@@ -64,7 +98,7 @@ void main() {
},
);
try {
await channel.invokeMethod('sayHello', 'hello');
await channel.invokeMethod<dynamic>('sayHello', 'hello');
fail('Exception expected');
} on PlatformException catch (e) {
expect(e.code, equals('bad'));
......@@ -80,7 +114,7 @@ void main() {
(ByteData message) async => null,
);
try {
await channel.invokeMethod('sayHello', 'hello');
await channel.invokeMethod<void>('sayHello', 'hello');
fail('Exception expected');
} on MissingPluginException catch (e) {
expect(e.message, contains('sayHello'));
......
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