Integration test for channel communication (#9621)

diff --git a/bin/internal/engine.version b/bin/internal/engine.version
index 63bdadf..2482519 100644
--- a/bin/internal/engine.version
+++ b/bin/internal/engine.version
@@ -1 +1 @@
-a5b64899c9392183b4b5df7e89fa8f0950a8e509
+3211d2fca262ba348a0bbcecefeb3e1b0b832faf
diff --git a/dev/devicelab/bin/tasks/platform_channel_test.dart b/dev/devicelab/bin/tasks/channels_integration_test.dart
similarity index 77%
copy from dev/devicelab/bin/tasks/platform_channel_test.dart
copy to dev/devicelab/bin/tasks/channels_integration_test.dart
index d1afc67..8b60289 100644
--- a/dev/devicelab/bin/tasks/platform_channel_test.dart
+++ b/dev/devicelab/bin/tasks/channels_integration_test.dart
@@ -4,11 +4,11 @@
 
 import 'dart:async';
 
-import 'package:flutter_devicelab/tasks/perf_tests.dart';
 import 'package:flutter_devicelab/framework/adb.dart';
 import 'package:flutter_devicelab/framework/framework.dart';
+import 'package:flutter_devicelab/tasks/integration_tests.dart';
 
 Future<Null> main() async {
   deviceOperatingSystem = DeviceOperatingSystem.android;
-  await task(createPlatformServiceDriverTest());
+  await task(createChannelsIntegrationTest());
 }
diff --git a/dev/devicelab/bin/tasks/platform_channel_test_ios.dart b/dev/devicelab/bin/tasks/channels_integration_test_ios.dart
similarity index 77%
copy from dev/devicelab/bin/tasks/platform_channel_test_ios.dart
copy to dev/devicelab/bin/tasks/channels_integration_test_ios.dart
index 07760fb..9ac1a3a 100644
--- a/dev/devicelab/bin/tasks/platform_channel_test_ios.dart
+++ b/dev/devicelab/bin/tasks/channels_integration_test_ios.dart
@@ -4,11 +4,11 @@
 
 import 'dart:async';
 
-import 'package:flutter_devicelab/tasks/perf_tests.dart';
 import 'package:flutter_devicelab/framework/adb.dart';
 import 'package:flutter_devicelab/framework/framework.dart';
+import 'package:flutter_devicelab/tasks/integration_tests.dart';
 
 Future<Null> main() async {
   deviceOperatingSystem = DeviceOperatingSystem.ios;
-  await task(createPlatformServiceDriverTest());
+  await task(createChannelsIntegrationTest());
 }
diff --git a/dev/devicelab/bin/tasks/platform_channel_test.dart b/dev/devicelab/bin/tasks/channels_integration_test_win.dart
similarity index 77%
copy from dev/devicelab/bin/tasks/platform_channel_test.dart
copy to dev/devicelab/bin/tasks/channels_integration_test_win.dart
index d1afc67..8b60289 100644
--- a/dev/devicelab/bin/tasks/platform_channel_test.dart
+++ b/dev/devicelab/bin/tasks/channels_integration_test_win.dart
@@ -4,11 +4,11 @@
 
 import 'dart:async';
 
-import 'package:flutter_devicelab/tasks/perf_tests.dart';
 import 'package:flutter_devicelab/framework/adb.dart';
 import 'package:flutter_devicelab/framework/framework.dart';
+import 'package:flutter_devicelab/tasks/integration_tests.dart';
 
 Future<Null> main() async {
   deviceOperatingSystem = DeviceOperatingSystem.android;
-  await task(createPlatformServiceDriverTest());
+  await task(createChannelsIntegrationTest());
 }
diff --git a/dev/devicelab/bin/tasks/platform_channel_test.dart b/dev/devicelab/bin/tasks/platform_channel_sample_test.dart
similarity index 77%
rename from dev/devicelab/bin/tasks/platform_channel_test.dart
rename to dev/devicelab/bin/tasks/platform_channel_sample_test.dart
index d1afc67..38b10b3 100644
--- a/dev/devicelab/bin/tasks/platform_channel_test.dart
+++ b/dev/devicelab/bin/tasks/platform_channel_sample_test.dart
@@ -4,11 +4,11 @@
 
 import 'dart:async';
 
-import 'package:flutter_devicelab/tasks/perf_tests.dart';
 import 'package:flutter_devicelab/framework/adb.dart';
 import 'package:flutter_devicelab/framework/framework.dart';
+import 'package:flutter_devicelab/tasks/integration_tests.dart';
 
 Future<Null> main() async {
   deviceOperatingSystem = DeviceOperatingSystem.android;
-  await task(createPlatformServiceDriverTest());
+  await task(createPlatformChannelSampleTest());
 }
diff --git a/dev/devicelab/bin/tasks/platform_channel_test_ios.dart b/dev/devicelab/bin/tasks/platform_channel_sample_test_ios.dart
similarity index 77%
rename from dev/devicelab/bin/tasks/platform_channel_test_ios.dart
rename to dev/devicelab/bin/tasks/platform_channel_sample_test_ios.dart
index 07760fb..9feeaa5 100644
--- a/dev/devicelab/bin/tasks/platform_channel_test_ios.dart
+++ b/dev/devicelab/bin/tasks/platform_channel_sample_test_ios.dart
@@ -4,11 +4,11 @@
 
 import 'dart:async';
 
-import 'package:flutter_devicelab/tasks/perf_tests.dart';
 import 'package:flutter_devicelab/framework/adb.dart';
 import 'package:flutter_devicelab/framework/framework.dart';
+import 'package:flutter_devicelab/tasks/integration_tests.dart';
 
 Future<Null> main() async {
   deviceOperatingSystem = DeviceOperatingSystem.ios;
-  await task(createPlatformServiceDriverTest());
+  await task(createPlatformChannelSampleTest());
 }
diff --git a/dev/devicelab/lib/tasks/integration_tests.dart b/dev/devicelab/lib/tasks/integration_tests.dart
new file mode 100644
index 0000000..004e7e5
--- /dev/null
+++ b/dev/devicelab/lib/tasks/integration_tests.dart
@@ -0,0 +1,57 @@
+// Copyright (c) 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 '../framework/adb.dart';
+import '../framework/framework.dart';
+import '../framework/ios.dart';
+import '../framework/utils.dart';
+
+TaskFunction createChannelsIntegrationTest() {
+  return new DriverTest(
+    '${flutterDirectory.path}/dev/integration_tests/channels',
+    'lib/main.dart',
+  );
+}
+
+TaskFunction createPlatformChannelSampleTest() {
+  return new DriverTest(
+    '${flutterDirectory.path}/examples/platform_channel',
+    'test_driver/button_tap.dart',
+  );
+}
+
+class DriverTest {
+
+  DriverTest(this.testDirectory, this.testTarget);
+
+  final String testDirectory;
+  final String testTarget;
+
+  Future<TaskResult> call() {
+    return inDirectory(testDirectory, () async {
+      final Device device = await devices.workingDevice;
+      await device.unlock();
+      final String deviceId = device.deviceId;
+      await flutter('packages', options: <String>['get']);
+
+      if (deviceOperatingSystem == DeviceOperatingSystem.ios) {
+        await prepareProvisioningCertificates(testDirectory);
+        // This causes an Xcode project to be created.
+        await flutter('build', options: <String>['ios', '--profile']);
+      }
+
+      await flutter('drive', options: <String>[
+        '-v',
+        '-t',
+        testTarget,
+        '-d',
+        deviceId,
+      ]);
+
+      return new TaskResult.success(null);
+    });
+  }
+}
diff --git a/dev/devicelab/lib/tasks/perf_tests.dart b/dev/devicelab/lib/tasks/perf_tests.dart
index 87da30c..c59f216 100644
--- a/dev/devicelab/lib/tasks/perf_tests.dart
+++ b/dev/devicelab/lib/tasks/perf_tests.dart
@@ -10,14 +10,6 @@
 import '../framework/ios.dart';
 import '../framework/utils.dart';
 
-
-TaskFunction createPlatformServiceDriverTest() {
-  return new DriverTest(
-      '${flutterDirectory.path}/examples/platform_channel',
-      'test_driver/button_tap.dart',
-  );
-}
-
 TaskFunction createComplexLayoutScrollPerfTest() {
   return new PerfTest(
     '${flutterDirectory.path}/dev/benchmarks/complex_layout',
@@ -175,40 +167,6 @@
   }
 }
 
-
-class DriverTest {
-
-  DriverTest(this.testDirectory, this.testTarget);
-
-  final String testDirectory;
-  final String testTarget;
-
-  Future<TaskResult> call() {
-    return inDirectory(testDirectory, () async {
-      final Device device = await devices.workingDevice;
-      await device.unlock();
-      final String deviceId = device.deviceId;
-      await flutter('packages', options: <String>['get']);
-
-      if (deviceOperatingSystem == DeviceOperatingSystem.ios) {
-        await prepareProvisioningCertificates(testDirectory);
-        // This causes an Xcode project to be created.
-        await flutter('build', options: <String>['ios', '--profile']);
-      }
-
-      await flutter('drive', options: <String>[
-        '-v',
-        '-t',
-        testTarget,
-        '-d',
-        deviceId,
-      ]);
-
-      return new TaskResult.success(null);
-    });
-  }
-}
-
 class BuildTest {
 
   BuildTest(this.testDirectory);
diff --git a/dev/devicelab/manifest.yaml b/dev/devicelab/manifest.yaml
index 2aeb98d..b527fd3 100644
--- a/dev/devicelab/manifest.yaml
+++ b/dev/devicelab/manifest.yaml
@@ -83,7 +83,13 @@
     required_agent_capabilities: ["has-android-device"]
     flaky: true
 
-  platform_channel_test:
+  channels_integration_test:
+    description: >
+      Checks that platform channels work on Android.
+    stage: devicelab
+    required_agent_capabilities: ["has-android-device"]
+
+  platform_channel_sample_test:
     description: >
      Runs a driver test on the Platform Channel sample app on Android.
     stage: devicelab
@@ -154,7 +160,13 @@
 
   # iOS on-device tests
 
-  platform_channel_test_ios:
+  channels_integration_test_ios:
+    description: >
+      Checks that platform channels work on iOS.
+    stage: devicelab_ios
+    required_agent_capabilities: ["has-ios-device"]
+
+  platform_channel_sample_test_ios:
     description: >
       Runs a driver test on the Platform Channel sample app on iOS.
     stage: devicelab_ios
@@ -207,6 +219,13 @@
 
   # Tests running on Windows host
 
+  channels_integration_test_win:
+    description: >
+      Checks that platform channels work when app is launched from Windows.
+    stage: devicelab_win
+    required_agent_capabilities: ["windows"]
+    flaky: true
+
   flutter_gallery_win__build:
     description: >
       Collects various performance metrics from AOT builds of the Flutter
diff --git a/dev/integration_tests/README.md b/dev/integration_tests/README.md
new file mode 100644
index 0000000..b385c93
--- /dev/null
+++ b/dev/integration_tests/README.md
@@ -0,0 +1,4 @@
+Automated Flutter integration test suites. Each suite consists of a complete
+Flutter app and a `flutter_driver` specification that drives tests from the UI.
+
+Intended for use with devicelab.
diff --git a/dev/integration_tests/channels/.gitignore b/dev/integration_tests/channels/.gitignore
new file mode 100644
index 0000000..eb15c3d
--- /dev/null
+++ b/dev/integration_tests/channels/.gitignore
@@ -0,0 +1,10 @@
+.DS_Store
+.atom/
+.idea
+.packages
+.pub/
+build/
+ios/.generated/
+packages
+pubspec.lock
+.flutter-plugins
diff --git a/dev/integration_tests/channels/README.md b/dev/integration_tests/channels/README.md
new file mode 100644
index 0000000..6004698
--- /dev/null
+++ b/dev/integration_tests/channels/README.md
@@ -0,0 +1,3 @@
+# channels
+
+Integration test of platform channels.
diff --git a/dev/integration_tests/channels/android/.gitignore b/dev/integration_tests/channels/android/.gitignore
new file mode 100644
index 0000000..5c4ef82
--- /dev/null
+++ b/dev/integration_tests/channels/android/.gitignore
@@ -0,0 +1,12 @@
+*.iml
+.gradle
+/local.properties
+/.idea/workspace.xml
+/.idea/libraries
+.DS_Store
+/build
+/captures
+
+/gradle
+/gradlew
+/gradlew.bat
diff --git a/dev/integration_tests/channels/android/app/build.gradle b/dev/integration_tests/channels/android/app/build.gradle
new file mode 100644
index 0000000..4e2e46b
--- /dev/null
+++ b/dev/integration_tests/channels/android/app/build.gradle
@@ -0,0 +1,46 @@
+def localProperties = new Properties()
+def localPropertiesFile = rootProject.file('local.properties')
+if (localPropertiesFile.exists()) {
+    localPropertiesFile.withInputStream { stream ->
+        localProperties.load(stream)
+    }
+}
+
+def flutterRoot = localProperties.getProperty('flutter.sdk')
+if (flutterRoot == null) {
+    throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
+}
+
+apply plugin: 'com.android.application'
+apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
+
+android {
+    compileSdkVersion 25
+    buildToolsVersion '25.0.2'
+
+    lintOptions {
+        disable 'InvalidPackage'
+    }
+
+    defaultConfig {
+        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+    }
+
+    buildTypes {
+        release {
+            // TODO: Add your own signing config for the release build.
+            // Signing with the debug keys for now, so `flutter run --release` works.
+            signingConfig signingConfigs.debug
+        }
+    }
+}
+
+flutter {
+    source '../..'
+}
+
+dependencies {
+    androidTestCompile 'com.android.support:support-annotations:25.0.0'
+    androidTestCompile 'com.android.support.test:runner:0.5'
+    androidTestCompile 'com.android.support.test:rules:0.5'
+}
diff --git a/dev/integration_tests/channels/android/app/src/main/AndroidManifest.xml b/dev/integration_tests/channels/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..b63cc17
--- /dev/null
+++ b/dev/integration_tests/channels/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,32 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.yourcompany.channels"
+    android:versionCode="1"
+    android:versionName="0.0.1">
+
+    <uses-sdk android:minSdkVersion="16" android:targetSdkVersion="21" />
+
+    <!-- The INTERNET permission is required for development. Specifically,
+         flutter needs it to communicate with the running application
+         to allow setting breakpoints, to provide hot reload, etc.
+    -->
+    <uses-permission android:name="android.permission.INTERNET"/>
+
+    <!-- io.flutter.app.FlutterApplication is an android.app.Application that
+         calls FlutterMain.startInitialization(this); in its onCreate method.
+         In most cases you can leave this as-is, but you if you want to provide
+         additional functionality it is fine to subclass or reimplement
+         FlutterApplication and put your custom class here. -->
+    <application android:name="io.flutter.app.FlutterApplication" android:label="channels" android:icon="@mipmap/ic_launcher">
+        <activity android:name=".MainActivity"
+                  android:launchMode="singleTop"
+                  android:theme="@android:style/Theme.Black.NoTitleBar"
+                  android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection"
+                  android:hardwareAccelerated="true"
+                  android:windowSoftInputMode="adjustResize">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/dev/integration_tests/channels/android/app/src/main/java/com/yourcompany/channels/MainActivity.java b/dev/integration_tests/channels/android/app/src/main/java/com/yourcompany/channels/MainActivity.java
new file mode 100644
index 0000000..ebd9f18
--- /dev/null
+++ b/dev/integration_tests/channels/android/app/src/main/java/com/yourcompany/channels/MainActivity.java
@@ -0,0 +1,142 @@
+// 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.
+
+package com.yourcompany.channels;
+
+import java.nio.ByteBuffer;
+
+import android.os.Bundle;
+
+import io.flutter.app.FlutterActivity;
+import io.flutter.plugin.common.*;
+import io.flutter.plugins.PluginRegistry;
+
+public class MainActivity extends FlutterActivity {
+    PluginRegistry pluginRegistry;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        pluginRegistry = new PluginRegistry();
+        pluginRegistry.registerAll(this);
+
+        setupMessageHandshake(new BasicMessageChannel<>(getFlutterView(), "binary-msg", BinaryCodec.INSTANCE));
+        setupMessageHandshake(new BasicMessageChannel<>(getFlutterView(), "string-msg", StringCodec.INSTANCE));
+        setupMessageHandshake(new BasicMessageChannel<>(getFlutterView(), "json-msg", JSONMessageCodec.INSTANCE));
+        setupMessageHandshake(new BasicMessageChannel<>(getFlutterView(), "std-msg", StandardMessageCodec.INSTANCE));
+        setupMethodHandshake(new MethodChannel(getFlutterView(), "json-method", JSONMethodCodec.INSTANCE));
+        setupMethodHandshake(new MethodChannel(getFlutterView(), "std-method", StandardMethodCodec.INSTANCE));
+    }
+
+    private <T> void setupMessageHandshake(final BasicMessageChannel<T> channel) {
+        // On message receipt, do a send/reply/send round-trip in the other direction,
+        // then reply to the first message.
+        channel.setMessageHandler(new BasicMessageChannel.MessageHandler<T>() {
+            @Override
+            public void onMessage(final T message, final BasicMessageChannel.Reply<T> reply) {
+                final T messageEcho = echo(message);
+                channel.send(messageEcho, new BasicMessageChannel.Reply<T>() {
+                    @Override
+                    public void reply(T replyMessage) {
+                        channel.send(echo(replyMessage));
+                        reply.reply(messageEcho);
+                    }
+                });
+            }
+        });
+    }
+
+    // Outgoing ByteBuffer messages must be direct-allocated and payload placed between
+    // positon 0 and current position.
+    @SuppressWarnings("unchecked")
+    private <T> T echo(T message) {
+        if (message instanceof ByteBuffer) {
+            final ByteBuffer buffer = (ByteBuffer) message;
+            final ByteBuffer echo = ByteBuffer.allocateDirect(buffer.remaining());
+            echo.put(buffer);
+            return (T) echo;
+        }
+        return message;
+    }
+
+    private void setupMethodHandshake(final MethodChannel channel) {
+        channel.setMethodCallHandler(new MethodChannel.MethodCallHandler() {
+            @Override
+            public void onMethodCall(final MethodCall methodCall, final MethodChannel.Result result) {
+                switch (methodCall.method) {
+                    case "success":
+                        doSuccessHandshake(channel, methodCall, result);
+                        break;
+                    case "error":
+                        doErrorHandshake(channel, methodCall, result);
+                        break;
+                    default:
+                        doNotImplementedHandshake(channel, methodCall, result);
+                        break;
+
+                }
+            }
+        });
+    }
+
+    private void doSuccessHandshake(final MethodChannel channel, final MethodCall methodCall, final MethodChannel.Result result) {
+        channel.invokeMethod(methodCall.method, methodCall.arguments, new MethodChannel.Result() {
+            @Override
+            public void success(Object o) {
+                channel.invokeMethod(methodCall.method, o);
+                result.success(methodCall.arguments);
+            }
+
+            @Override
+            public void error(String code, String message, Object details) {
+                throw new AssertionError("Should not be called");
+            }
+
+            @Override
+            public void notImplemented() {
+                throw new AssertionError("Should not be called");
+            }
+        });
+    }
+
+    private void doErrorHandshake(final MethodChannel channel, final MethodCall methodCall, final MethodChannel.Result result) {
+        channel.invokeMethod(methodCall.method, methodCall.arguments, new MethodChannel.Result() {
+            @Override
+            public void success(Object o) {
+                throw new AssertionError("Should not be called");
+            }
+
+            @Override
+            public void error(String code, String message, Object details) {
+                channel.invokeMethod(methodCall.method, details);
+                result.error(code, message, methodCall.arguments);
+            }
+
+            @Override
+            public void notImplemented() {
+                throw new AssertionError("Should not be called");
+            }
+        });
+    }
+
+    private void doNotImplementedHandshake(final MethodChannel channel, final MethodCall methodCall, final MethodChannel.Result result) {
+        channel.invokeMethod(methodCall.method, methodCall.arguments, new MethodChannel.Result() {
+            @Override
+            public void success(Object o) {
+                throw new AssertionError("Should not be called");
+            }
+
+            @Override
+            public void error(String code, String message, Object details) {
+                throw new AssertionError("Should not be called");
+            }
+
+            @Override
+            public void notImplemented() {
+                channel.invokeMethod(methodCall.method, null);
+                result.notImplemented();
+            }
+        });
+    }
+}
diff --git a/dev/integration_tests/channels/android/app/src/main/java/io/flutter/plugins/PluginRegistry.java b/dev/integration_tests/channels/android/app/src/main/java/io/flutter/plugins/PluginRegistry.java
new file mode 100644
index 0000000..92f7a74
--- /dev/null
+++ b/dev/integration_tests/channels/android/app/src/main/java/io/flutter/plugins/PluginRegistry.java
@@ -0,0 +1,14 @@
+package io.flutter.plugins;
+
+import io.flutter.app.FlutterActivity;
+
+
+/**
+ * Generated file. Do not edit.
+ */
+
+public class PluginRegistry {
+
+    public void registerAll(FlutterActivity activity) {
+    }
+}
diff --git a/dev/integration_tests/channels/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/dev/integration_tests/channels/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..db77bb4
--- /dev/null
+++ b/dev/integration_tests/channels/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/dev/integration_tests/channels/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/dev/integration_tests/channels/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..17987b7
--- /dev/null
+++ b/dev/integration_tests/channels/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/dev/integration_tests/channels/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/dev/integration_tests/channels/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..09d4391
--- /dev/null
+++ b/dev/integration_tests/channels/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/dev/integration_tests/channels/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/dev/integration_tests/channels/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..d5f1c8d
--- /dev/null
+++ b/dev/integration_tests/channels/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/dev/integration_tests/channels/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/dev/integration_tests/channels/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..4d6372e
--- /dev/null
+++ b/dev/integration_tests/channels/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/dev/integration_tests/channels/android/build.gradle b/dev/integration_tests/channels/android/build.gradle
new file mode 100644
index 0000000..ee5325d
--- /dev/null
+++ b/dev/integration_tests/channels/android/build.gradle
@@ -0,0 +1,29 @@
+buildscript {
+    repositories {
+        jcenter()
+    }
+
+    dependencies {
+        classpath 'com.android.tools.build:gradle:2.2.3'
+    }
+}
+
+allprojects {
+    repositories {
+        jcenter()
+    }
+}
+
+rootProject.buildDir = '../build'
+subprojects {
+    project.buildDir = "${rootProject.buildDir}/${project.name}"
+    project.evaluationDependsOn(':app')
+}
+
+task clean(type: Delete) {
+    delete rootProject.buildDir
+}
+
+task wrapper(type: Wrapper) {
+    gradleVersion = '2.14.1'
+}
diff --git a/dev/integration_tests/channels/android/gradle.properties b/dev/integration_tests/channels/android/gradle.properties
new file mode 100644
index 0000000..8bd86f6
--- /dev/null
+++ b/dev/integration_tests/channels/android/gradle.properties
@@ -0,0 +1 @@
+org.gradle.jvmargs=-Xmx1536M
diff --git a/dev/integration_tests/channels/android/settings.gradle b/dev/integration_tests/channels/android/settings.gradle
new file mode 100644
index 0000000..115da6c
--- /dev/null
+++ b/dev/integration_tests/channels/android/settings.gradle
@@ -0,0 +1,15 @@
+include ':app'
+
+def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
+
+def plugins = new Properties()
+def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
+if (pluginsFile.exists()) {
+    pluginsFile.withInputStream { stream -> plugins.load(stream) }
+}
+
+plugins.each { name, path ->
+    def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
+    include ":$name"
+    project(":$name").projectDir = pluginDirectory
+}
diff --git a/dev/integration_tests/channels/ios/.gitignore b/dev/integration_tests/channels/ios/.gitignore
new file mode 100644
index 0000000..797c015
--- /dev/null
+++ b/dev/integration_tests/channels/ios/.gitignore
@@ -0,0 +1,39 @@
+.idea/
+.vagrant/
+.sconsign.dblite
+.svn/
+
+.DS_Store
+*.swp
+profile
+
+DerivedData/
+build/
+
+*.pbxuser
+*.mode1v3
+*.mode2v3
+*.perspectivev3
+
+!default.pbxuser
+!default.mode1v3
+!default.mode2v3
+!default.perspectivev3
+
+xcuserdata
+
+*.moved-aside
+
+*.pyc
+*sync/
+Icon?
+.tags*
+
+/Flutter/app.flx
+/Flutter/app.zip
+/Flutter/App.framework
+/Flutter/Flutter.framework
+/Flutter/Generated.xcconfig
+/ServiceDefinitions.json
+
+Pods/
diff --git a/dev/integration_tests/channels/ios/Flutter/AppFrameworkInfo.plist b/dev/integration_tests/channels/ios/Flutter/AppFrameworkInfo.plist
new file mode 100644
index 0000000..6c2de80
--- /dev/null
+++ b/dev/integration_tests/channels/ios/Flutter/AppFrameworkInfo.plist
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+  <key>CFBundleDevelopmentRegion</key>
+  <string>en</string>
+  <key>CFBundleExecutable</key>
+  <string>App</string>
+  <key>CFBundleIdentifier</key>
+  <string>io.flutter.flutter.app</string>
+  <key>CFBundleInfoDictionaryVersion</key>
+  <string>6.0</string>
+  <key>CFBundleName</key>
+  <string>App</string>
+  <key>CFBundlePackageType</key>
+  <string>FMWK</string>
+  <key>CFBundleShortVersionString</key>
+  <string>1.0</string>
+  <key>CFBundleSignature</key>
+  <string>????</string>
+  <key>CFBundleVersion</key>
+  <string>1.0</string>
+  <key>UIRequiredDeviceCapabilities</key>
+  <array>
+    <string>arm64</string>
+  </array>
+  <key>MinimumOSVersion</key>
+  <string>8.0</string>
+</dict>
+</plist>
diff --git a/dev/integration_tests/channels/ios/Flutter/Debug.xcconfig b/dev/integration_tests/channels/ios/Flutter/Debug.xcconfig
new file mode 100644
index 0000000..72c15fd
--- /dev/null
+++ b/dev/integration_tests/channels/ios/Flutter/Debug.xcconfig
@@ -0,0 +1,4 @@
+#include "Generated.xcconfig"
+#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
+
+#include "TestConfig.xcconfig"
diff --git a/dev/integration_tests/channels/ios/Flutter/Release.xcconfig b/dev/integration_tests/channels/ios/Flutter/Release.xcconfig
new file mode 100644
index 0000000..8d3ed2b
--- /dev/null
+++ b/dev/integration_tests/channels/ios/Flutter/Release.xcconfig
@@ -0,0 +1,4 @@
+#include "Generated.xcconfig"
+#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
+
+#include "TestConfig.xcconfig"
diff --git a/dev/integration_tests/channels/ios/Flutter/TestConfig.xcconfig b/dev/integration_tests/channels/ios/Flutter/TestConfig.xcconfig
new file mode 100644
index 0000000..6c52ef8
--- /dev/null
+++ b/dev/integration_tests/channels/ios/Flutter/TestConfig.xcconfig
@@ -0,0 +1,5 @@
+ProvisioningStyle=Manual
+CODE_SIGN_IDENTITY=iPhone Developer
+PROVISIONING_PROFILE=Xcode Managed Profile
+DEVELOPMENT_TEAM=...
+PROVISIONING_PROFILE_SPECIFIER=...
diff --git a/dev/integration_tests/channels/ios/Podfile b/dev/integration_tests/channels/ios/Podfile
new file mode 100644
index 0000000..74b3de0
--- /dev/null
+++ b/dev/integration_tests/channels/ios/Podfile
@@ -0,0 +1,38 @@
+# Uncomment this line to define a global platform for your project
+# platform :ios, '9.0'
+
+if ENV['FLUTTER_FRAMEWORK_DIR'] == nil
+  abort('Please set FLUTTER_FRAMEWORK_DIR to the directory containing Flutter.framework')
+end
+
+target 'Runner' do
+  use_frameworks!
+
+  # Pods for Runner
+
+  # Flutter Pods
+  pod 'Flutter', :path => ENV['FLUTTER_FRAMEWORK_DIR']
+
+  if File.exists? '../.flutter-plugins'
+    flutter_root = File.expand_path('..')
+    File.foreach('../.flutter-plugins') { |line|
+      plugin = line.split(pattern='=')
+      if plugin.length == 2
+        name = plugin[0].strip()
+        path = plugin[1].strip()
+        resolved_path = File.expand_path("#{path}/ios", flutter_root)
+        pod name, :path => resolved_path
+      else
+        puts "Invalid plugin specification: #{line}"
+      end
+    }
+  end
+end
+
+post_install do |installer|
+  installer.pods_project.targets.each do |target|
+    target.build_configurations.each do |config|
+      config.build_settings['ENABLE_BITCODE'] = 'NO'
+    end
+  end
+end
diff --git a/dev/integration_tests/channels/ios/Podfile.lock b/dev/integration_tests/channels/ios/Podfile.lock
new file mode 100644
index 0000000..8e25faa
--- /dev/null
+++ b/dev/integration_tests/channels/ios/Podfile.lock
@@ -0,0 +1,16 @@
+PODS:
+  - Flutter (1.0.0)
+
+DEPENDENCIES:
+  - Flutter (from `/Users/mravn/github/engine/src/out/ios_debug_unopt`)
+
+EXTERNAL SOURCES:
+  Flutter:
+    :path: "/Users/mravn/github/engine/src/out/ios_debug_unopt"
+
+SPEC CHECKSUMS:
+  Flutter: d674e78c937094a75ac71dd77e921e840bea3dbf
+
+PODFILE CHECKSUM: cc70c01bca487bebd110b87397f017f3b76a89f1
+
+COCOAPODS: 1.2.1
diff --git a/dev/integration_tests/channels/ios/Runner.xcodeproj/project.pbxproj b/dev/integration_tests/channels/ios/Runner.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..b61088c
--- /dev/null
+++ b/dev/integration_tests/channels/ios/Runner.xcodeproj/project.pbxproj
@@ -0,0 +1,505 @@
+// !$*UTF8*$!
+{
+	archiveVersion = 1;
+	classes = {
+	};
+	objectVersion = 46;
+	objects = {
+
+/* Begin PBXBuildFile section */
+		1498D2341E8E89220040F4C2 /* PluginRegistry.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* PluginRegistry.m */; };
+		3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
+		3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; };
+		3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		913134F849F6C8DEAC837F96 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C803412E9584DEAC0259A174 /* Pods_Runner.framework */; };
+		9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; };
+		9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; };
+		9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB31CF90195004384FC /* Generated.xcconfig */; };
+		9740EEBB1CF902C7004384FC /* app.flx in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB71CF902C7004384FC /* app.flx */; };
+		978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; };
+		97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; };
+		97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
+		97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
+		97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+		9705A1C41CF9048500538489 /* Embed Frameworks */ = {
+			isa = PBXCopyFilesBuildPhase;
+			buildActionMask = 2147483647;
+			dstPath = "";
+			dstSubfolderSpec = 10;
+			files = (
+				3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */,
+				9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */,
+			);
+			name = "Embed Frameworks";
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+		1498D2321E8E86230040F4C2 /* PluginRegistry.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PluginRegistry.h; sourceTree = "<group>"; };
+		1498D2331E8E89220040F4C2 /* PluginRegistry.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PluginRegistry.m; sourceTree = "<group>"; };
+		3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
+		3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = "<group>"; };
+		7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
+		7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
+		7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
+		9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
+		9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
+		9740EEB71CF902C7004384FC /* app.flx */ = {isa = PBXFileReference; lastKnownFileType = file; name = app.flx; path = Flutter/app.flx; sourceTree = "<group>"; };
+		9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = "<group>"; };
+		97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
+		97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
+		97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
+		97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
+		97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
+		97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+		C803412E9584DEAC0259A174 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+		97C146EB1CF9000F007C117D /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */,
+				3B80C3941E831B6300D905FE /* App.framework in Frameworks */,
+				913134F849F6C8DEAC837F96 /* Pods_Runner.framework in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+		840012C8B5EDBCF56B0E4AC1 /* Pods */ = {
+			isa = PBXGroup;
+			children = (
+			);
+			name = Pods;
+			sourceTree = "<group>";
+		};
+		9740EEB11CF90186004384FC /* Flutter */ = {
+			isa = PBXGroup;
+			children = (
+				9740EEB71CF902C7004384FC /* app.flx */,
+				3B80C3931E831B6300D905FE /* App.framework */,
+				3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
+				9740EEBA1CF902C7004384FC /* Flutter.framework */,
+				9740EEB21CF90195004384FC /* Debug.xcconfig */,
+				7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
+				9740EEB31CF90195004384FC /* Generated.xcconfig */,
+			);
+			name = Flutter;
+			sourceTree = "<group>";
+		};
+		97C146E51CF9000F007C117D = {
+			isa = PBXGroup;
+			children = (
+				9740EEB11CF90186004384FC /* Flutter */,
+				97C146F01CF9000F007C117D /* Runner */,
+				97C146EF1CF9000F007C117D /* Products */,
+				840012C8B5EDBCF56B0E4AC1 /* Pods */,
+				CF3B75C9A7D2FA2A4C99F110 /* Frameworks */,
+			);
+			sourceTree = "<group>";
+		};
+		97C146EF1CF9000F007C117D /* Products */ = {
+			isa = PBXGroup;
+			children = (
+				97C146EE1CF9000F007C117D /* Runner.app */,
+			);
+			name = Products;
+			sourceTree = "<group>";
+		};
+		97C146F01CF9000F007C117D /* Runner */ = {
+			isa = PBXGroup;
+			children = (
+				7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */,
+				7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */,
+				97C146FA1CF9000F007C117D /* Main.storyboard */,
+				97C146FD1CF9000F007C117D /* Assets.xcassets */,
+				97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
+				97C147021CF9000F007C117D /* Info.plist */,
+				97C146F11CF9000F007C117D /* Supporting Files */,
+				1498D2321E8E86230040F4C2 /* PluginRegistry.h */,
+				1498D2331E8E89220040F4C2 /* PluginRegistry.m */,
+			);
+			path = Runner;
+			sourceTree = "<group>";
+		};
+		97C146F11CF9000F007C117D /* Supporting Files */ = {
+			isa = PBXGroup;
+			children = (
+				97C146F21CF9000F007C117D /* main.m */,
+			);
+			name = "Supporting Files";
+			sourceTree = "<group>";
+		};
+		CF3B75C9A7D2FA2A4C99F110 /* Frameworks */ = {
+			isa = PBXGroup;
+			children = (
+				C803412E9584DEAC0259A174 /* Pods_Runner.framework */,
+			);
+			name = Frameworks;
+			sourceTree = "<group>";
+		};
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+		97C146ED1CF9000F007C117D /* Runner */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
+			buildPhases = (
+				AB1344B0443C71CD721E1BB7 /* [CP] Check Pods Manifest.lock */,
+				9740EEB61CF901F6004384FC /* Run Script */,
+				97C146EA1CF9000F007C117D /* Sources */,
+				97C146EB1CF9000F007C117D /* Frameworks */,
+				97C146EC1CF9000F007C117D /* Resources */,
+				9705A1C41CF9048500538489 /* Embed Frameworks */,
+				95BB15E9E1769C0D146AA592 /* [CP] Embed Pods Frameworks */,
+				532EA9D341340B1DCD08293D /* [CP] Copy Pods Resources */,
+				3B06AD1E1E4923F5004D2608 /* Thin Binary */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+			);
+			name = Runner;
+			productName = Runner;
+			productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
+			productType = "com.apple.product-type.application";
+		};
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+		97C146E61CF9000F007C117D /* Project object */ = {
+			isa = PBXProject;
+			attributes = {
+				LastUpgradeCheck = 0830;
+				ORGANIZATIONNAME = "The Chromium Authors";
+				TargetAttributes = {
+					97C146ED1CF9000F007C117D = {
+						CreatedOnToolsVersion = 7.3.1;
+						DevelopmentTeam = AQ7UHDBEXJ;
+						ProvisioningStyle = Automatic;
+					};
+				};
+			};
+			buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
+			compatibilityVersion = "Xcode 3.2";
+			developmentRegion = English;
+			hasScannedForEncodings = 0;
+			knownRegions = (
+				en,
+				Base,
+			);
+			mainGroup = 97C146E51CF9000F007C117D;
+			productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
+			projectDirPath = "";
+			projectRoot = "";
+			targets = (
+				97C146ED1CF9000F007C117D /* Runner */,
+			);
+		};
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+		97C146EC1CF9000F007C117D /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				9740EEBB1CF902C7004384FC /* app.flx in Resources */,
+				97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
+				9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */,
+				3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
+				9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */,
+				97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
+				97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+		3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputPaths = (
+			);
+			name = "Thin Binary";
+			outputPaths = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin";
+		};
+		532EA9D341340B1DCD08293D /* [CP] Copy Pods Resources */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputPaths = (
+			);
+			name = "[CP] Copy Pods Resources";
+			outputPaths = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
+			showEnvVarsInLog = 0;
+		};
+		95BB15E9E1769C0D146AA592 /* [CP] Embed Pods Frameworks */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputPaths = (
+			);
+			name = "[CP] Embed Pods Frameworks";
+			outputPaths = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
+			showEnvVarsInLog = 0;
+		};
+		9740EEB61CF901F6004384FC /* Run Script */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputPaths = (
+			);
+			name = "Run Script";
+			outputPaths = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
+		};
+		AB1344B0443C71CD721E1BB7 /* [CP] Check Pods Manifest.lock */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputPaths = (
+			);
+			name = "[CP] Check Pods Manifest.lock";
+			outputPaths = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n    # print error to STDERR\n    echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n    exit 1\nfi\n";
+			showEnvVarsInLog = 0;
+		};
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+		97C146EA1CF9000F007C117D /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */,
+				97C146F31CF9000F007C117D /* main.m in Sources */,
+				1498D2341E8E89220040F4C2 /* PluginRegistry.m in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXVariantGroup section */
+		97C146FA1CF9000F007C117D /* Main.storyboard */ = {
+			isa = PBXVariantGroup;
+			children = (
+				97C146FB1CF9000F007C117D /* Base */,
+			);
+			name = Main.storyboard;
+			sourceTree = "<group>";
+		};
+		97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
+			isa = PBXVariantGroup;
+			children = (
+				97C147001CF9000F007C117D /* Base */,
+			);
+			name = LaunchScreen.storyboard;
+			sourceTree = "<group>";
+		};
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+		97C147031CF9000F007C117D /* Debug */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+				CLANG_CXX_LIBRARY = "libc++";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+				COPY_PHASE_STRIP = NO;
+				DEBUG_INFORMATION_FORMAT = dwarf;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				ENABLE_TESTABILITY = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu99;
+				GCC_DYNAMIC_NO_PIC = NO;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_OPTIMIZATION_LEVEL = 0;
+				GCC_PREPROCESSOR_DEFINITIONS = (
+					"DEBUG=1",
+					"$(inherited)",
+				);
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				IPHONEOS_DEPLOYMENT_TARGET = 8.0;
+				MTL_ENABLE_DEBUG_INFO = YES;
+				ONLY_ACTIVE_ARCH = YES;
+				SDKROOT = iphoneos;
+				TARGETED_DEVICE_FAMILY = "1,2";
+			};
+			name = Debug;
+		};
+		97C147041CF9000F007C117D /* Release */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+				CLANG_CXX_LIBRARY = "libc++";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+				COPY_PHASE_STRIP = NO;
+				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+				ENABLE_NS_ASSERTIONS = NO;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu99;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				IPHONEOS_DEPLOYMENT_TARGET = 8.0;
+				MTL_ENABLE_DEBUG_INFO = NO;
+				SDKROOT = iphoneos;
+				TARGETED_DEVICE_FAMILY = "1,2";
+				VALIDATE_PRODUCT = YES;
+			};
+			name = Release;
+		};
+		97C147061CF9000F007C117D /* Debug */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
+			buildSettings = {
+				ARCHS = arm64;
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				CODE_SIGN_IDENTITY = "iPhone Developer";
+				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+				DEVELOPMENT_TEAM = AQ7UHDBEXJ;
+				ENABLE_BITCODE = NO;
+				FRAMEWORK_SEARCH_PATHS = (
+					"$(inherited)",
+					"$(PROJECT_DIR)/Flutter",
+				);
+				INFOPLIST_FILE = Runner/Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+				LIBRARY_SEARCH_PATHS = (
+					"$(inherited)",
+					"$(PROJECT_DIR)/Flutter",
+				);
+				PRODUCT_BUNDLE_IDENTIFIER = com.yourcompany.channels;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				PROVISIONING_PROFILE = "";
+				PROVISIONING_PROFILE_SPECIFIER = "";
+				ProvisioningStyle = Automatic;
+			};
+			name = Debug;
+		};
+		97C147071CF9000F007C117D /* Release */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+			buildSettings = {
+				ARCHS = arm64;
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				CODE_SIGN_IDENTITY = "iPhone Developer";
+				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+				DEVELOPMENT_TEAM = AQ7UHDBEXJ;
+				ENABLE_BITCODE = NO;
+				FRAMEWORK_SEARCH_PATHS = (
+					"$(inherited)",
+					"$(PROJECT_DIR)/Flutter",
+				);
+				INFOPLIST_FILE = Runner/Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+				LIBRARY_SEARCH_PATHS = (
+					"$(inherited)",
+					"$(PROJECT_DIR)/Flutter",
+				);
+				PRODUCT_BUNDLE_IDENTIFIER = com.yourcompany.channels;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				PROVISIONING_PROFILE = "";
+				PROVISIONING_PROFILE_SPECIFIER = "";
+				ProvisioningStyle = Automatic;
+			};
+			name = Release;
+		};
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+		97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				97C147031CF9000F007C117D /* Debug */,
+				97C147041CF9000F007C117D /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				97C147061CF9000F007C117D /* Debug */,
+				97C147071CF9000F007C117D /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+/* End XCConfigurationList section */
+	};
+	rootObject = 97C146E61CF9000F007C117D /* Project object */;
+}
diff --git a/dev/integration_tests/channels/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/dev/integration_tests/channels/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..21a3cc1
--- /dev/null
+++ b/dev/integration_tests/channels/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+   version = "1.0">
+   <FileRef
+      location = "group:Runner.xcodeproj">
+   </FileRef>
+   <FileRef
+      location = "group:Pods/Pods.xcodeproj">
+   </FileRef>
+</Workspace>
diff --git a/dev/integration_tests/channels/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/dev/integration_tests/channels/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
new file mode 100644
index 0000000..1c95807
--- /dev/null
+++ b/dev/integration_tests/channels/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "0830"
+   version = "1.3">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+               BuildableName = "Runner.app"
+               BlueprintName = "Runner"
+               ReferencedContainer = "container:Runner.xcodeproj">
+            </BuildableReference>
+         </BuildActionEntry>
+      </BuildActionEntries>
+   </BuildAction>
+   <TestAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES">
+      <Testables>
+      </Testables>
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+            BuildableName = "Runner.app"
+            BlueprintName = "Runner"
+            ReferencedContainer = "container:Runner.xcodeproj">
+         </BuildableReference>
+      </MacroExpansion>
+      <AdditionalOptions>
+      </AdditionalOptions>
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      debugServiceExtension = "internal"
+      allowLocationSimulation = "YES">
+      <BuildableProductRunnable
+         runnableDebuggingMode = "0">
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+            BuildableName = "Runner.app"
+            BlueprintName = "Runner"
+            ReferencedContainer = "container:Runner.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+      <AdditionalOptions>
+      </AdditionalOptions>
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Release"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      debugDocumentVersioning = "YES">
+      <BuildableProductRunnable
+         runnableDebuggingMode = "0">
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+            BuildableName = "Runner.app"
+            BlueprintName = "Runner"
+            ReferencedContainer = "container:Runner.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>
diff --git a/dev/integration_tests/channels/ios/Runner.xcworkspace/contents.xcworkspacedata b/dev/integration_tests/channels/ios/Runner.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..21a3cc1
--- /dev/null
+++ b/dev/integration_tests/channels/ios/Runner.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+   version = "1.0">
+   <FileRef
+      location = "group:Runner.xcodeproj">
+   </FileRef>
+   <FileRef
+      location = "group:Pods/Pods.xcodeproj">
+   </FileRef>
+</Workspace>
diff --git a/dev/integration_tests/channels/ios/Runner/AppDelegate.h b/dev/integration_tests/channels/ios/Runner/AppDelegate.h
new file mode 100644
index 0000000..138b680
--- /dev/null
+++ b/dev/integration_tests/channels/ios/Runner/AppDelegate.h
@@ -0,0 +1,10 @@
+// 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 <UIKit/UIKit.h>
+#import <Flutter/Flutter.h>
+
+@interface AppDelegate : FlutterAppDelegate
+
+@end
diff --git a/dev/integration_tests/channels/ios/Runner/AppDelegate.m b/dev/integration_tests/channels/ios/Runner/AppDelegate.m
new file mode 100644
index 0000000..94c06d8
--- /dev/null
+++ b/dev/integration_tests/channels/ios/Runner/AppDelegate.m
@@ -0,0 +1,99 @@
+// 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.
+
+#include "AppDelegate.h"
+#include "PluginRegistry.h"
+
+@implementation AppDelegate {
+  PluginRegistry *plugins;
+}
+
+- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
+  // Override point for customization after application launch.
+  FlutterViewController *flutterController =
+      (FlutterViewController *)self.window.rootViewController;
+  plugins = [[PluginRegistry alloc] initWithController:flutterController];
+
+  [self setupMessagingHandshakeOnChannel:
+    [FlutterBasicMessageChannel messageChannelWithName:@"binary-msg"
+                                       binaryMessenger:flutterController
+                                                 codec:[FlutterBinaryCodec sharedInstance]]];
+  [self setupMessagingHandshakeOnChannel:
+    [FlutterBasicMessageChannel messageChannelWithName:@"string-msg"
+                                       binaryMessenger:flutterController
+                                                 codec:[FlutterStringCodec sharedInstance]]];
+  [self setupMessagingHandshakeOnChannel:
+    [FlutterBasicMessageChannel messageChannelWithName:@"json-msg"
+                                       binaryMessenger:flutterController
+                                                 codec:[FlutterJSONMessageCodec sharedInstance]]];
+  [self setupMessagingHandshakeOnChannel:
+    [FlutterBasicMessageChannel messageChannelWithName:@"std-msg"
+                                       binaryMessenger:flutterController
+                                                 codec:[FlutterStandardMessageCodec sharedInstance]]];
+  [self setupMethodCallSuccessHandshakeOnChannel:
+    [FlutterMethodChannel methodChannelWithName:@"json-method"
+                                binaryMessenger:flutterController
+                                          codec:[FlutterJSONMethodCodec sharedInstance]]];
+  [self setupMethodCallSuccessHandshakeOnChannel:
+    [FlutterMethodChannel methodChannelWithName:@"std-method"
+                                binaryMessenger:flutterController
+                                          codec:[FlutterStandardMethodCodec sharedInstance]]];
+  return YES;
+}
+
+- (void)setupMessagingHandshakeOnChannel:(FlutterBasicMessageChannel*)channel {
+  [channel setMessageHandler:^(id message, FlutterReply reply) {
+    [channel sendMessage:message reply:^(id messageReply) {
+      [channel sendMessage:messageReply];
+      reply(message);
+    }];
+  }];
+}
+
+- (void)setupMethodCallSuccessHandshakeOnChannel:(FlutterMethodChannel*)channel {
+  [channel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
+    if ([call.method isEqual:@"success"]) {
+      [channel invokeMethod:call.method arguments:call.arguments result:^(id value) {
+        [channel invokeMethod:call.method arguments:value];
+        result(call.arguments);
+      }];
+    } else if ([call.method isEqual:@"error"]) {
+      [channel invokeMethod:call.method arguments:call.arguments result:^(id value) {
+        FlutterError* error = (FlutterError*) value;
+        [channel invokeMethod:call.method arguments:error.details];
+        result(error);
+      }];
+    } else {
+      [channel invokeMethod:call.method arguments:call.arguments result:^(id value) {
+        NSAssert(value == FlutterMethodNotImplemented, @"Result must be not implemented");
+        [channel invokeMethod:call.method arguments:nil];
+        result(FlutterMethodNotImplemented);
+      }];
+    }
+  }];
+}
+
+- (void)applicationWillResignActive:(UIApplication *)application {
+    // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
+    // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
+}
+
+- (void)applicationDidEnterBackground:(UIApplication *)application {
+    // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
+    // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
+}
+
+- (void)applicationWillEnterForeground:(UIApplication *)application {
+    // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
+}
+
+- (void)applicationDidBecomeActive:(UIApplication *)application {
+    // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
+}
+
+- (void)applicationWillTerminate:(UIApplication *)application {
+    // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
+}
+
+@end
diff --git a/dev/integration_tests/channels/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/dev/integration_tests/channels/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..d22f10b
--- /dev/null
+++ b/dev/integration_tests/channels/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,116 @@
+{
+  "images" : [
+    {
+      "size" : "20x20",
+      "idiom" : "iphone",
+      "filename" : "Icon-App-20x20@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "20x20",
+      "idiom" : "iphone",
+      "filename" : "Icon-App-20x20@3x.png",
+      "scale" : "3x"
+    },
+    {
+      "size" : "29x29",
+      "idiom" : "iphone",
+      "filename" : "Icon-App-29x29@1x.png",
+      "scale" : "1x"
+    },
+    {
+      "size" : "29x29",
+      "idiom" : "iphone",
+      "filename" : "Icon-App-29x29@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "29x29",
+      "idiom" : "iphone",
+      "filename" : "Icon-App-29x29@3x.png",
+      "scale" : "3x"
+    },
+    {
+      "size" : "40x40",
+      "idiom" : "iphone",
+      "filename" : "Icon-App-40x40@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "40x40",
+      "idiom" : "iphone",
+      "filename" : "Icon-App-40x40@3x.png",
+      "scale" : "3x"
+    },
+    {
+      "size" : "60x60",
+      "idiom" : "iphone",
+      "filename" : "Icon-App-60x60@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "60x60",
+      "idiom" : "iphone",
+      "filename" : "Icon-App-60x60@3x.png",
+      "scale" : "3x"
+    },
+    {
+      "size" : "20x20",
+      "idiom" : "ipad",
+      "filename" : "Icon-App-20x20@1x.png",
+      "scale" : "1x"
+    },
+    {
+      "size" : "20x20",
+      "idiom" : "ipad",
+      "filename" : "Icon-App-20x20@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "29x29",
+      "idiom" : "ipad",
+      "filename" : "Icon-App-29x29@1x.png",
+      "scale" : "1x"
+    },
+    {
+      "size" : "29x29",
+      "idiom" : "ipad",
+      "filename" : "Icon-App-29x29@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "40x40",
+      "idiom" : "ipad",
+      "filename" : "Icon-App-40x40@1x.png",
+      "scale" : "1x"
+    },
+    {
+      "size" : "40x40",
+      "idiom" : "ipad",
+      "filename" : "Icon-App-40x40@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "76x76",
+      "idiom" : "ipad",
+      "filename" : "Icon-App-76x76@1x.png",
+      "scale" : "1x"
+    },
+    {
+      "size" : "76x76",
+      "idiom" : "ipad",
+      "filename" : "Icon-App-76x76@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "83.5x83.5",
+      "idiom" : "ipad",
+      "filename" : "Icon-App-83.5x83.5@2x.png",
+      "scale" : "2x"
+    }
+  ],
+  "info" : {
+    "version" : 1,
+    "author" : "xcode"
+  }
+}
diff --git a/dev/integration_tests/channels/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/dev/integration_tests/channels/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
new file mode 100644
index 0000000..28c6bf0
--- /dev/null
+++ b/dev/integration_tests/channels/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
Binary files differ
diff --git a/dev/integration_tests/channels/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/dev/integration_tests/channels/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
new file mode 100644
index 0000000..2ccbfd9
--- /dev/null
+++ b/dev/integration_tests/channels/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
Binary files differ
diff --git a/dev/integration_tests/channels/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/dev/integration_tests/channels/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
new file mode 100644
index 0000000..f091b6b
--- /dev/null
+++ b/dev/integration_tests/channels/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
Binary files differ
diff --git a/dev/integration_tests/channels/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/dev/integration_tests/channels/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
new file mode 100644
index 0000000..4cde121
--- /dev/null
+++ b/dev/integration_tests/channels/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
Binary files differ
diff --git a/dev/integration_tests/channels/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/dev/integration_tests/channels/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
new file mode 100644
index 0000000..d0ef06e
--- /dev/null
+++ b/dev/integration_tests/channels/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
Binary files differ
diff --git a/dev/integration_tests/channels/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/dev/integration_tests/channels/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
new file mode 100644
index 0000000..dcdc230
--- /dev/null
+++ b/dev/integration_tests/channels/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
Binary files differ
diff --git a/dev/integration_tests/channels/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/dev/integration_tests/channels/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
new file mode 100644
index 0000000..2ccbfd9
--- /dev/null
+++ b/dev/integration_tests/channels/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
Binary files differ
diff --git a/dev/integration_tests/channels/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/dev/integration_tests/channels/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
new file mode 100644
index 0000000..c8f9ed8
--- /dev/null
+++ b/dev/integration_tests/channels/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
Binary files differ
diff --git a/dev/integration_tests/channels/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/dev/integration_tests/channels/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
new file mode 100644
index 0000000..a6d6b86
--- /dev/null
+++ b/dev/integration_tests/channels/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
Binary files differ
diff --git a/dev/integration_tests/channels/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/dev/integration_tests/channels/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
new file mode 100644
index 0000000..a6d6b86
--- /dev/null
+++ b/dev/integration_tests/channels/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
Binary files differ
diff --git a/dev/integration_tests/channels/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/dev/integration_tests/channels/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
new file mode 100644
index 0000000..75b2d16
--- /dev/null
+++ b/dev/integration_tests/channels/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
Binary files differ
diff --git a/dev/integration_tests/channels/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/dev/integration_tests/channels/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
new file mode 100644
index 0000000..c4df70d
--- /dev/null
+++ b/dev/integration_tests/channels/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
Binary files differ
diff --git a/dev/integration_tests/channels/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/dev/integration_tests/channels/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
new file mode 100644
index 0000000..6a84f41
--- /dev/null
+++ b/dev/integration_tests/channels/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
Binary files differ
diff --git a/dev/integration_tests/channels/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/dev/integration_tests/channels/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
new file mode 100644
index 0000000..d0e1f58
--- /dev/null
+++ b/dev/integration_tests/channels/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
Binary files differ
diff --git a/dev/integration_tests/channels/ios/Runner/Base.lproj/LaunchScreen.storyboard b/dev/integration_tests/channels/ios/Runner/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 0000000..ebf48f6
--- /dev/null
+++ b/dev/integration_tests/channels/ios/Runner/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" initialViewController="01J-lp-oVM">
+    <dependencies>
+        <deployment identifier="iOS"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
+    </dependencies>
+    <scenes>
+        <!--View Controller-->
+        <scene sceneID="EHf-IW-A2E">
+            <objects>
+                <viewController id="01J-lp-oVM" sceneMemberID="viewController">
+                    <layoutGuides>
+                        <viewControllerLayoutGuide type="top" id="Llm-lL-Icb"/>
+                        <viewControllerLayoutGuide type="bottom" id="xb3-aO-Qok"/>
+                    </layoutGuides>
+                    <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
+                        <rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
+                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+                        <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
+                    </view>
+                </viewController>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
+            </objects>
+            <point key="canvasLocation" x="53" y="375"/>
+        </scene>
+    </scenes>
+</document>
diff --git a/dev/integration_tests/channels/ios/Runner/Base.lproj/Main.storyboard b/dev/integration_tests/channels/ios/Runner/Base.lproj/Main.storyboard
new file mode 100644
index 0000000..f3c2851
--- /dev/null
+++ b/dev/integration_tests/channels/ios/Runner/Base.lproj/Main.storyboard
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
+    <dependencies>
+        <deployment identifier="iOS"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
+    </dependencies>
+    <scenes>
+        <!--Flutter View Controller-->
+        <scene sceneID="tne-QT-ifu">
+            <objects>
+                <viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
+                    <layoutGuides>
+                        <viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
+                        <viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
+                    </layoutGuides>
+                    <view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
+                        <rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
+                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+                        <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
+                    </view>
+                </viewController>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
+            </objects>
+        </scene>
+    </scenes>
+</document>
diff --git a/dev/integration_tests/channels/ios/Runner/Info.plist b/dev/integration_tests/channels/ios/Runner/Info.plist
new file mode 100644
index 0000000..04cb09d
--- /dev/null
+++ b/dev/integration_tests/channels/ios/Runner/Info.plist
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>CFBundleDevelopmentRegion</key>
+	<string>en</string>
+	<key>CFBundleExecutable</key>
+	<string>$(EXECUTABLE_NAME)</string>
+	<key>CFBundleIdentifier</key>
+	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+	<key>CFBundleInfoDictionaryVersion</key>
+	<string>6.0</string>
+	<key>CFBundleName</key>
+	<string>channels</string>
+	<key>CFBundlePackageType</key>
+	<string>APPL</string>
+	<key>CFBundleShortVersionString</key>
+	<string>1.0</string>
+	<key>CFBundleSignature</key>
+	<string>????</string>
+	<key>CFBundleVersion</key>
+	<string>1</string>
+	<key>LSRequiresIPhoneOS</key>
+	<true/>
+	<key>UILaunchStoryboardName</key>
+	<string>LaunchScreen</string>
+	<key>UIMainStoryboardFile</key>
+	<string>Main</string>
+	<key>UIRequiredDeviceCapabilities</key>
+	<array>
+		<string>arm64</string>
+	</array>
+	<key>UISupportedInterfaceOrientations</key>
+	<array>
+		<string>UIInterfaceOrientationPortrait</string>
+		<string>UIInterfaceOrientationLandscapeLeft</string>
+		<string>UIInterfaceOrientationLandscapeRight</string>
+	</array>
+	<key>UISupportedInterfaceOrientations~ipad</key>
+	<array>
+		<string>UIInterfaceOrientationPortrait</string>
+		<string>UIInterfaceOrientationPortraitUpsideDown</string>
+		<string>UIInterfaceOrientationLandscapeLeft</string>
+		<string>UIInterfaceOrientationLandscapeRight</string>
+	</array>
+	<key>UIViewControllerBasedStatusBarAppearance</key>
+	<false/>
+</dict>
+</plist>
diff --git a/dev/integration_tests/channels/ios/Runner/PluginRegistry.h b/dev/integration_tests/channels/ios/Runner/PluginRegistry.h
new file mode 100644
index 0000000..df039db
--- /dev/null
+++ b/dev/integration_tests/channels/ios/Runner/PluginRegistry.h
@@ -0,0 +1,18 @@
+//
+//  Generated file. Do not edit.
+//
+
+#ifndef PluginRegistry_h
+#define PluginRegistry_h
+
+#import <Flutter/Flutter.h>
+
+
+@interface PluginRegistry : NSObject
+
+
+- (instancetype)initWithController:(FlutterViewController *)controller;
+
+@end
+
+#endif /* PluginRegistry_h */
diff --git a/dev/integration_tests/channels/ios/Runner/PluginRegistry.m b/dev/integration_tests/channels/ios/Runner/PluginRegistry.m
new file mode 100644
index 0000000..0a34729
--- /dev/null
+++ b/dev/integration_tests/channels/ios/Runner/PluginRegistry.m
@@ -0,0 +1,15 @@
+//
+//  Generated file. Do not edit.
+//
+
+#import "PluginRegistry.h"
+
+@implementation PluginRegistry
+
+- (instancetype)initWithController:(FlutterViewController *)controller {
+  if (self = [super init]) {
+  }
+  return self;
+}
+
+@end
diff --git a/dev/integration_tests/channels/ios/Runner/main.m b/dev/integration_tests/channels/ios/Runner/main.m
new file mode 100644
index 0000000..945818b
--- /dev/null
+++ b/dev/integration_tests/channels/ios/Runner/main.m
@@ -0,0 +1,14 @@
+// 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 <UIKit/UIKit.h>
+#import <Flutter/Flutter.h>
+#import "AppDelegate.h"
+
+int main(int argc, char * argv[]) {
+    @autoreleasepool {
+        return UIApplicationMain(argc, argv, nil,
+                                 NSStringFromClass([AppDelegate class]));
+    }
+}
diff --git a/dev/integration_tests/channels/lib/main.dart b/dev/integration_tests/channels/lib/main.dart
new file mode 100644
index 0000000..025f3e1
--- /dev/null
+++ b/dev/integration_tests/channels/lib/main.dart
@@ -0,0 +1,193 @@
+// 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/material.dart';
+import 'package:flutter_driver/driver_extension.dart';
+
+import 'src/basic_messaging.dart';
+import 'src/method_calls.dart';
+import 'src/test_step.dart';
+
+void main() {
+  enableFlutterDriverExtension();
+  runApp(new TestApp());
+}
+
+class TestApp extends StatefulWidget {
+  @override
+  _TestAppState createState() => new _TestAppState();
+}
+
+class _TestAppState extends State<TestApp> {
+  static final List<dynamic> aList = <dynamic>[
+    false,
+    0,
+    0.0,
+    'hello',
+    <dynamic>[
+      <String, dynamic>{'key': 42}
+    ],
+  ];
+  static final Map<String, dynamic> aMap = <String, dynamic>{
+    'a': false,
+    'b': 0,
+    'c': 0.0,
+    'd': 'hello',
+    'e': <dynamic>[
+      <String, dynamic>{'key': 42}
+    ]
+  };
+  static final Uint8List someUint8s = new Uint8List.fromList(<int>[
+    0xBA,
+    0x5E,
+    0xBA,
+    0x11,
+  ]);
+  static final Int32List someInt32s = new Int32List.fromList(<int>[
+    -0x7fffffff - 1,
+    0,
+    0x7fffffff,
+  ]);
+  static final Int64List someInt64s = new Int64List.fromList(<int>[
+    -0x7fffffffffffffff - 1,
+    0,
+    0x7fffffffffffffff,
+  ]);
+  static final Float64List someFloat64s =
+      new Float64List.fromList(<double>[
+    double.NAN,
+    double.NEGATIVE_INFINITY,
+    -double.MAX_FINITE,
+    -double.MIN_POSITIVE,
+    -0.0,
+    0.0,
+    double.MIN_POSITIVE,
+    double.MAX_FINITE,
+    double.INFINITY,
+  ]);
+  static final List<TestStep> steps = <TestStep>[
+    () => methodCallJsonSuccessHandshake(null),
+    () => methodCallJsonSuccessHandshake(true),
+    () => methodCallJsonSuccessHandshake(7),
+    () => methodCallJsonSuccessHandshake('world'),
+    () => methodCallJsonSuccessHandshake(aList),
+    () => methodCallJsonSuccessHandshake(aMap),
+    () => methodCallJsonNotImplementedHandshake(),
+    () => methodCallStandardSuccessHandshake(null),
+    () => methodCallStandardSuccessHandshake(true),
+    () => methodCallStandardSuccessHandshake(7),
+    () => methodCallStandardSuccessHandshake('world'),
+    () => methodCallStandardSuccessHandshake(aList),
+    () => methodCallStandardSuccessHandshake(aMap),
+    () => methodCallJsonErrorHandshake(null),
+    () => methodCallJsonErrorHandshake('world'),
+    () => methodCallStandardErrorHandshake(null),
+    () => methodCallStandardErrorHandshake('world'),
+    () => methodCallStandardNotImplementedHandshake(),
+    () => basicBinaryHandshake(null),
+    () => basicBinaryHandshake(new ByteData(0)),
+    () => basicBinaryHandshake(new ByteData(4)..setUint32(0, 0x12345678)),
+    () => basicStringHandshake('hello, world'),
+    () => basicStringHandshake('hello \u263A \u{1f602} unicode'),
+    () => basicStringHandshake(''),
+    () => basicStringHandshake(null),
+    () => basicJsonHandshake(null),
+    () => basicJsonHandshake(true),
+    () => basicJsonHandshake(false),
+    () => basicJsonHandshake(0),
+    () => basicJsonHandshake(-7),
+    () => basicJsonHandshake(7),
+    () => basicJsonHandshake(1 << 32),
+    () => basicJsonHandshake(1 << 56),
+    () => basicJsonHandshake(0.0),
+    () => basicJsonHandshake(-7.0),
+    () => basicJsonHandshake(7.0),
+    () => basicJsonHandshake(''),
+    () => basicJsonHandshake('hello, world'),
+    () => basicJsonHandshake('hello, "world"'),
+    () => basicJsonHandshake('hello \u263A \u{1f602} unicode'),
+    () => basicJsonHandshake(<dynamic>[]),
+    () => basicJsonHandshake(aList),
+    () => basicJsonHandshake(<String, dynamic>{}),
+    () => basicJsonHandshake(aMap),
+    () => basicStandardHandshake(null),
+    () => basicStandardHandshake(true),
+    () => basicStandardHandshake(false),
+    () => basicStandardHandshake(0),
+    () => basicStandardHandshake(-7),
+    () => basicStandardHandshake(7),
+    () => basicStandardHandshake(1 << 32),
+    () => basicStandardHandshake(1 << 64),
+    () => basicStandardHandshake(1 << 128),
+    () => basicStandardHandshake(0.0),
+    () => basicStandardHandshake(-7.0),
+    () => basicStandardHandshake(7.0),
+    () => basicStandardHandshake(''),
+    () => basicStandardHandshake('hello, world'),
+    () => basicStandardHandshake('hello \u263A \u{1f602} unicode'),
+    () => basicStandardHandshake(someUint8s),
+    () => basicStandardHandshake(someInt32s),
+    () => basicStandardHandshake(someInt64s),
+    () => basicStandardHandshake(someFloat64s),
+    () => basicStandardHandshake(<dynamic>[]),
+    () => basicStandardHandshake(aList),
+    () => basicStandardHandshake(<String, dynamic>{}),
+    () => basicStandardHandshake(<dynamic, dynamic>{7: true, false: -7}),
+    () => basicStandardHandshake(aMap),
+    () => basicBinaryMessageToUnknownChannel(),
+    () => basicStringMessageToUnknownChannel(),
+    () => basicJsonMessageToUnknownChannel(),
+    () => basicStandardMessageToUnknownChannel(),
+  ];
+  Future<TestStepResult> _result;
+  int _step = 0;
+
+  @override
+  void initState() {
+    super.initState();
+  }
+
+  void _executeNextStep() {
+    setState(() {
+      if (_step < steps.length)
+        _result = steps[_step++]();
+      else
+        _result = new Future<TestStepResult>.value(TestStepResult.complete);
+    });
+  }
+
+  Widget _buildTestResultWidget(
+    BuildContext context,
+    AsyncSnapshot<TestStepResult> snapshot,
+  ) {
+    return new TestStepResult.fromSnapshot(snapshot).asWidget(context);
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return new MaterialApp(
+      title: 'Channels Test',
+      home: new Scaffold(
+        appBar: new AppBar(
+          title: const Text('Channels Test'),
+        ),
+        body: new Padding(
+          padding: const EdgeInsets.all(20.0),
+          child: new FutureBuilder<TestStepResult>(
+            future: _result,
+            builder: _buildTestResultWidget,
+          ),
+        ),
+        floatingActionButton: new FloatingActionButton(
+          key: const ValueKey<String>('step'),
+          onPressed: _executeNextStep,
+          child: new Icon(Icons.navigate_next),
+        ),
+      ),
+    );
+  }
+}
diff --git a/dev/integration_tests/channels/lib/src/basic_messaging.dart b/dev/integration_tests/channels/lib/src/basic_messaging.dart
new file mode 100644
index 0000000..f2105a8
--- /dev/null
+++ b/dev/integration_tests/channels/lib/src/basic_messaging.dart
@@ -0,0 +1,144 @@
+// 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 'test_step.dart';
+
+Future<TestStepResult> basicBinaryHandshake(ByteData message) async {
+  const BasicMessageChannel<ByteData> channel =
+      const BasicMessageChannel<ByteData>(
+    'binary-msg',
+    const BinaryCodec(),
+  );
+  return _basicMessageHandshake<ByteData>(
+      'Binary >${toString(message)}<', channel, message);
+}
+
+Future<TestStepResult> basicStringHandshake(String message) async {
+  const BasicMessageChannel<String> channel = const BasicMessageChannel<String>(
+    'string-msg',
+    const StringCodec(),
+  );
+  return _basicMessageHandshake<String>('String >$message<', channel, message);
+}
+
+Future<TestStepResult> basicJsonHandshake(dynamic message) async {
+  const BasicMessageChannel<dynamic> channel =
+      const BasicMessageChannel<dynamic>(
+    'json-msg',
+    const JSONMessageCodec(),
+  );
+  return _basicMessageHandshake<dynamic>('JSON >$message<', channel, message);
+}
+
+Future<TestStepResult> basicStandardHandshake(dynamic message) async {
+  const BasicMessageChannel<dynamic> channel =
+      const BasicMessageChannel<dynamic>(
+    'std-msg',
+    const StandardMessageCodec(),
+  );
+  return _basicMessageHandshake<dynamic>(
+      'Standard >${toString(message)}<', channel, message);
+}
+
+Future<TestStepResult> basicBinaryMessageToUnknownChannel() async {
+  const BasicMessageChannel<ByteData> channel =
+      const BasicMessageChannel<ByteData>(
+    'binary-unknown',
+    const BinaryCodec(),
+  );
+  return _basicMessageToUnknownChannel<ByteData>('Binary', channel);
+}
+
+Future<TestStepResult> basicStringMessageToUnknownChannel() async {
+  const BasicMessageChannel<String> channel = const BasicMessageChannel<String>(
+    'string-unknown',
+    const StringCodec(),
+  );
+  return _basicMessageToUnknownChannel<String>('String', channel);
+}
+
+Future<TestStepResult> basicJsonMessageToUnknownChannel() async {
+  const BasicMessageChannel<dynamic> channel =
+      const BasicMessageChannel<dynamic>(
+    'json-unknown',
+    const JSONMessageCodec(),
+  );
+  return _basicMessageToUnknownChannel<dynamic>('JSON', channel);
+}
+
+Future<TestStepResult> basicStandardMessageToUnknownChannel() async {
+  const BasicMessageChannel<dynamic> channel =
+      const BasicMessageChannel<dynamic>(
+    'std-unknown',
+    const StandardMessageCodec(),
+  );
+  return _basicMessageToUnknownChannel<dynamic>('Standard', channel);
+}
+
+/// Sends the specified message to the platform, doing a
+/// receive message/send reply/receive reply echo handshake initiated by the
+/// platform, then expecting a reply echo to the original message.
+///
+/// Fails, if an error occurs, or if any message seen is not deeply equal to
+/// the original message.
+Future<TestStepResult> _basicMessageHandshake<T>(
+  String description,
+  BasicMessageChannel<T> channel,
+  T message,
+) async {
+  final List<dynamic> received = <dynamic>[];
+  channel.setMessageHandler((T message) async {
+    received.add(message);
+    return message;
+  });
+  dynamic messageEcho = nothing;
+  dynamic error = nothing;
+  try {
+    messageEcho = await channel.send(message);
+  } catch (e) {
+    error = e;
+  }
+  return resultOfHandshake(
+    'Basic message handshake',
+    description,
+    message,
+    received,
+    messageEcho,
+    error,
+  );
+}
+
+/// Sends a message on a channel that no one listens on.
+Future<TestStepResult> _basicMessageToUnknownChannel<T>(
+  String description,
+  BasicMessageChannel<T> channel,
+) async {
+  dynamic messageEcho = nothing;
+  dynamic error = nothing;
+  try {
+    messageEcho = await channel.send(null);
+  } catch (e) {
+    error = e;
+  }
+  return resultOfHandshake(
+    'Message on unknown channel',
+    description,
+    null,
+    <dynamic>[null, null],
+    messageEcho,
+    error,
+  );
+}
+
+String toString(dynamic message) {
+  if (message is ByteData)
+    return message.buffer
+        .asUint8List(message.offsetInBytes, message.lengthInBytes)
+        .toString();
+  else
+    return '$message';
+}
diff --git a/dev/integration_tests/channels/lib/src/method_calls.dart b/dev/integration_tests/channels/lib/src/method_calls.dart
new file mode 100644
index 0000000..7db46cd
--- /dev/null
+++ b/dev/integration_tests/channels/lib/src/method_calls.dart
@@ -0,0 +1,133 @@
+// 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 'package:flutter/services.dart';
+import 'test_step.dart';
+
+Future<TestStepResult> methodCallJsonSuccessHandshake(dynamic payload) async {
+  const MethodChannel channel =
+      const MethodChannel('json-method', const JSONMethodCodec());
+  return _methodCallSuccessHandshake(
+      'JSON success($payload)', channel, payload);
+}
+
+Future<TestStepResult> methodCallJsonErrorHandshake(dynamic payload) async {
+  const MethodChannel channel =
+      const MethodChannel('json-method', const JSONMethodCodec());
+  return _methodCallErrorHandshake('JSON error($payload)', channel, payload);
+}
+
+Future<TestStepResult> methodCallJsonNotImplementedHandshake() async {
+  const MethodChannel channel =
+      const MethodChannel('json-method', const JSONMethodCodec());
+  return _methodCallNotImplementedHandshake('JSON notImplemented()', channel);
+}
+
+Future<TestStepResult> methodCallStandardSuccessHandshake(
+    dynamic payload) async {
+  const MethodChannel channel =
+      const MethodChannel('std-method', const StandardMethodCodec());
+  return _methodCallSuccessHandshake(
+      'Standard success($payload)', channel, payload);
+}
+
+Future<TestStepResult> methodCallStandardErrorHandshake(dynamic payload) async {
+  const MethodChannel channel =
+      const MethodChannel('std-method', const StandardMethodCodec());
+  return _methodCallErrorHandshake(
+      'Standard error($payload)', channel, payload);
+}
+
+Future<TestStepResult> methodCallStandardNotImplementedHandshake() async {
+  const MethodChannel channel =
+      const MethodChannel('std-method', const StandardMethodCodec());
+  return _methodCallNotImplementedHandshake(
+      'Standard notImplemented()', channel);
+}
+
+Future<TestStepResult> _methodCallSuccessHandshake(
+  String description,
+  MethodChannel channel,
+  dynamic arguments,
+) async {
+  final List<dynamic> received = <dynamic>[];
+  channel.setMethodCallHandler((MethodCall call) async {
+    received.add(call.arguments);
+    return call.arguments;
+  });
+  dynamic result = nothing;
+  dynamic error = nothing;
+  try {
+    result = await channel.invokeMethod('success', arguments);
+  } catch (e) {
+    error = e;
+  }
+  return resultOfHandshake(
+    'Method call success handshake',
+    description,
+    arguments,
+    received,
+    result,
+    error,
+  );
+}
+
+Future<TestStepResult> _methodCallErrorHandshake(
+  String description,
+  MethodChannel channel,
+  dynamic arguments,
+) async {
+  final List<dynamic> received = <dynamic>[];
+  channel.setMethodCallHandler((MethodCall call) async {
+    received.add(call.arguments);
+    throw new PlatformException(
+        code: 'error', message: null, details: arguments);
+  });
+  dynamic errorDetails = nothing;
+  dynamic error = nothing;
+  try {
+    error = await channel.invokeMethod('error', arguments);
+  } on PlatformException catch (e) {
+    errorDetails = e.details;
+  } catch (e) {
+    error = e;
+  }
+  return resultOfHandshake(
+    'Method call error handshake',
+    description,
+    arguments,
+    received,
+    errorDetails,
+    error,
+  );
+}
+
+Future<TestStepResult> _methodCallNotImplementedHandshake(
+  String description,
+  MethodChannel channel,
+) async {
+  final List<dynamic> received = <dynamic>[];
+  channel.setMethodCallHandler((MethodCall call) async {
+    received.add(call.arguments);
+    throw new MissingPluginException();
+  });
+  dynamic result = nothing;
+  dynamic error = nothing;
+  try {
+    error = await channel.invokeMethod('notImplemented');
+  } on MissingPluginException {
+    result = null;
+  } catch (e) {
+    error = e;
+  }
+  return resultOfHandshake(
+    'Method call not implemented handshake',
+    description,
+    null,
+    received,
+    result,
+    error,
+  );
+}
diff --git a/dev/integration_tests/channels/lib/src/test_step.dart b/dev/integration_tests/channels/lib/src/test_step.dart
new file mode 100644
index 0000000..1f65880
--- /dev/null
+++ b/dev/integration_tests/channels/lib/src/test_step.dart
@@ -0,0 +1,168 @@
+// 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/material.dart';
+
+enum TestStatus { ok, pending, failed, complete }
+
+typedef Future<TestStepResult> TestStep();
+
+const String nothing = '-';
+
+/// Result of a test step checking a nested communication handshake
+/// between the Flutter app and the platform:
+///
+/// - The Flutter app sends a message to the platform.
+/// - The platform, on receipt, echos the message back to Flutter in a separate message.
+/// - The Flutter app records the incoming message echo and replies.
+/// - The platform, on receipt of reply, echos the reply back to Flutter in a separate message.
+/// - The Flutter app records the incoming reply echo.
+/// - The platform finally replies to the original message with another echo.
+class TestStepResult {
+  static const TextStyle bold = const TextStyle(fontWeight: FontWeight.bold);
+  static const TestStepResult complete = const TestStepResult(
+    'Test complete',
+    nothing,
+    TestStatus.complete,
+  );
+
+  const TestStepResult(
+    this.name,
+    this.description,
+    this.status, {
+    this.messageSent = nothing,
+    this.messageEcho = nothing,
+    this.messageReceived = nothing,
+    this.replyEcho = nothing,
+    this.error = nothing,
+  });
+
+  factory TestStepResult.fromSnapshot(AsyncSnapshot<TestStepResult> snapshot) {
+    switch (snapshot.connectionState) {
+      case ConnectionState.none:
+        return const TestStepResult('Not started', nothing, TestStatus.ok);
+      case ConnectionState.waiting:
+        return const TestStepResult('Executing', nothing, TestStatus.pending);
+      case ConnectionState.done:
+        if (snapshot.hasData) {
+          return snapshot.data;
+        } else {
+          final TestStepResult result = snapshot.error;
+          return result;
+        }
+        break;
+      default:
+        throw 'Unsupported state ${snapshot.connectionState}';
+    }
+  }
+
+  final String name;
+  final String description;
+  final TestStatus status;
+  final dynamic messageSent;
+  final dynamic messageEcho;
+  final dynamic messageReceived;
+  final dynamic replyEcho;
+  final dynamic error;
+
+  Widget asWidget(BuildContext context) {
+    return new Column(
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: <Widget>[
+        new Text('Step: $name', style: bold),
+        new Text(description),
+        const Text(' '),
+        new Text('Msg sent: ${_toString(messageSent)}'),
+        new Text('Msg rvcd: ${_toString(messageReceived)}'),
+        new Text('Reply echo: ${_toString(replyEcho)}'),
+        new Text('Msg echo: ${_toString(messageEcho)}'),
+        new Text('Error: ${_toString(error)}'),
+        const Text(' '),
+        new Text(
+          status.toString().substring('TestStatus.'.length),
+          key: new ValueKey<String>(
+              status == TestStatus.pending ? 'nostatus' : 'status'),
+          style: bold,
+        ),
+      ],
+    );
+  }
+}
+
+Future<TestStepResult> resultOfHandshake(
+  String name,
+  String description,
+  dynamic message,
+  List<dynamic> received,
+  dynamic messageEcho,
+  dynamic error,
+) async {
+  assert(message != nothing);
+  while (received.length < 2) received.add(nothing);
+  TestStatus status;
+  if (!_deepEquals(messageEcho, message) ||
+      received.length != 2 ||
+      !_deepEquals(received[0], message) ||
+      !_deepEquals(received[1], message)) {
+    status = TestStatus.failed;
+  } else if (error != nothing) {
+    status = TestStatus.failed;
+  } else {
+    status = TestStatus.ok;
+  }
+  return new TestStepResult(
+    name,
+    description,
+    status,
+    messageSent: message,
+    messageEcho: messageEcho,
+    messageReceived: received[0],
+    replyEcho: received[1],
+    error: error,
+  );
+}
+
+String _toString(dynamic message) {
+  if (message is ByteData)
+    return message.buffer
+        .asUint8List(message.offsetInBytes, message.lengthInBytes)
+        .toString();
+  else
+    return '$message';
+}
+
+bool _deepEquals(dynamic a, dynamic b) {
+  if (a == b) return true;
+  if (a is double && a.isNaN) return b is double && b.isNaN;
+  if (a is ByteData) return b is ByteData && _deepEqualsByteData(a, b);
+  if (a is List) return b is List && _deepEqualsList(a, b);
+  if (a is Map) return b is Map && _deepEqualsMap(a, b);
+  return false;
+}
+
+bool _deepEqualsByteData(ByteData a, ByteData b) {
+  return _deepEqualsList(
+    a.buffer.asUint8List(a.offsetInBytes, a.lengthInBytes),
+    b.buffer.asUint8List(b.offsetInBytes, b.lengthInBytes),
+  );
+}
+
+bool _deepEqualsList(List<dynamic> a, List<dynamic> b) {
+  if (a.length != b.length) return false;
+  for (int i = 0; i < a.length; i++) {
+    if (!_deepEquals(a[i], b[i])) return false;
+  }
+  return true;
+}
+
+bool _deepEqualsMap(Map<dynamic, dynamic> a, Map<dynamic, dynamic> b) {
+  if (a.length != b.length) return false;
+  for (dynamic key in a.keys) {
+    if (!b.containsKey(key) || !_deepEquals(a[key], b[key])) return false;
+  }
+  return true;
+}
diff --git a/dev/integration_tests/channels/pubspec.yaml b/dev/integration_tests/channels/pubspec.yaml
new file mode 100644
index 0000000..3c26fdd
--- /dev/null
+++ b/dev/integration_tests/channels/pubspec.yaml
@@ -0,0 +1,10 @@
+name: channels
+description: Integration test for platform channels.
+
+dependencies:
+  flutter:
+    sdk: flutter
+  flutter_driver:
+    sdk: flutter
+flutter:
+  uses-material-design: true
diff --git a/dev/integration_tests/channels/test_driver/main_test.dart b/dev/integration_tests/channels/test_driver/main_test.dart
new file mode 100644
index 0000000..1048c69
--- /dev/null
+++ b/dev/integration_tests/channels/test_driver/main_test.dart
@@ -0,0 +1,34 @@
+// 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_driver/flutter_driver.dart';
+import 'package:test/test.dart';
+
+void main() {
+  group('channel suite', () {
+    FlutterDriver driver;
+
+    setUpAll(() async {
+      driver = await FlutterDriver.connect();
+    });
+
+    test('step through', () async {
+      final SerializableFinder stepButton = find.byValueKey('step');
+      final SerializableFinder statusField = find.byValueKey('status');
+      int step = 0;
+      while (await driver.getText(statusField) == 'ok') {
+        await driver.tap(stepButton);
+        step++;
+      }
+      final String status = await driver.getText(statusField);
+      if (status != 'complete') {
+        fail('Failed at step $step with status $status');
+      }
+    });
+
+    tearDownAll(() async {
+      driver?.close();
+    });
+  });
+}
diff --git a/packages/flutter/lib/src/services/message_codec.dart b/packages/flutter/lib/src/services/message_codec.dart
index 5b1124a..7cf3620 100644
--- a/packages/flutter/lib/src/services/message_codec.dart
+++ b/packages/flutter/lib/src/services/message_codec.dart
@@ -7,9 +7,12 @@
 
 import 'package:flutter/foundation.dart';
 
+import 'platform_channel.dart';
+
 /// A message encoding/decoding mechanism.
 ///
-/// Both operations throw [FormatException], if conversion fails.
+/// Both operations throw an exception, if conversion fails. Such situations
+/// should be treated as programming errors.
 ///
 /// See also:
 ///
@@ -92,15 +95,7 @@
 
 /// 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.
+/// All operations throw an exception, if conversion fails.
 ///
 /// See also:
 ///
@@ -109,7 +104,7 @@
 /// * [PlatformEventChannel], which use [MethodCodec]s for communication
 ///   between Flutter and platform plugins.
 abstract class MethodCodec {
-  /// Encodes the specified [methodCall] in binary.
+  /// Encodes the specified [methodCall] into binary.
   ByteData encodeMethodCall(MethodCall methodCall);
 
   /// Decodes the specified [methodCall] from binary.
@@ -139,10 +134,10 @@
 ///
 /// * [MethodCodec], which throws a [PlatformException], if a received result
 ///   envelope represents an error.
-/// * [PlatformMethodChannel.invokeMethod], which completes the returned future
+/// * [MethodChannel.invokeMethod], which completes the returned future
 ///   with a [PlatformException], if invoking the platform plugin method
 ///   results in an error envelope.
-/// * [PlatformEventChannel.receiveBroadcastStream], which emits
+/// * [EventChannel.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 {
@@ -175,9 +170,11 @@
 ///
 /// See also:
 ///
-/// * [PlatformMethodChannel.invokeMethod], which completes the returned future
+/// * [MethodChannel.invokeMethod], which completes the returned future
 ///   with a [MissingPluginException], if no plugin handler for the method call
 ///   was found.
+/// * [OptionalMethodChannel.invokeMethod], which completes the returned future
+///   with `null`, if no plugin handler for the method call was found.
 class MissingPluginException implements Exception {
   /// Creates a [MissingPluginException] with an optional human-readable
   /// error message.
diff --git a/packages/flutter/lib/src/services/platform_channel.dart b/packages/flutter/lib/src/services/platform_channel.dart
index c064c28..a766ff9 100644
--- a/packages/flutter/lib/src/services/platform_channel.dart
+++ b/packages/flutter/lib/src/services/platform_channel.dart
@@ -17,14 +17,15 @@
 /// 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
+/// by creating a `BasicMessageChannel` 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 use of unsupported values should be considered programming errors, and
+/// will result in exceptions being thrown. The `null` message is supported
+/// for all codecs.
 ///
-/// 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.
+/// The logical identity of the channel is given by its name. Identically named
+/// channels will interfere with each other's communication.
 ///
 /// See: <https://flutter.io/platform-channels/>
 class BasicMessageChannel<T> {
@@ -41,23 +42,21 @@
 
   /// 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.
+  /// Returns a [Future] which completes to the received response, which may
+  /// be `null`.
   Future<T> send(T message) async {
-    return codec.decodeMessage(
-      await BinaryMessages.send(name, codec.encodeMessage(message))
-    );
+    return codec.decodeMessage(await BinaryMessages.send(name, codec.encodeMessage(message)));
   }
 
   /// Sets a callback for receiving messages from the platform plugins on this
-  /// channel.
+  /// channel. Messages may be `null`.
   ///
   /// The given callback will replace the currently registered callback for this
   /// channel, if any. To remove the handler, pass `null` as the `handler`
   /// argument.
   ///
-  /// The handler's return value, if non-null, is sent back to the platform
-  /// plugins as a response.
+  /// The handler's return value is sent back to the platform plugins as a
+  /// message reply. It may be `null`.
   void setMessageHandler(Future<T> handler(T message)) {
     if (handler == null) {
       BinaryMessages.setMessageHandler(name, null);
@@ -69,12 +68,13 @@
   }
 
   /// Sets a mock callback for intercepting messages sent on this channel.
+  /// Messages may be `null`.
   ///
   /// 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.
+  /// The handler's return value is used as a message reply. It may be `null`.
   ///
   /// This is intended for testing. Messages intercepted in this manner are not
   /// sent to platform plugins.
@@ -95,12 +95,15 @@
 /// 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`,
+/// by creating a `MethodChannel` counterpart of this channel on the
+/// platform side. The Dart type of arguments and results is `dynamic`,
 /// but only values supported by the specified [MethodCodec] can be used.
+/// The use of unsupported values should be considered programming errors, and
+/// will result in exceptions being thrown. The `null` value is supported
+/// for all codecs.
 ///
-/// The identity of the channel is given by its name, so other uses of that name
-/// with may interfere with this channel's communication.
+/// The logical identity of the channel is given by its name. Identically named
+/// channels will interfere with each other's communication.
 ///
 /// See: <https://flutter.io/platform-channels/>
 class MethodChannel {
@@ -124,8 +127,8 @@
   ///
   /// * a result (possibly `null`), on successful invocation;
   /// * a [PlatformException], if the invocation failed in the platform plugin;
-  /// * a [FormatException], if encoding or decoding failed.
-  /// * a [MissingPluginException], if the method has not been implemented.
+  /// * a [MissingPluginException], if the method has not been implemented by a
+  ///   platform plugin.
   Future<dynamic> invokeMethod(String method, [dynamic arguments]) async {
     assert(method != null);
     final dynamic result = await BinaryMessages.send(
@@ -150,26 +153,12 @@
   /// populate an error envelope which is sent back instead. If the future
   /// completes with a [MissingPluginException], an empty reply is sent
   /// similarly to what happens if no method call handler has been set.
+  /// Any other exception results in an error envelope being sent.
   void setMethodCallHandler(Future<dynamic> handler(MethodCall call)) {
-    if (handler == null) {
-      BinaryMessages.setMessageHandler(name, null);
-    } else {
-      BinaryMessages.setMessageHandler(
-        name,
-        (ByteData message) async {
-          final MethodCall call = codec.decodeMethodCall(message);
-          try {
-            final dynamic result = await handler(call);
-            return codec.encodeSuccessEnvelope(result);
-          } on PlatformException catch (e) {
-            return codec.encodeErrorEnvelope(
-              code: e.code, message: e.message, details: e.details);
-          } on MissingPluginException {
-            return null;
-          }
-        },
-      );
-    }
+    BinaryMessages.setMessageHandler(
+      name,
+      handler == null ? null : (ByteData message) => _handleAsMethodCall(message, handler),
+    );
   }
 
   /// Sets a mock callback for intercepting method invocations on this channel.
@@ -186,24 +175,26 @@
   /// This is intended for testing. Method calls intercepted in this manner are
   /// not sent to platform plugins.
   void setMockMethodCallHandler(Future<dynamic> handler(MethodCall call)) {
-    if (handler == null) {
-      BinaryMessages.setMockMessageHandler(name, null);
-    } else {
-      BinaryMessages.setMockMessageHandler(
-        name,
-        (ByteData message) async {
-          final MethodCall call = codec.decodeMethodCall(message);
-          try {
-            final dynamic result = await handler(call);
-            return codec.encodeSuccessEnvelope(result);
-          } on PlatformException catch (e) {
-            return codec.encodeErrorEnvelope(
-              code: e.code, message: e.message, details: e.details);
-          } on MissingPluginException {
-            return null;
-          }
-        },
+    BinaryMessages.setMockMessageHandler(
+      name,
+      handler == null ? null : (ByteData message) => _handleAsMethodCall(message, handler),
+    );
+  }
+
+  Future<ByteData> _handleAsMethodCall(ByteData message, Future<dynamic> handler(MethodCall call)) async {
+    final MethodCall call = codec.decodeMethodCall(message);
+    try {
+      return codec.encodeSuccessEnvelope(await handler(call));
+    } on PlatformException catch (e) {
+      return codec.encodeErrorEnvelope(
+        code: e.code,
+        message: e.message,
+        details: e.details,
       );
+    } on MissingPluginException {
+      return null;
+    } catch (e) {
+      return codec.encodeErrorEnvelope(code: 'error', message: e.toString(), details: null);
     }
   }
 }
@@ -230,18 +221,19 @@
 /// A named channel for communicating with platform plugins using event streams.
 ///
 /// Stream setup requests are encoded into binary before being sent,
-/// and binary events 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 FlutterEventChannel counterpart of this channel on
-/// the platform side. The Dart type of events sent and received is `dynamic`,
-/// but only values supported by the specified [MethodCodec] can be used.
+/// and binary events and errors 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 an `EventChannel` counterpart of
+/// this channel on the platform side. The Dart type of events 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.
+/// The logical identity of the channel is given by its name. Identically named
+/// channels will interfere with each other's communication.
 ///
 /// See: <https://flutter.io/platform-channels/>
 class EventChannel {
-  /// Creates a [EventChannel] with the specified [name].
+  /// Creates an [EventChannel] with the specified [name].
   ///
   /// The [codec] used will be [StandardMethodCodec], unless otherwise
   /// specified.
@@ -263,61 +255,44 @@
   /// 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],
-  /// [MissingPluginException], 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 [MethodChannel.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.
+  /// Errors occurring during stream activation or deactivation are reported
+  /// through the [FlutterError] facility. Stream activation happens only when
+  /// stream listener count changes from 0 to 1. Stream deactivation happens
+  /// only when stream listener count changes from 1 to 0.
   Stream<dynamic> receiveBroadcastStream([dynamic arguments]) {
     final MethodChannel methodChannel = new MethodChannel(name, codec);
     StreamController<dynamic> controller;
-    controller = new StreamController<dynamic>.broadcast(
-      onListen: () async {
-        BinaryMessages.setMessageHandler(
-          name, (ByteData reply) async {
-            if (reply == null) {
-              controller.close();
-            } else {
-              try {
-                controller.add(codec.decodeEnvelope(reply));
-              } catch (e) {
-                controller.addError(e);
-              }
-            }
-          }
-        );
-        try {
-          await methodChannel.invokeMethod('listen', arguments);
-        } catch (e) {
-          BinaryMessages.setMessageHandler(name, null);
-          controller.addError(e);
-        }
-      }, onCancel: () async {
-        BinaryMessages.setMessageHandler(name, null);
-        try {
-          await methodChannel.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',
-          ));
-        }
+    controller = new StreamController<dynamic>.broadcast(onListen: () async {
+      BinaryMessages.setMessageHandler(name, (ByteData reply) async {
+        if (reply == null)
+          controller.close();
+        else
+          controller.add(codec.decodeEnvelope(reply));
+      });
+      try {
+        await methodChannel.invokeMethod('listen', arguments);
+      } catch (exception, stack) {
+        FlutterError.reportError(new FlutterErrorDetails(
+          exception: exception,
+          stack: stack,
+          library: 'services library',
+          context: 'while activating platform stream on channel $name',
+        ));
       }
-    );
+    }, onCancel: () async {
+      BinaryMessages.setMessageHandler(name, null);
+      try {
+        await methodChannel.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;
   }
 }
diff --git a/packages/flutter/lib/src/services/platform_messages.dart b/packages/flutter/lib/src/services/platform_messages.dart
index 972c314..8cb40ba 100644
--- a/packages/flutter/lib/src/services/platform_messages.dart
+++ b/packages/flutter/lib/src/services/platform_messages.dart
@@ -8,17 +8,20 @@
 
 import 'package:flutter/foundation.dart';
 
+import 'platform_channel.dart';
+
 typedef Future<ByteData> _MessageHandler(ByteData message);
 
 /// Sends binary messages to and receives binary messages from platform plugins.
 ///
 /// See also:
 ///
-/// * [BasicMessageChannel], which provides messaging services similar to
-/// [BinaryMessages], but with pluggable message codecs in support of sending
-/// strings or semi-structured messages.
-/// * [MethodChannel], which provides higher-level platform
-/// communication such as method invocations and event streams.
+/// * [BasicMessageChannel], which provides basic messaging services similar to
+///   `BinaryMessages`, but with pluggable message codecs in support of sending
+///   strings or semi-structured messages.
+/// * [MethodChannel], which provides platform communication using asynchronous
+///   method calls.
+/// * [EventChannel], which provides platform communication using event streams.
 ///
 /// See: <https://flutter.io/platform-channels/>
 class BinaryMessages {
@@ -54,7 +57,7 @@
   /// Typically called by [ServicesBinding] to handle platform messages received
   /// from [ui.window.onPlatformMessage].
   ///
-  /// To register a handler for a given message channel, see [PlatformChannel].
+  /// To register a handler for a given message channel, see [setMessageHandler].
   static Future<Null> handlePlatformMessage(
         String channel, ByteData data, ui.PlatformMessageResponseCallback callback) async {
     ByteData response;
diff --git a/packages/flutter/test/services/platform_channel_test.dart b/packages/flutter/test/services/platform_channel_test.dart
index 6478146..d141c4f 100644
--- a/packages/flutter/test/services/platform_channel_test.dart
+++ b/packages/flutter/test/services/platform_channel_test.dart
@@ -9,7 +9,7 @@
 import 'package:test/test.dart';
 
 void main() {
-  group('PlatformMessageChannel', () {
+  group('BasicMessageChannel', () {
     const MessageCodec<String> string = const StringCodec();
     const BasicMessageChannel<String> channel = const BasicMessageChannel<String>('ch', string);
     test('can send string message and get reply', () async {
@@ -34,7 +34,7 @@
     });
   });
 
-  group('PlatformMethodChannel', () {
+  group('MethodChannel', () {
     const MessageCodec<dynamic> jsonMessage = const JSONMessageCodec();
     const MethodCodec jsonMethod = const JSONMethodCodec();
     const MethodChannel channel = const MethodChannel('ch7', jsonMethod);
@@ -89,8 +89,75 @@
         fail('MissingPluginException expected');
       }
     });
+    test('can handle method call with no registered plugin', () async {
+      channel.setMethodCallHandler(null);
+      final ByteData call = jsonMethod.encodeMethodCall(new MethodCall('sayHello', 'hello'));
+      ByteData envelope;
+      await BinaryMessages.handlePlatformMessage('ch7', call, (ByteData result) {
+        envelope = result;
+      });
+      expect(envelope, isNull);
+    });
+    test('can handle method call of unimplemented method', () async {
+      channel.setMethodCallHandler((MethodCall call) async {
+        throw new MissingPluginException();
+      });
+      final ByteData call = jsonMethod.encodeMethodCall(new MethodCall('sayHello', 'hello'));
+      ByteData envelope;
+      await BinaryMessages.handlePlatformMessage('ch7', call, (ByteData result) {
+        envelope = result;
+      });
+      expect(envelope, isNull);
+    });
+    test('can handle method call with successful result', () async {
+      channel.setMethodCallHandler((MethodCall call) async => '${call.arguments}, world');
+      final ByteData call = jsonMethod.encodeMethodCall(new MethodCall('sayHello', 'hello'));
+      ByteData envelope;
+      await BinaryMessages.handlePlatformMessage('ch7', call, (ByteData result) {
+        envelope = result;
+      });
+      expect(jsonMethod.decodeEnvelope(envelope), equals('hello, world'));
+    });
+    test('can handle method call with expressive error result', () async {
+      channel.setMethodCallHandler((MethodCall call) async {
+        throw new PlatformException(code: 'bad', message: 'sayHello failed', details: null);
+      });
+      final ByteData call = jsonMethod.encodeMethodCall(new MethodCall('sayHello', 'hello'));
+      ByteData envelope;
+      await BinaryMessages.handlePlatformMessage('ch7', call, (ByteData result) {
+        envelope = result;
+      });
+      try {
+        jsonMethod.decodeEnvelope(envelope);
+        fail('Exception expected');
+      } on PlatformException catch(e) {
+        expect(e.code, equals('bad'));
+        expect(e.message, equals('sayHello failed'));
+      } catch (e) {
+        fail('PlatformException expected');
+      }
+    });
+    test('can handle method call with other error result', () async {
+      channel.setMethodCallHandler((MethodCall call) async {
+        throw new ArgumentError('bad');
+      });
+      final ByteData call = jsonMethod.encodeMethodCall(new MethodCall('sayHello', 'hello'));
+      ByteData envelope;
+      await BinaryMessages.handlePlatformMessage('ch7', call, (ByteData result) {
+        envelope = result;
+      });
+      try {
+        jsonMethod.decodeEnvelope(envelope);
+        fail('Exception expected');
+      } on PlatformException catch(e) {
+        expect(e.code, equals('error'));
+        expect(e.message, equals('Invalid argument(s): bad'));
+      } catch (e) {
+        fail('PlatformException expected');
+      }
+    });
   });
-  group('PlatformEventChannel', () {
+  group('EventChannel', () {
     const MessageCodec<dynamic> jsonMessage = const JSONMessageCodec();
     const MethodCodec jsonMethod = const JSONMethodCodec();
     const EventChannel channel = const EventChannel('ch', jsonMethod);