Commit 390993d0 authored by Mikkel Nygaard Ravn's avatar Mikkel Nygaard Ravn Committed by GitHub

PlatformXxxChannel concepts added to support Flutter/platform interop (#8394)

New concepts: PlatformMessageChannel (basic message send/receive superseding some existing  PlatformMessages methods), PlatformMethodChannel (method invocation and event streams), pluggable codecs for messages and method calls: unencoded binary, string, json, and 'standard' flutter binary encoding.
parent 41d81132
7f25cd0d65ca52a5fddb5f41abf5b82acbe14085 74de13c0bde4eeb967391bd2a7ba973c525113b1
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.platformservices" package="com.example.flutter"
android:versionCode="1" android:versionCode="1"
android:versionName="0.0.1"> android:versionName="0.0.1">
......
...@@ -4,85 +4,60 @@ ...@@ -4,85 +4,60 @@
package com.example.flutter; package com.example.flutter;
import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.location.Location; import android.location.Location;
import android.location.LocationManager; import android.location.LocationManager;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import io.flutter.app.FlutterActivity; import io.flutter.app.FlutterActivity;
import io.flutter.view.FlutterMain; import io.flutter.plugin.common.FlutterMethodChannel;
import io.flutter.plugin.common.FlutterMethodChannel.MethodCallHandler;
import io.flutter.plugin.common.FlutterMethodChannel.Response;
import io.flutter.plugin.common.MethodCall;
import io.flutter.view.FlutterView; import io.flutter.view.FlutterView;
import java.io.File;
import org.json.JSONException;
import org.json.JSONObject;
public class ExampleActivity extends FlutterActivity { public class ExampleActivity extends FlutterActivity {
private static final String TAG = "ExampleActivity";
private FlutterView flutterView; private FlutterView flutterView;
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
new FlutterMethodChannel(getFlutterView(), "geo").setMethodCallHandler(new MethodCallHandler() {
flutterView = getFlutterView(); @Override
flutterView.addOnMessageListener("getLocation", public void onMethodCall(MethodCall call, Response response) {
new FlutterView.OnMessageListener() { if (call.method.equals("getLocation")) {
@Override if (!(call.arguments instanceof String)) {
public String onMessage(FlutterView view, String message) { throw new IllegalArgumentException("Invalid argument type, String expected");
return onGetLocation(message); }
getLocation((String) call.arguments, response);
} else {
throw new IllegalArgumentException("Unknown method " + call.method);
} }
}); }
});
} }
private String onGetLocation(String json) { private void getLocation(String provider, Response response) {
String provider;
try {
JSONObject message = new JSONObject(json);
provider = message.getString("provider");
} catch (JSONException e) {
Log.e(TAG, "JSON exception", e);
return null;
}
String locationProvider; String locationProvider;
if (provider.equals("network")) { if (provider.equals("network")) {
locationProvider = LocationManager.NETWORK_PROVIDER; locationProvider = LocationManager.NETWORK_PROVIDER;
} else if (provider.equals("gps")) { } else if (provider.equals("gps")) {
locationProvider = LocationManager.GPS_PROVIDER; locationProvider = LocationManager.GPS_PROVIDER;
} else { } else {
return null; throw new IllegalArgumentException("Unknown provider " + provider);
} }
String permission = "android.permission.ACCESS_FINE_LOCATION"; String permission = "android.permission.ACCESS_FINE_LOCATION";
Location location = null;
if (checkCallingOrSelfPermission(permission) == PackageManager.PERMISSION_GRANTED) { if (checkCallingOrSelfPermission(permission) == PackageManager.PERMISSION_GRANTED) {
LocationManager locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE); LocationManager locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
location = locationManager.getLastKnownLocation(locationProvider); Location location = locationManager.getLastKnownLocation(locationProvider);
}
JSONObject reply = new JSONObject();
try {
if (location != null) { if (location != null) {
reply.put("latitude", location.getLatitude()); response.success(new double[] { location.getLatitude(), location.getLongitude() });
reply.put("longitude", location.getLongitude());
} else { } else {
reply.put("latitude", 0); response.error("unknown", "Location unknown", null);
reply.put("longitude", 0);
} }
} catch (JSONException e) { } else {
Log.e(TAG, "JSON exception", e); response.error("permission", "Access denied", null);
return null;
} }
return reply.toString();
} }
} }
\ No newline at end of file
...@@ -13,8 +13,7 @@ class PlatformServices extends StatefulWidget { ...@@ -13,8 +13,7 @@ class PlatformServices extends StatefulWidget {
} }
class _PlatformServicesState extends State<PlatformServices> { class _PlatformServicesState extends State<PlatformServices> {
double _latitude; Future<dynamic> _locationRequest;
double _longitude;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
...@@ -26,28 +25,42 @@ class _PlatformServicesState extends State<PlatformServices> { ...@@ -26,28 +25,42 @@ class _PlatformServicesState extends State<PlatformServices> {
new Text('Hello from Flutter!'), new Text('Hello from Flutter!'),
new RaisedButton( new RaisedButton(
child: new Text('Get Location'), child: new Text('Get Location'),
onPressed: _getLocation onPressed: _requestLocation,
), ),
new Text('Latitude: $_latitude, Longitude: $_longitude'), new FutureBuilder<dynamic>(
] future: _locationRequest,
) builder: _buildLocation,
) ),
],
),
),
); );
} }
Future<Null> _getLocation() async { void _requestLocation() {
final Map<String, String> message = <String, String>{'provider': 'network'};
final Map<String, dynamic> reply = await PlatformMessages.sendJSON('getLocation', message);
// If the widget was removed from the tree while the message was in flight,
// we want to discard the reply rather than calling setState to update our
// non-existent appearance.
if (!mounted)
return;
setState(() { setState(() {
_latitude = reply['latitude'].toDouble(); _locationRequest = const PlatformMethodChannel('geo').invokeMethod(
_longitude = reply['longitude'].toDouble(); 'getLocation',
'network',
);
}); });
} }
Widget _buildLocation(BuildContext context, AsyncSnapshot<dynamic> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
return new Text('Press button to request location');
case ConnectionState.waiting:
return new Text('Awaiting response...');
default:
try {
final List<double> location = snapshot.requireData;
return new Text('Lat. ${location[0]}, Long. ${location[1]}');
} on PlatformException catch (e) {
return new Text('Request failed: ${e.message}');
}
}
}
} }
void main() { void main() {
......
...@@ -24,4 +24,5 @@ export 'src/foundation/licenses.dart'; ...@@ -24,4 +24,5 @@ export 'src/foundation/licenses.dart';
export 'src/foundation/observer_list.dart'; export 'src/foundation/observer_list.dart';
export 'src/foundation/platform.dart'; export 'src/foundation/platform.dart';
export 'src/foundation/print.dart'; export 'src/foundation/print.dart';
export 'src/foundation/serialization.dart';
export 'src/foundation/synchronous_future.dart'; export 'src/foundation/synchronous_future.dart';
...@@ -13,6 +13,8 @@ library services; ...@@ -13,6 +13,8 @@ library services;
export 'src/services/asset_bundle.dart'; export 'src/services/asset_bundle.dart';
export 'src/services/binding.dart'; export 'src/services/binding.dart';
export 'src/services/clipboard.dart'; export 'src/services/clipboard.dart';
export 'src/services/message_codec.dart';
export 'src/services/message_codecs.dart';
export 'src/services/haptic_feedback.dart'; export 'src/services/haptic_feedback.dart';
export 'src/services/image_cache.dart'; export 'src/services/image_cache.dart';
export 'src/services/image_decoder.dart'; export 'src/services/image_decoder.dart';
...@@ -20,6 +22,7 @@ export 'src/services/image_provider.dart'; ...@@ -20,6 +22,7 @@ export 'src/services/image_provider.dart';
export 'src/services/image_resolution.dart'; export 'src/services/image_resolution.dart';
export 'src/services/image_stream.dart'; export 'src/services/image_stream.dart';
export 'src/services/path_provider.dart'; export 'src/services/path_provider.dart';
export 'src/services/platform_channel.dart';
export 'src/services/platform_messages.dart'; export 'src/services/platform_messages.dart';
export 'src/services/raw_keyboard.dart'; export 'src/services/raw_keyboard.dart';
export 'src/services/system_chrome.dart'; export 'src/services/system_chrome.dart';
......
// Copyright 2017 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:typed_data';
import 'package:typed_data/typed_buffers.dart' show Uint8Buffer;
/// Write-only buffer for incrementally building a [ByteData] instance.
///
/// A WriteBuffer instance can be used only once. Attempts to reuse will result
/// in [NoSuchMethodError]s being thrown.
///
/// The byte order of serialized data is [Endianness.BIG_ENDIAN].
/// The byte order of deserialized data is [Endianness.HOST_ENDIAN].
class WriteBuffer {
Uint8Buffer _buffer;
ByteData _eightBytes;
Uint8List _eightBytesAsList;
WriteBuffer() {
_buffer = new Uint8Buffer();
_eightBytes = new ByteData(8);
_eightBytesAsList = _eightBytes.buffer.asUint8List();
}
void putUint8(int byte) {
_buffer.add(byte);
}
void putInt32(int value) {
putUint8(value >> 24);
putUint8(value >> 16);
putUint8(value >> 8);
putUint8(value);
}
void putInt64(int value) {
putUint8(value >> 56);
putUint8(value >> 48);
putUint8(value >> 40);
putUint8(value >> 32);
putUint8(value >> 24);
putUint8(value >> 16);
putUint8(value >> 8);
putUint8(value);
}
void putFloat64(double value) {
_eightBytes.setFloat64(0, value);
_buffer.addAll(_eightBytesAsList);
}
void putUint8List(Uint8List list) {
_buffer.addAll(list);
}
void putInt32List(Int32List list) {
_alignTo(4);
if (Endianness.HOST_ENDIAN == Endianness.BIG_ENDIAN) {
_buffer.addAll(list.buffer.asUint8List(list.offsetInBytes, 4 * list.length));
} else {
for (final int value in list) {
putInt32(value);
}
}
}
void putInt64List(Int64List list) {
_alignTo(8);
if (Endianness.HOST_ENDIAN == Endianness.BIG_ENDIAN) {
_buffer.addAll(list.buffer.asUint8List(list.offsetInBytes, 8 * list.length));
} else {
for (final int value in list) {
putInt64(value);
}
}
}
void putFloat64List(Float64List list) {
_alignTo(8);
if (Endianness.HOST_ENDIAN == Endianness.BIG_ENDIAN) {
_buffer.addAll(list.buffer.asUint8List(list.offsetInBytes, 8 * list.length));
} else {
for (final double value in list) {
putFloat64(value);
}
}
}
void _alignTo(int alignment) {
final int mod = _buffer.length % alignment;
if (mod != 0) {
for (int i = 0; i < alignment - mod; i++) {
_buffer.add(0);
}
}
}
ByteData done() {
final ByteData result = _buffer.buffer.asByteData(0, _buffer.lengthInBytes);
_buffer = null;
return result;
}
}
/// Read-only buffer for reading sequentially from a [ByteData] instance.
///
/// The byte order of serialized data is [Endianness.BIG_ENDIAN].
/// The byte order of deserialized data is [Endianness.HOST_ENDIAN].
class ReadBuffer {
final ByteData data;
int position = 0;
/// Creates a [ReadBuffer] for reading from the specified [data].
ReadBuffer(this.data) {
assert(data != null);
}
int getUint8() {
return data.getUint8(position++);
}
int getInt32() {
final int value = data.getInt32(position);
position += 4;
return value;
}
int getInt64() {
final int value = data.getInt64(position);
position += 8;
return value;
}
double getFloat64() {
final double value = data.getFloat64(position);
position += 8;
return value;
}
Uint8List getUint8List(int length) {
final Uint8List list = data.buffer.asUint8List(data.offsetInBytes + position, length);
position += length;
return list;
}
Int32List getInt32List(int length) {
_alignTo(4);
Int32List list;
if (Endianness.HOST_ENDIAN == Endianness.BIG_ENDIAN) {
list = data.buffer.asInt32List(data.offsetInBytes + position, length);
} else {
final ByteData invertedData = new ByteData(4 * length);
for (int i = 0; i < length; i++) {
invertedData.setInt32(i * 4, data.getInt32(position + i * 4, Endianness.HOST_ENDIAN));
}
list = new Int32List.view(invertedData.buffer);
}
position += 4 * length;
return list;
}
Int64List getInt64List(int length) {
_alignTo(8);
Int64List list;
if (Endianness.HOST_ENDIAN == Endianness.BIG_ENDIAN) {
list = data.buffer.asInt64List(data.offsetInBytes + position, length);
} else {
final ByteData invertedData = new ByteData(8 * length);
for (int i = 0; i < length; i++) {
invertedData.setInt64(i * 8, data.getInt64(position + i * 8, Endianness.HOST_ENDIAN));
}
list = new Int64List.view(invertedData.buffer);
}
position += 8 * length;
return list;
}
Float64List getFloat64List(int length) {
_alignTo(8);
Float64List list;
if (Endianness.HOST_ENDIAN == Endianness.BIG_ENDIAN) {
list = data.buffer.asFloat64List(data.offsetInBytes + position, length);
} else {
final ByteData invertedData = new ByteData(8 * length);
for (int i = 0; i < length; i++) {
invertedData.setFloat64(i * 8, data.getFloat64(position + i * 8, Endianness.HOST_ENDIAN));
}
list = new Float64List.view(invertedData.buffer);
}
position += 8 * length;
return list;
}
void _alignTo(int alignment) {
final int mod = position % alignment;
if (mod != 0) {
position += alignment - mod;
}
}
bool get hasRemaining => position < data.lengthInBytes;
}
// Copyright 2017 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:typed_data';
import 'package:meta/meta.dart';
/// A message encoding/decoding mechanism.
///
/// Both operations throw [FormatException], if conversion fails.
///
/// See also:
///
/// * [PlatformMessageChannel], which use [MessageCodec]s for communication
/// between Flutter and platform plugins.
abstract class MessageCodec<T> {
/// Encodes the specified [message] in binary.
///
/// Returns `null` if the message is `null`.
ByteData encodeMessage(T message);
/// Decodes the specified [message] from binary.
///
/// Returns `null` if the message is `null`.
T decodeMessage(ByteData message);
}
/// A codec for method calls and enveloped results.
///
/// Result envelopes are binary messages with enough structure that the codec can
/// distinguish between a successful result and an error. In the former case,
/// the codec must be able to extract the result payload, possibly `null`. In
/// the latter case, the codec must be able to extract an error code string,
/// a (human-readable) error message string, and a value providing any
/// additional error details, possibly `null`. These data items are used to
/// populate a [PlatformException].
///
/// All operations throw [FormatException], if conversion fails.
///
/// See also:
///
/// * [PlatformMethodChannel], which use [MethodCodec]s for communication
/// between Flutter and platform plugins.
abstract class MethodCodec {
/// Encodes the specified method call in binary.
///
/// The [name] of the method must be non-null. The [arguments] may be `null`.
ByteData encodeMethodCall(String name, dynamic arguments);
/// Decodes the specified result [envelope] from binary.
///
/// Throws [PlatformException], if [envelope] represents an error.
dynamic decodeEnvelope(ByteData envelope);
}
/// Thrown to indicate that a platform interaction failed in the platform
/// plugin.
///
/// See also:
///
/// * [MethodCodec], which throws a [PlatformException], if a received result
/// envelope represents an error.
/// * [PlatformMethodChannel.invokeMethod], which completes the returned future
/// with a [PlatformException], if invoking the platform plugin method
/// results in an error envelope.
/// * [PlatformMethodChannel.receiveBroadcastStream], which emits
/// [PlatformException]s as error events, whenever an event received from the
/// platform plugin is wrapped in an error envelope.
class PlatformException implements Exception {
/// Creates a [PlatformException] with the specified error [code] and optional
/// [message], and with the optional error [details] which must be a valid
/// value for the [MethodCodec] involved in the interaction.
PlatformException({
@required this.code,
this.message,
this.details,
}) {
assert(code != null);
}
/// An error code.
final String code;
/// A human-readable error message, possibly `null`.
final String message;
/// Error details, possibly `null`.
final dynamic details;
@override
String toString() => 'PlatformException($code, $message, $details)';
}
This diff is collapsed.
// Copyright 2017 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 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'message_codec.dart';
import 'message_codecs.dart';
import 'platform_messages.dart';
/// A named channel for communicating with platform plugins using asynchronous
/// message passing.
///
/// Messages are encoded into binary before being sent, and binary messages
/// received are decoded into Dart values. The [MessageCodec] used must be
/// compatible with the one used by the platform plugin. This can be achieved
/// by creating a FlutterMessageChannel counterpart of this channel on the
/// platform side. The Dart type of messages sent and received is [T],
/// but only the values supported by the specified [MessageCodec] can be used.
///
/// The identity of the channel is given by its name, so other uses of that name
/// with may interfere with this channel's communication. Specifically, at most
/// one message handler can be registered with the channel name at any given
/// time.
class PlatformMessageChannel<T> {
/// Creates a [PlatformMessageChannel] with the specified [name] and [codec].
///
/// Neither [name] nor [codec] may be `null`.
const PlatformMessageChannel(this.name, this.codec);
/// The logical channel on which communication happens, not `null`.
final String name;
/// The message codec used by this channel, not `null`.
final MessageCodec<T> codec;
/// Sends the specified [message] to the platform plugins on this channel.
///
/// Returns a [Future] which completes to the received and decoded response,
/// or to a [FormatException], if encoding or decoding fails.
Future<T> send(T message) async {
return codec.decodeMessage(
await PlatformMessages.sendBinary(name, codec.encodeMessage(message))
);
}
/// Sets a callback for receiving messages from the platform plugins on this
/// channel.
///
/// The given callback will replace the currently registered callback for this
/// channel's name.
///
/// The handler's return value, if non-null, is sent back to the platform
/// plugins as a response.
void setMessageHandler(Future<T> handler(T message)) {
PlatformMessages.setBinaryMessageHandler(name, (ByteData message) async {
return codec.encodeMessage(await handler(codec.decodeMessage(message)));
});
}
/// Sets a mock callback for intercepting messages sent on this channel.
///
/// The given callback will replace the currently registered mock callback for
/// this channel, if any. To remove the mock handler, pass `null` as the
/// `handler` argument.
///
/// The handler's return value, if non-null, is used as a response.
///
/// This is intended for testing. Messages intercepted in this manner are not
/// sent to platform plugins.
void setMockMessageHandler(Future<T> handler(T message)) {
if (handler == null) {
PlatformMessages.setMockBinaryMessageHandler(name, null);
} else {
PlatformMessages.setMockBinaryMessageHandler(name, (ByteData message) async {
return codec.encodeMessage(await handler(codec.decodeMessage(message)));
});
}
}
}
/// A named channel for communicating with platform plugins using asynchronous
/// method calls and event streams.
///
/// Method calls are encoded into binary before being sent, and binary results
/// received are decoded into Dart values. The [MethodCodec] used must be
/// compatible with the one used by the platform plugin. This can be achieved
/// by creating a FlutterMethodChannel counterpart of this channel on the
/// platform side. The Dart type of messages sent and received is `dynamic`,
/// but only values supported by the specified [MethodCodec] can be used.
///
/// The identity of the channel is given by its name, so other uses of that name
/// with may interfere with this channel's communication.
class PlatformMethodChannel {
/// Creates a [PlatformMethodChannel] with the specified [name].
///
/// The [codec] used will be [StandardMethodCodec], unless otherwise
/// specified.
///
/// Neither [name] nor [codec] may be `null`.
const PlatformMethodChannel(this.name, [this.codec = const StandardMethodCodec()]);
/// The logical channel on which communication happens, not `null`.
final String name;
/// The message codec used by this channel, not `null`.
final MethodCodec codec;
/// Invokes a [method] on this channel with the specified [arguments].
///
/// Returns a [Future] which completes to one of the following:
///
/// * a result (possibly `null`), on successful invocation;
/// * a [PlatformException], if the invocation failed in the platform plugin;
/// * a [FormatException], if encoding or decoding failed.
Future<dynamic> invokeMethod(String method, [dynamic arguments]) async {
assert(method != null);
return codec.decodeEnvelope(await PlatformMessages.sendBinary(
name,
codec.encodeMethodCall(method, arguments),
));
}
/// Sets up a broadcast stream for receiving events on this channel.
///
/// Returns a broadcast [Stream] which emits events to listeners as follows:
///
/// * a decoded data event (possibly `null`) for each successful event
/// received from the platform plugin;
/// * an error event containing a [PlatformException] for each error event
/// received from the platform plugin;
/// * an error event containing a [FormatException] for each event received
/// where decoding fails;
/// * an error event containing a [PlatformException] or [FormatException]
/// whenever stream setup fails (stream setup is done only when listener
/// count changes from 0 to 1).
///
/// Notes for platform plugin implementers:
///
/// Plugins must expose methods named `listen` and `cancel` suitable for
/// invocations by [invokeMethod]. Both methods are invoked with the specified
/// [arguments].
///
/// Following the semantics of broadcast streams, `listen` will be called as
/// the first listener registers with the returned stream, and `cancel` when
/// the last listener cancels its registration. This pattern may repeat
/// indefinitely. Platform plugins should consume no stream-related resources
/// while listener count is zero.
Stream<dynamic> receiveBroadcastStream([dynamic arguments]) {
StreamController<dynamic> controller;
controller = new StreamController<dynamic>.broadcast(
onListen: () async {
PlatformMessages.setBinaryMessageHandler(
name, (ByteData reply) async {
if (reply == null) {
controller.close();
} else {
try {
controller.add(codec.decodeEnvelope(reply));
} catch (e) {
controller.addError(e);
}
}
}
);
try {
await invokeMethod('listen', arguments);
} catch (e) {
PlatformMessages.setBinaryMessageHandler(name, null);
controller.addError(e);
}
}, onCancel: () async {
PlatformMessages.setBinaryMessageHandler(name, null);
try {
await invokeMethod('cancel', arguments);
} catch (exception, stack) {
FlutterError.reportError(new FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'services library',
context: 'while de-activating platform stream on channel $name',
));
}
}
);
return controller.stream;
}
}
// Copyright 2016 The Chromium Authors. All rights reserved. // Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
...@@ -66,8 +66,7 @@ class PlatformMessages { ...@@ -66,8 +66,7 @@ class PlatformMessages {
/// Typically called by [ServicesBinding] to handle platform messages received /// Typically called by [ServicesBinding] to handle platform messages received
/// from [ui.window.onPlatformMessage]. /// from [ui.window.onPlatformMessage].
/// ///
/// To register a handler for a given message channel, see /// To register a handler for a given message channel, see [PlatformChannel].
/// [setStringMessageHandler] and [setJSONMessageHandler].
static Future<Null> handlePlatformMessage( static Future<Null> handlePlatformMessage(
String channel, ByteData data, ui.PlatformMessageResponseCallback callback) async { String channel, ByteData data, ui.PlatformMessageResponseCallback callback) async {
ByteData response; ByteData response;
...@@ -104,6 +103,8 @@ class PlatformMessages { ...@@ -104,6 +103,8 @@ class PlatformMessages {
/// ///
/// Returns a [Future] which completes to the received response, decoded as a /// Returns a [Future] which completes to the received response, decoded as a
/// UTF-8 string, or to an error, if the decoding fails. /// UTF-8 string, or to an error, if the decoding fails.
///
/// Deprecated, use [PlatformMessageChannel.send] instead.
static Future<String> sendString(String channel, String message) async { static Future<String> sendString(String channel, String message) async {
return _decodeUTF8(await sendBinary(channel, _encodeUTF8(message))); return _decodeUTF8(await sendBinary(channel, _encodeUTF8(message)));
} }
...@@ -115,6 +116,8 @@ class PlatformMessages { ...@@ -115,6 +116,8 @@ class PlatformMessages {
/// Returns a [Future] which completes to the received response, decoded as a /// Returns a [Future] which completes to the received response, decoded as a
/// UTF-8-encoded JSON representation of a JSON value (a [String], [bool], /// UTF-8-encoded JSON representation of a JSON value (a [String], [bool],
/// [double], [List], or [Map]), or to an error, if the decoding fails. /// [double], [List], or [Map]), or to an error, if the decoding fails.
///
/// Deprecated, use [PlatformMessageChannel.send] instead.
static Future<dynamic> sendJSON(String channel, dynamic json) async { static Future<dynamic> sendJSON(String channel, dynamic json) async {
return _decodeJSON(await sendString(channel, _encodeJSON(json))); return _decodeJSON(await sendString(channel, _encodeJSON(json)));
} }
...@@ -129,6 +132,8 @@ class PlatformMessages { ...@@ -129,6 +132,8 @@ class PlatformMessages {
/// The response from the method call is decoded as UTF-8, then the UTF-8 is /// The response from the method call is decoded as UTF-8, then the UTF-8 is
/// decoded as JSON. The returned [Future] completes to this fully decoded /// decoded as JSON. The returned [Future] completes to this fully decoded
/// response, or to an error, if the decoding fails. /// response, or to an error, if the decoding fails.
///
/// Deprecated, use [PlatformMethodChannel.invokeMethod] instead.
static Future<dynamic> invokeMethod(String channel, String method, [ List<dynamic> args = const <Null>[] ]) { static Future<dynamic> invokeMethod(String channel, String method, [ List<dynamic> args = const <Null>[] ]) {
return sendJSON(channel, <String, dynamic>{ return sendJSON(channel, <String, dynamic>{
'method': method, 'method': method,
...@@ -155,6 +160,8 @@ class PlatformMessages { ...@@ -155,6 +160,8 @@ class PlatformMessages {
/// ///
/// The handler's return value, if non-null, is sent as a response, encoded as /// The handler's return value, if non-null, is sent as a response, encoded as
/// a UTF-8 string. /// a UTF-8 string.
///
/// Deprecated, use [PlatformMessageChannel.setMessageHandler] instead.
static void setStringMessageHandler(String channel, Future<String> handler(String message)) { static void setStringMessageHandler(String channel, Future<String> handler(String message)) {
setBinaryMessageHandler(channel, (ByteData message) async { setBinaryMessageHandler(channel, (ByteData message) async {
return _encodeUTF8(await handler(_decodeUTF8(message))); return _encodeUTF8(await handler(_decodeUTF8(message)));
...@@ -169,6 +176,8 @@ class PlatformMessages { ...@@ -169,6 +176,8 @@ class PlatformMessages {
/// ///
/// The handler's return value, if non-null, is sent as a response, encoded as /// The handler's return value, if non-null, is sent as a response, encoded as
/// JSON and then as a UTF-8 string. /// JSON and then as a UTF-8 string.
///
/// Deprecated, use [PlatformMessageChannel.setMessageHandler] instead.
static void setJSONMessageHandler(String channel, Future<dynamic> handler(dynamic message)) { static void setJSONMessageHandler(String channel, Future<dynamic> handler(dynamic message)) {
setStringMessageHandler(channel, (String message) async { setStringMessageHandler(channel, (String message) async {
return _encodeJSON(await handler(_decodeJSON(message))); return _encodeJSON(await handler(_decodeJSON(message)));
...@@ -205,6 +214,8 @@ class PlatformMessages { ...@@ -205,6 +214,8 @@ class PlatformMessages {
/// ///
/// This is intended for testing. Messages intercepted in this manner are not /// This is intended for testing. Messages intercepted in this manner are not
/// sent to platform plugins. /// sent to platform plugins.
///
/// Deprecated, use [PlatformMessageChannel.setMockMessageHandler] instead.
static void setMockStringMessageHandler(String channel, Future<String> handler(String message)) { static void setMockStringMessageHandler(String channel, Future<String> handler(String message)) {
if (handler == null) { if (handler == null) {
setMockBinaryMessageHandler(channel, null); setMockBinaryMessageHandler(channel, null);
...@@ -227,6 +238,8 @@ class PlatformMessages { ...@@ -227,6 +238,8 @@ class PlatformMessages {
/// ///
/// This is intended for testing. Messages intercepted in this manner are not /// This is intended for testing. Messages intercepted in this manner are not
/// sent to platform plugins. /// sent to platform plugins.
///
/// Deprecated, use [PlatformMessageChannel.setMockMessageHandler] instead.
static void setMockJSONMessageHandler(String channel, Future<dynamic> handler(dynamic message)) { static void setMockJSONMessageHandler(String channel, Future<dynamic> handler(dynamic message)) {
if (handler == null) { if (handler == null) {
setMockStringMessageHandler(channel, null); setMockStringMessageHandler(channel, null);
......
...@@ -204,6 +204,18 @@ class AsyncSnapshot<T> { ...@@ -204,6 +204,18 @@ class AsyncSnapshot<T> {
/// Latest data received. Is `null`, if [error] is not. /// Latest data received. Is `null`, if [error] is not.
final T data; final T data;
/// Returns latest data received, failing if there is no data.
///
/// Throws [error], if [hasError]. Throws [StateError], if neither [hasData]
/// nor [hasError].
T get requireData {
if (hasData)
return data;
if (hasError)
throw error;
throw new StateError('Snapshot has neither data nor error');
}
/// Latest error object received. Is `null`, if [data] is not. /// Latest error object received. Is `null`, if [data] is not.
final Object error; final Object error;
......
// Copyright 2017 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 'package:flutter/foundation.dart';
import 'package:test/test.dart';
import 'dart:typed_data';
void main() {
group('Write and read buffer round-trip', () {
test('of single byte', () {
final WriteBuffer write = new WriteBuffer();
write.putUint8(201);
final ByteData written = write.done();
expect(written.lengthInBytes, equals(1));
final ReadBuffer read = new ReadBuffer(written);
expect(read.getUint8(), equals(201));
});
test('of 32-bit integer', () {
final WriteBuffer write = new WriteBuffer();
write.putInt32(-9);
final ByteData written = write.done();
expect(written.lengthInBytes, equals(4));
final ReadBuffer read = new ReadBuffer(written);
expect(read.getInt32(), equals(-9));
});
test('of 64-bit integer', () {
final WriteBuffer write = new WriteBuffer();
write.putInt64(-9000000000000);
final ByteData written = write.done();
expect(written.lengthInBytes, equals(8));
final ReadBuffer read = new ReadBuffer(written);
expect(read.getInt64(), equals(-9000000000000));
});
test('of double', () {
final WriteBuffer write = new WriteBuffer();
write.putFloat64(3.14);
final ByteData written = write.done();
expect(written.lengthInBytes, equals(8));
final ReadBuffer read = new ReadBuffer(written);
expect(read.getFloat64(), equals(3.14));
});
test('of 32-bit int list when unaligned', () {
final Int32List integers = new Int32List.fromList(<int>[-99, 2, 99]);
final WriteBuffer write = new WriteBuffer();
write.putUint8(9);
write.putInt32List(integers);
final ByteData written = write.done();
expect(written.lengthInBytes, equals(16));
final ReadBuffer read = new ReadBuffer(written);
read.getUint8();
expect(read.getInt32List(3), equals(integers));
});
test('of 64-bit int list when unaligned', () {
final Int64List integers = new Int64List.fromList(<int>[-99, 2, 99]);
final WriteBuffer write = new WriteBuffer();
write.putUint8(9);
write.putInt64List(integers);
final ByteData written = write.done();
expect(written.lengthInBytes, equals(32));
final ReadBuffer read = new ReadBuffer(written);
read.getUint8();
expect(read.getInt64List(3), equals(integers));
});
test('of double list when unaligned', () {
final Float64List doubles = new Float64List.fromList(<double>[3.14, double.NAN]);
final WriteBuffer write = new WriteBuffer();
write.putUint8(9);
write.putFloat64List(doubles);
final ByteData written = write.done();
expect(written.lengthInBytes, equals(24));
final ReadBuffer read = new ReadBuffer(written);
read.getUint8();
final Float64List readDoubles = read.getFloat64List(2);
expect(readDoubles[0], equals(3.14));
expect(readDoubles[1], isNaN);
});
});
}
\ No newline at end of file
// Copyright 2017 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 'dart:typed_data';
import 'package:flutter/services.dart';
import 'package:test/test.dart';
void main() {
group('PlatformMessageChannel', () {
const MessageCodec<String> string = const StringCodec();
const PlatformMessageChannel<String> channel = const PlatformMessageChannel<String>('ch', string);
test('can send string message and get reply', () async {
PlatformMessages.setMockBinaryMessageHandler(
'ch',
(ByteData message) async => string.encodeMessage(string.decodeMessage(message) + ' world'),
);
final String reply = await channel.send('hello');
expect(reply, equals('hello world'));
});
test('can receive string message and send reply', () async {
channel.setMessageHandler((String message) async => message + ' world');
String reply;
await PlatformMessages.handlePlatformMessage(
'ch',
const StringCodec().encodeMessage('hello'),
(ByteData replyBinary) {
reply = string.decodeMessage(replyBinary);
}
);
expect(reply, equals('hello world'));
});
});
group('PlatformMethodChannel', () {
const MessageCodec<dynamic> jsonMessage = const JSONMessageCodec();
const MethodCodec jsonMethod = const JSONMethodCodec();
const PlatformMethodChannel channel = const PlatformMethodChannel('ch', jsonMethod);
test('can invoke method and get result', () async {
PlatformMessages.setMockBinaryMessageHandler(
'ch',
(ByteData message) async {
final List<dynamic> methodCall = jsonMessage.decodeMessage(message);
if (methodCall[0] == 'sayHello')
return jsonMessage.encodeMessage(<dynamic>['${methodCall[1]} world']);
else
return jsonMessage.encodeMessage(<dynamic>['unknown', null, null]);
},
);
final String result = await channel.invokeMethod('sayHello', 'hello');
expect(result, equals('hello world'));
});
test('can invoke method and get error', () async {
PlatformMessages.setMockBinaryMessageHandler(
'ch',
(ByteData message) async {
return jsonMessage.encodeMessage(<dynamic>[
'unknown',
'Method not understood',
<String, dynamic>{'a': 42, 'b': 3.14},
]);
},
);
try {
await channel.invokeMethod('sayHello', 'hello');
fail('Exception expected');
} on PlatformException catch(e) {
expect(e.code, equals('unknown'));
expect(e.message, equals('Method not understood'));
expect(e.details, equals(<String, dynamic>{'a': 42, 'b': 3.14}));
}
});
test('can receive event stream', () async {
void emitEvent(dynamic event) {
PlatformMessages.handlePlatformMessage(
'ch',
event,
(ByteData reply) {},
);
}
bool cancelled = false;
PlatformMessages.setMockBinaryMessageHandler(
'ch',
(ByteData message) async {
final List<dynamic> methodCall = jsonMessage.decodeMessage(message);
if (methodCall[0] == 'listen') {
final String argument = methodCall[1];
emitEvent(jsonMessage.encodeMessage(<dynamic>[argument + '1']));
emitEvent(jsonMessage.encodeMessage(<dynamic>[argument + '2']));
emitEvent(null);
return jsonMessage.encodeMessage(<dynamic>[null]);
} else if (methodCall[0] == 'cancel') {
cancelled = true;
return jsonMessage.encodeMessage(<dynamic>[null]);
} else {
fail('Expected listen or cancel');
}
},
);
final List<dynamic> events = await channel.receiveBroadcastStream('hello').toList();
expect(events, orderedEquals(<String>['hello1', 'hello2']));
await new Future<Null>.delayed(const Duration());
expect(cancelled, isTrue);
});
});
}
\ No newline at end of file
...@@ -11,6 +11,26 @@ void main() { ...@@ -11,6 +11,26 @@ void main() {
Widget snapshotText(BuildContext context, AsyncSnapshot<String> snapshot) { Widget snapshotText(BuildContext context, AsyncSnapshot<String> snapshot) {
return new Text(snapshot.toString()); return new Text(snapshot.toString());
} }
group('AsyncSnapshot', () {
test('requiring data succeeds if data is present', () {
expect(
new AsyncSnapshot<String>.withData(ConnectionState.done, 'hello').requireData,
'hello',
);
});
test('requiring data fails if there is an error', () {
expect(
() => new AsyncSnapshot<String>.withError(ConnectionState.done, 'error').requireData,
throwsA(equals('error')),
);
});
test('requiring data fails if snapshot has neither data nor error', () {
expect(
() => new AsyncSnapshot<String>.nothing().requireData,
throwsStateError,
);
});
});
group('Async smoke tests', () { group('Async smoke tests', () {
testWidgets('FutureBuilder', (WidgetTester tester) async { testWidgets('FutureBuilder', (WidgetTester tester) async {
await tester.pumpWidget(new FutureBuilder<String>( await tester.pumpWidget(new FutureBuilder<String>(
......
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