Skip to content

PlatformChannel concept added to support interop #8394

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Mar 1, 2017
Merged

PlatformChannel concept added to support interop #8394

merged 14 commits into from
Mar 1, 2017

Conversation

mravn-google
Copy link
Contributor

@mravn-google mravn-google commented Feb 24, 2017

Matches flutter/engine#3446 on flutter/engine that introduces a FlutterChannel on the platform side.
Unit tests pending.
Ready for design review.

@@ -10,7 +10,7 @@

<application android:name="io.flutter.app.FlutterApplication" android:label="@string/app_name" >
<activity
android:name=".ExampleActivity"
android:name="com.example.flutter.ExampleActivity"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason we're using com.example.platformservices on line 3 and com.example.flutter here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. The original manifest package was wrong, and I was unaware of the . prefix shorthand. Fixed.

)
)
);
return new Material(child: new Center(child: new Column(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you add trailing commas everywhere there are permitted and run dartfmt, you'll get code that matches https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo in this example.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

_latitude = reply['latitude'].toDouble();
_longitude = reply['longitude'].toDouble();
_locationRequest =
new PlatformChannel('geo').invokeMethod('getLocation', 'network');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

/// * [String]s
/// * [List]s of supported values
/// * [Map]s from strings to supported values
static const MessageCodec json = const _JSONCodec();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not make _JSONCodec a public class?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

/// * [Uint8List]s, [Int32List]s, [Int64List]s, [Float64List]s
/// * [List]s of supported values
/// * [Map]s from supported values to supported values
static const MessageCodec standard = const _StandardCodec();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.

The way you've structured this code, these two are special children. It would be better to structure things in a way that they're not special and someone could use a different codec (e.g., protobufs) easily.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

/// 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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As in the Java code review, I think we should disentangle the methodcall-based and message-based codecs. If necessary, we can have separate PlatformChannel classes to drive them. For example, we could have a PlatformMessageChannel and a PlatformMethodChannel (or some other better naming scheme.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

/// populate a [PlatformException].
///
/// All operations throw [FormatException], if conversion fails.
abstract class MessageCodec {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd break this file up into three separate files:

  1. One file that has the abstract classes for MessageCodec and MethodCodec.
  2. Another file that defines concrete implementations of these codecs for "standard" and "JSON". This separation will prevent these codecs from being special children because (2) will be able to import (1) but (1) should not import (2).
  3. A third file that contains the PlatformChannel class that has the pretty front-ends.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

}
}

class _WriteBuffer {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd move this out into foundation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.


void putInt32(int value) {
_fourBytes.setInt32(0, value);
buffer.addAll(_fourBytesAsList);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this really more efficient than doing the bit smashing manually?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just get a ByteData view of buffer.buffer and use setInt32 directly?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Direct bit-smashing applied for ints.

For doubles, it seems I would need to create a new ByteData view instance of buffer.buffer for each double added, because the buffer is dynamically extended. Retained original approach for now.

}
}

class _StandardCodec implements MessageCodec {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As requested in the Java review, please document this protocol. Here are examples where we've documented similar protocols:

https://github.com/flutter/engine/blob/master/lib/ui/text.dart#L184
https://github.com/flutter/engine/blob/master/lib/ui/painting.dart#L310

Doesn't have to be anything fancy. Just enough documentation so that someone could write, for example, a C# implementation of the protocol without having to reverse engineer the other implementations.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

]
)
)
new FutureBuilder<dynamic>(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice.

setState(() {
_latitude = reply['latitude'].toDouble();
_longitude = reply['longitude'].toDouble();
_locationRequest = new PlatformMethodChannel('geo').invokeMethod(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it recommend to make a new channel for each request? If so, I wonder if we can make it const constructible. No action needed. Just wondering aloud.

@@ -0,0 +1,204 @@
// Copyright 2015 The Chromium Authors. All rights reserved.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2017

if (Endianness.HOST_ENDIAN == Endianness.BIG_ENDIAN) {
_buffer.addAll(list.buffer.asUint8List(list.offsetInBytes, 4 * list.length));
} else {
for (final int value in list) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't usually put final here but we probably should.



/// Thrown to indicate that a platform interaction resulted in an error.
class PlatformException implements Exception {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why have a PlatformException that's different from FlutterException?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. These are errors reported from the other side of the channel. Would you be willing to add a bit more to the dartdoc explaining that? I thought these were errors in the mechanism itself rather than errors from the platform-specific implementation on the other side.

/// A message encoding/decoding mechanism.
///
/// Both operations throw [FormatException], if conversion fails.
abstract class MessageCodec<T> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beautiful

// * Strings are encoded using their UTF-8 representation. First the length
// of that in bytes is encoded using the expanding format, then follows the
// UTF-8 encoding itself.
// * Uint8Lists, Int32Lists, Int64Lists, and Float64Lists are encoded by first
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Document alignment and byte ordering.

// type byte (Lists are assumed to be heterogeneous).
// * Maps are encoded by first encoding their length in the expanding format,
// then follows the recursive encoding of each key/value pair, including the
// type byte for both (Maps are assumed to be heterogeneous).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perfect. Thank you.

ByteData encodeMessage(dynamic message) {
if (message == null) {
return null;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@@ -0,0 +1,198 @@
// Copyright 2016 The Chromium Authors. All rights reserved.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2017

PlatformMethodChannel(this.name, [this.codec = const StandardMethodCodec()]) {
assert(name != null);
assert(codec != null);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like it could be const constructible except for the asserts.

onListen: () async {
PlatformMessages.setBinaryMessageHandler(
name,
(ByteData reply) async {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'd usually combine this line and the previous one.

controller.addError(e);
}
}, onCancel: () async {
PlatformMessages.setBinaryMessageHandler(name, null);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be indented to match line 181, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Argh - the indentation is completely garbled in this class! Appears my IntelliJ does that on copy/paste. I moved nicely formatted code from PlatformMessages...

T get requireData {
if (hasData)
return data;
else if (hasError)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see it explicitly in the style guide, but generally we skip the else after return or throw statements. (Also applies to line 216

@abarth
Copy link
Contributor

abarth commented Mar 1, 2017

LGTM

@abarth
Copy link
Contributor

abarth commented Mar 1, 2017

If I were writing this patch, I'd smoke test each type of channel against a mock implementation. (I wouldn't try to hit every type of encoded message, but I'd just makes sure messages and errors can get through as expected.)

This looks great. Just some trivial nits.

@mravn-google
Copy link
Contributor Author

mravn-google commented Mar 1, 2017 via email

@mravn-google mravn-google merged commit 390993d into flutter:master Mar 1, 2017
ByteData _eightBytes;
Uint8List _eightBytesAsList;

WriteBuffer() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: here and in the class below, please move the constructor to above the fields. We put constructors first so that someone quickly glancing at the code can quickly tell if the class has an implicit constructor or explicit constructors.

}

static void _writeSize(WriteBuffer buffer, int value) {
assert(0 <= value && value < 0xffffffff);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not <= 0xffffffff ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it's so that you can expand the format later to store even bigger ints?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

your decoder just reads 0xffffffff as that, though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expanding the format further was not the intention. I just had myself confused, that's all. Fix upcoming.

buffer.putUint8(_kLargeInt);
final List<int> hex = UTF8.encoder.convert(value.toRadixString(16));
_writeSize(buffer, hex.length);
buffer.putUint8List(hex);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

haha, that escalated fast. :-P

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. It's a temporary solution. Make it work, make it right, make it fast...

list = data.buffer.asInt64List(data.offsetInBytes + position, length);
} else {
final ByteData invertedData = new ByteData(8 * length);
for (int i = 0; i < length; i++) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: no braces around single-line blocks


void _alignTo(int alignment) {
final int mod = position % alignment;
if (mod != 0) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: similar here and elsewhere

@@ -1,4 +1,4 @@
// Copyright 2016 The Chromium Authors. All rights reserved.
// Copyright 2017 The Chromium Authors. All rights reserved.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: (don't fix this, but for future knowledge): don't change the copyright dates.

@@ -104,6 +103,8 @@ class PlatformMessages {
///
/// Returns a [Future] which completes to the received response, decoded as a
/// UTF-8 string, or to an error, if the decoding fails.
///
/// Deprecated, use [PlatformMessageChannel.send] instead.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mark the deprecated methods using @deprecated?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(or just remove them)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's the plan. Just wanted to land this PR first, then I'll remove uses of these methods.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent.

@Hixie
Copy link
Contributor

Hixie commented Mar 1, 2017

This is great. Thanks so much for refactoring all this into this much saner mechanism.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Aug 14, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants