Merge pull request #2695 from abarth/inherit_render_object_widget

Let RenderObjectWidgets use inherited properties
diff --git a/examples/hello_android/README.md b/examples/hello_android/README.md
new file mode 100644
index 0000000..c7d64c4
--- /dev/null
+++ b/examples/hello_android/README.md
@@ -0,0 +1,16 @@
+# Example of building a Flutter app for Android using Gradle
+
+This project demonstrates how to embed Flutter within an Android application
+and build the Android and Flutter components with Gradle.
+
+To build this project:
+
+* Create a `local.properties` file with these entries:
+  * `sdk.dir=[path to the Android SDK]`
+  * `flutter.sdk=[path to the Flutter SDK]`
+  * `flutter.jar=[path to the flutter.jar file in your build of the Flutter engine]`
+
+Then run:
+
+* `gradle wrapper`
+* `./gradlew build`
diff --git a/examples/hello_android/app/build.gradle b/examples/hello_android/app/build.gradle
new file mode 100644
index 0000000..8ece56f
--- /dev/null
+++ b/examples/hello_android/app/build.gradle
@@ -0,0 +1,15 @@
+apply plugin: 'com.android.application'
+apply plugin: 'flutter'
+
+android {
+    compileSdkVersion 22
+    buildToolsVersion '22.0.1'
+
+    lintOptions {
+        disable 'InvalidPackage'
+    }
+}
+
+flutter {
+    source 'src/flutter'
+}
diff --git a/examples/hello_android/app/src/flutter/lib/main.dart b/examples/hello_android/app/src/flutter/lib/main.dart
new file mode 100644
index 0000000..151b1da
--- /dev/null
+++ b/examples/hello_android/app/src/flutter/lib/main.dart
@@ -0,0 +1,7 @@
+// Copyright 2016 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/widgets.dart';
+
+void main() => runApp(new Center(child: new Text('Hello from Flutter!')));
diff --git a/examples/hello_android/app/src/flutter/pubspec.yaml b/examples/hello_android/app/src/flutter/pubspec.yaml
new file mode 100644
index 0000000..5669909
--- /dev/null
+++ b/examples/hello_android/app/src/flutter/pubspec.yaml
@@ -0,0 +1,4 @@
+name: gradle
+dependencies:
+  flutter:
+    path: ../../../../../packages/flutter
diff --git a/examples/hello_android/app/src/main/AndroidManifest.xml b/examples/hello_android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..a272e14
--- /dev/null
+++ b/examples/hello_android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.example.flutter"
+    android:versionCode="1"
+    android:versionName="1.0.0" >
+
+    <uses-sdk android:minSdkVersion="16" android:targetSdkVersion="21" />
+    <uses-permission android:name="android.permission.INTERNET"/>
+
+    <application android:name="org.domokit.sky.shell.SkyApplication" android:label="@string/app_name" >
+        <activity
+            android:name=".FlutterActivity"
+            android:label="@string/app_name"
+            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection"
+            android:launchMode="singleTask"
+            android:theme="@android:style/Theme.Black.NoTitleBar"
+            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/examples/hello_android/app/src/main/java/com/example/flutter/FlutterActivity.java b/examples/hello_android/app/src/main/java/com/example/flutter/FlutterActivity.java
new file mode 100644
index 0000000..7323d6f
--- /dev/null
+++ b/examples/hello_android/app/src/main/java/com/example/flutter/FlutterActivity.java
@@ -0,0 +1,39 @@
+// Copyright 2016 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.example.flutter;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+import org.chromium.base.PathUtils;
+import org.domokit.sky.shell.SkyApplication;
+import org.domokit.sky.shell.SkyMain;
+import org.domokit.sky.shell.PlatformViewAndroid;
+
+import java.io.File;
+
+public class FlutterActivity extends Activity {
+    private PlatformViewAndroid flutterView;
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        SkyMain.ensureInitialized(getApplicationContext(), null);
+        setContentView(R.layout.flutter_layout);
+
+        flutterView = (PlatformViewAndroid) findViewById(R.id.flutter_view);
+        File appBundle = new File(PathUtils.getDataDirectory(this), SkyApplication.APP_BUNDLE);
+        flutterView.runFromBundle(appBundle.getPath(), null);
+    }
+
+    @Override
+    protected void onDestroy() {
+        if (flutterView != null) {
+            flutterView.destroy();
+        }
+        super.onDestroy();
+    }
+}
diff --git a/examples/hello_android/app/src/main/res/layout/flutter_layout.xml b/examples/hello_android/app/src/main/res/layout/flutter_layout.xml
new file mode 100644
index 0000000..bc8c11d
--- /dev/null
+++ b/examples/hello_android/app/src/main/res/layout/flutter_layout.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent"
+    >
+  <TextView
+      android:id="@+id/text_view"
+      android:layout_width="fill_parent"
+      android:layout_height="wrap_content"
+      android:text="@string/title"
+      />
+  <org.domokit.sky.shell.PlatformViewAndroid
+      android:id="@+id/flutter_view"
+      android:layout_width="fill_parent"
+      android:layout_height="fill_parent"
+      />
+</LinearLayout>
diff --git a/examples/hello_android/app/src/main/res/values/strings.xml b/examples/hello_android/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..670cfae
--- /dev/null
+++ b/examples/hello_android/app/src/main/res/values/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="app_name">Flutter App</string>
+    <string name="title">Flutter Application</string>
+</resources>
diff --git a/examples/hello_android/build.gradle b/examples/hello_android/build.gradle
new file mode 100644
index 0000000..ba21767
--- /dev/null
+++ b/examples/hello_android/build.gradle
@@ -0,0 +1,17 @@
+buildscript {
+    repositories {
+        jcenter()
+    }
+
+    dependencies {
+        classpath 'com.android.tools.build:gradle:1.5.0'
+    }
+}
+
+task clean(type: Delete) {
+    delete rootProject.buildDir
+}
+
+task wrapper(type: Wrapper) {
+    gradleVersion = '2.8'
+}
diff --git a/examples/hello_android/buildSrc/build.gradle b/examples/hello_android/buildSrc/build.gradle
new file mode 100644
index 0000000..b62a213
--- /dev/null
+++ b/examples/hello_android/buildSrc/build.gradle
@@ -0,0 +1,7 @@
+repositories {
+  jcenter()
+}
+
+dependencies {
+  compile "com.android.tools.build:gradle:1.5.0"
+}
diff --git a/examples/hello_android/buildSrc/src/main/groovy/FlutterPlugin.groovy b/examples/hello_android/buildSrc/src/main/groovy/FlutterPlugin.groovy
new file mode 100644
index 0000000..bdd0c2e
--- /dev/null
+++ b/examples/hello_android/buildSrc/src/main/groovy/FlutterPlugin.groovy
@@ -0,0 +1,107 @@
+// Copyright 2016 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 org.domokit.sky.gradle
+
+import com.android.builder.model.AndroidProject
+import org.gradle.api.DefaultTask
+import org.gradle.api.GradleException
+import org.gradle.api.Project
+import org.gradle.api.Plugin
+import org.gradle.api.Task
+import org.gradle.api.file.FileCollection
+import org.gradle.api.tasks.Copy
+import org.gradle.api.tasks.InputDirectory
+import org.gradle.api.tasks.OutputDirectory
+import org.gradle.api.tasks.TaskAction
+
+class FlutterPlugin implements Plugin<Project> {
+    private File sdkDir
+
+    @Override
+    void apply(Project project) {
+        Properties properties = new Properties()
+        properties.load(project.rootProject.file("local.properties").newDataInputStream())
+
+        String enginePath = properties.getProperty("flutter.jar")
+        if (enginePath == null) {
+            throw new GradleException("flutter.jar must be defined in local.properties")
+        }
+        FileCollection flutterEngine = project.files(enginePath)
+        if (!flutterEngine.singleFile.isFile()) {
+            throw new GradleException("flutter.jar must point to a Flutter engine JAR")
+        }
+
+        String sdkPath = properties.getProperty("flutter.sdk")
+        if (sdkPath == null) {
+            throw new GradleException("flutter.sdk must be defined in local.properties")
+        }
+        sdkDir = project.file(sdkPath)
+        if (!sdkDir.isDirectory()) {
+            throw new GradleException("flutter.sdk must point to the Flutter SDK directory")
+        }
+
+        project.extensions.create("flutter", FlutterExtension)
+        project.dependencies.add("compile", flutterEngine)
+        project.afterEvaluate this.&addFlutterTask
+    }
+
+    private void addFlutterTask(Project project) {
+        if (project.flutter.source == null) {
+            throw new GradleException("Must provide Flutter source directory")
+        }
+
+        FlutterTask flutterTask = project.tasks.create("flutterBuild", FlutterTask) {
+            sdkDir this.sdkDir
+            sourceDir project.file(project.flutter.source)
+            intermediateDir project.file("${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/flutter")
+        }
+
+        project.android.applicationVariants.all { variant ->
+            Task copyFlxTask = project.tasks.create(name: "copyFlx${variant.name.capitalize()}", type: Copy) {
+                dependsOn flutterTask
+                dependsOn variant.mergeAssets
+                from flutterTask.flxPath
+                into variant.mergeAssets.outputDir
+            }
+            variant.outputs[0].processResources.dependsOn(copyFlxTask)
+        }
+    }
+}
+
+class FlutterExtension {
+    String source
+}
+
+class FlutterTask extends DefaultTask {
+    File sdkDir
+
+    @InputDirectory
+    File sourceDir
+
+    @OutputDirectory
+    File intermediateDir
+
+    String getFlxPath() {
+        return "${intermediateDir}/app.flx"
+    }
+
+    @TaskAction
+    void build() {
+        if (!sourceDir.isDirectory()) {
+            throw new GradleException("Invalid Flutter source directory: ${sourceDir}")
+        }
+
+        intermediateDir.mkdirs()
+        project.exec {
+            executable "${sdkDir}/bin/flutter"
+            workingDir sourceDir
+            args "build"
+            args "-o", flxPath
+            args "--snapshot", "${intermediateDir}/snapshot_blob.bin"
+            args "--depfile", "${intermediateDir}/snapshot_blob.bin.d"
+            args "--working-dir", "${intermediateDir}/flx"
+        }
+    }
+}
diff --git a/examples/hello_android/buildSrc/src/main/resources/META-INF/gradle-plugins/flutter.properties b/examples/hello_android/buildSrc/src/main/resources/META-INF/gradle-plugins/flutter.properties
new file mode 100644
index 0000000..f8219be
--- /dev/null
+++ b/examples/hello_android/buildSrc/src/main/resources/META-INF/gradle-plugins/flutter.properties
@@ -0,0 +1 @@
+implementation-class=org.domokit.sky.gradle.FlutterPlugin
diff --git a/examples/hello_android/settings.gradle b/examples/hello_android/settings.gradle
new file mode 100644
index 0000000..e7b4def
--- /dev/null
+++ b/examples/hello_android/settings.gradle
@@ -0,0 +1 @@
+include ':app'
diff --git a/examples/stocks/test_driver/scroll_perf.dart b/examples/stocks/test_driver/scroll_perf.dart
index 623f7b7..8188a55 100644
--- a/examples/stocks/test_driver/scroll_perf.dart
+++ b/examples/stocks/test_driver/scroll_perf.dart
@@ -3,11 +3,9 @@
 // found in the LICENSE file.
 
 import 'package:flutter_driver/driver_extension.dart';
-import 'package:flutter_driver/src/error.dart';
 import 'package:stocks/main.dart' as app;
 
 void main() {
-  flutterDriverLog.listen(print);
   enableFlutterDriverExtension();
   app.main();
 }
diff --git a/examples/stocks/test_driver/scroll_perf_test.dart b/examples/stocks/test_driver/scroll_perf_test.dart
index 6165417..a6b3a30 100644
--- a/examples/stocks/test_driver/scroll_perf_test.dart
+++ b/examples/stocks/test_driver/scroll_perf_test.dart
@@ -19,22 +19,29 @@
         driver.close();
     });
 
-    test('tap on the floating action button; verify counter', () async {
-      // Find the scrollable stock list
-      ObjectRef stockList = await driver.findByValueKey('stock-list');
-      expect(stockList, isNotNull);
+    test('measure', () async {
+      Map<String, dynamic> profileJson = await driver.traceAction(() async {
+        // Find the scrollable stock list
+        ObjectRef stockList = await driver.findByValueKey('stock-list');
+        expect(stockList, isNotNull);
 
-      // Scroll down 5 times
-      for (int i = 0; i < 5; i++) {
-        await driver.scroll(stockList, 0.0, -300.0, new Duration(milliseconds: 300));
-        await new Future<Null>.delayed(new Duration(milliseconds: 500));
-      }
+        // Scroll down
+        for (int i = 0; i < 5; i++) {
+          await driver.scroll(stockList, 0.0, -300.0, new Duration(milliseconds: 300));
+          await new Future<Null>.delayed(new Duration(milliseconds: 500));
+        }
 
-      // Scroll up 5 times
-      for (int i = 0; i < 5; i++) {
-        await driver.scroll(stockList, 0.0, 300.0, new Duration(milliseconds: 300));
-        await new Future<Null>.delayed(new Duration(milliseconds: 500));
-      }
+        // Scroll up
+        for (int i = 0; i < 5; i++) {
+          await driver.scroll(stockList, 0.0, 300.0, new Duration(milliseconds: 300));
+          await new Future<Null>.delayed(new Duration(milliseconds: 500));
+        }
+      });
+
+      // Usually the profile is saved to a file and then analyzed using
+      // chrom://tracing or a script. Both are out of scope for this little
+      // test, so all we do is check that we received something.
+      expect(profileJson, isNotNull);
     });
   });
 }
diff --git a/packages/flutter/lib/src/material/app_bar.dart b/packages/flutter/lib/src/material/app_bar.dart
index cac114e..bbb1286 100644
--- a/packages/flutter/lib/src/material/app_bar.dart
+++ b/packages/flutter/lib/src/material/app_bar.dart
@@ -42,9 +42,9 @@
 
   AppBar copyWith({
     Key key,
-    Widget left,
-    Widget center,
-    List<Widget> right,
+    Widget leading,
+    Widget title,
+    List<Widget> actions,
     WidgetBuilder flexibleSpace,
     double foregroundOpacity,
     int elevation,
@@ -54,9 +54,9 @@
   }) {
     return new AppBar(
       key: key ?? this.key,
-      leading: left ?? this.leading,
-      title: center ?? this.title,
-      actions: right ?? this.actions,
+      leading: leading ?? this.leading,
+      title: title ?? this.title,
+      actions: actions ?? this.actions,
       flexibleSpace: flexibleSpace ?? this.flexibleSpace,
       foregroundOpacity: foregroundOpacity ?? this.foregroundOpacity,
       tabBar: tabBar ?? this.tabBar,
diff --git a/packages/flutter/lib/src/material/scaffold.dart b/packages/flutter/lib/src/material/scaffold.dart
index c0a6e0a..faf2cc8 100644
--- a/packages/flutter/lib/src/material/scaffold.dart
+++ b/packages/flutter/lib/src/material/scaffold.dart
@@ -406,10 +406,10 @@
     if (appBar == null)
       return null;
     EdgeInsets appBarPadding = new EdgeInsets.only(top: padding.top);
-    Widget left = appBar.leading;
-    if (left == null) {
+    Widget leading = appBar.leading;
+    if (leading == null) {
       if (config.drawer != null) {
-        left = new IconButton(
+        leading = new IconButton(
           icon: Icons.menu,
           onPressed: openDrawer,
           tooltip: 'Open navigation menu' // TODO(ianh): Figure out how to localize this string
@@ -417,7 +417,7 @@
       } else {
         _shouldShowBackArrow ??= Navigator.canPop(context);
         if (_shouldShowBackArrow) {
-          left = new IconButton(
+          leading = new IconButton(
             icon: Icons.arrow_back,
             onPressed: () => Navigator.pop(context),
             tooltip: 'Back' // TODO(ianh): Figure out how to localize this string
@@ -429,7 +429,7 @@
       elevation: elevation ?? appBar.elevation ?? 4,
       padding: appBarPadding,
       foregroundOpacity: foregroundOpacity,
-      left: left
+      leading: leading
     );
   }
 
diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart
index 300600e..3e88be0 100644
--- a/packages/flutter/lib/src/widgets/basic.dart
+++ b/packages/flutter/lib/src/widgets/basic.dart
@@ -1540,8 +1540,8 @@
 
 /// A paragraph of rich text.
 ///
-/// This class is rarely used directly. Instead, consider using [Text], which
-/// integrates with [DefaultTextStyle].
+/// Consider using [Text], which integrates with [DefaultTextStyle], rather than
+/// this widget, which requires explicit styling.
 class RichText extends LeafRenderObjectWidget {
   RichText({ Key key, this.text }) : super(key: key) {
     assert(text != null);
diff --git a/packages/flutter_driver/lib/src/driver.dart b/packages/flutter_driver/lib/src/driver.dart
index b58aeba..59fd3b3 100644
--- a/packages/flutter_driver/lib/src/driver.dart
+++ b/packages/flutter_driver/lib/src/driver.dart
@@ -29,6 +29,9 @@
   FlutterDriver.connectedTo(this._serviceClient, this._appIsolate);
 
   static const String _kFlutterExtensionMethod = 'ext.flutter_driver';
+  static const String _kStartTracingMethod = 'ext.flutter_startTracing';
+  static const String _kStopTracingMethod = 'ext.flutter_stopTracing';
+  static const String _kDownloadTraceDataMethod = 'ext.flutter_downloadTraceData';
   static const Duration _kDefaultTimeout = const Duration(seconds: 5);
   static const Duration _kDefaultPauseBetweenRetries = const Duration(milliseconds: 160);
 
@@ -205,6 +208,63 @@
     return result.text;
   }
 
+  /// Starts recording performance traces.
+  Future<Null> startTracing() async {
+    try {
+      await _appIsolate.invokeExtension(_kStartTracingMethod);
+      return null;
+    } catch(error, stackTrace) {
+      throw new DriverError(
+        'Failed to start tracing due to remote error',
+        error,
+        stackTrace
+      );
+    }
+  }
+
+  /// Stops recording performance traces and downloads the trace profile.
+  // TODO(yjbanov): return structured data rather than raw JSON once we have a
+  //                stable protocol to talk to.
+  Future<Map<String, dynamic>> stopTracingAndDownloadProfile() async {
+    Map<String, dynamic> stopResult =
+        await _appIsolate.invokeExtension(_kStopTracingMethod);
+    String traceFilePath = stopResult['trace_file_path'];
+
+    // Tracing data isn't available immediately as some of it is queued up in
+    // the event loop.
+    Stopwatch sw = new Stopwatch()..start();
+    while(sw.elapsed < const Duration(seconds: 30)) {
+      Map<String, dynamic> downloadResult =
+          await _appIsolate.invokeExtension(_kDownloadTraceDataMethod, <String, String>{
+            'trace_file_path': traceFilePath,
+          });
+
+      if (downloadResult['success'] == false)
+        throw new DriverError('Failed to download trace file: $traceFilePath');
+      else if (downloadResult['status'] != 'not ready')
+        return downloadResult;
+
+      // Give the event loop a chance to flush the trace log
+      await new Future<Null>.delayed(const Duration(milliseconds: 200));
+    }
+    throw new DriverError(
+      'Timed out waiting for tracing profile to become ready for download.'
+    );
+  }
+
+  /// Runs [action] and outputs a performance trace for it.
+  ///
+  /// Waits for the `Future` returned by [action] to complete prior to stopping
+  /// the trace.
+  ///
+  /// This is merely a convenience wrapper on top of [startTracing] and
+  /// [stopTracingAndDownloadProfile].
+  Future<Map<String, dynamic>> traceAction(Future<dynamic> action()) async {
+    await startTracing();
+    await action();
+    return stopTracingAndDownloadProfile();
+  }
+
   /// Calls the [evaluator] repeatedly until the result of the evaluation
   /// satisfies the [matcher].
   ///
diff --git a/packages/flutter_tools/lib/src/commands/analyze.dart b/packages/flutter_tools/lib/src/commands/analyze.dart
index b0a2ab3..011ba0b 100644
--- a/packages/flutter_tools/lib/src/commands/analyze.dart
+++ b/packages/flutter_tools/lib/src/commands/analyze.dart
@@ -332,8 +332,6 @@
       'Analyzing [${mainFile.path}]...',
       new RegExp('^\\[(hint|error)\\] Unused import \\(${mainFile.path},'),
       new RegExp(r'^\[.+\] .+ \(.+/\.pub-cache/.+'),
-      new RegExp('^\\[error\\] The argument type \'List<T>\' cannot be assigned to the parameter type \'List<.+>\''), // until we have generic methods, there's not much choice if you want to use map()
-      new RegExp(r'^\[error\] Type check failed: .*\(dynamic\) is not of type'), // allow unchecked casts from dynamic
       new RegExp('\\[warning\\] Missing concrete implementation of \'RenderObject\\.applyPaintTransform\''), // https://github.com/dart-lang/sdk/issues/25232
       new RegExp(r'[0-9]+ (error|warning|hint|lint).+found\.'),
       new RegExp(r'^$'),
diff --git a/packages/flutter_tools/lib/src/ios/simulators.dart b/packages/flutter_tools/lib/src/ios/simulators.dart
index 9623642..fe02d31 100644
--- a/packages/flutter_tools/lib/src/ios/simulators.dart
+++ b/packages/flutter_tools/lib/src/ios/simulators.dart
@@ -22,7 +22,7 @@
 const String _xcrunPath = '/usr/bin/xcrun';
 
 /// Test device created by Flutter when no other device is available.
-const String _kFlutterTestDevice = 'flutter.test.device';
+const String _kFlutterTestDeviceSuffix = '(Flutter)';
 
 class IOSSimulators extends PollingDeviceDiscovery {
   IOSSimulators() : super('IOSSimulators');
@@ -97,7 +97,7 @@
   }
 
   SimDevice _createTestDevice() {
-    String deviceType = _findSuitableDeviceType();
+    SimDeviceType deviceType = _findSuitableDeviceType();
     if (deviceType == null) {
       return null;
     }
@@ -109,18 +109,19 @@
 
     // Delete any old test devices
     getDevices()
-      .where((SimDevice d) => d.name == _kFlutterTestDevice)
+      .where((SimDevice d) => d.name.endsWith(_kFlutterTestDeviceSuffix))
       .forEach(_deleteDevice);
 
     // Create new device
-    List<String> args = [_xcrunPath, 'simctl', 'create', _kFlutterTestDevice, deviceType, runtime];
+    String deviceName = '${deviceType.name} $_kFlutterTestDeviceSuffix';
+    List<String> args = [_xcrunPath, 'simctl', 'create', deviceName, deviceType.identifier, runtime];
     printTrace(args.join(' '));
     runCheckedSync(args);
 
-    return getDevices().firstWhere((SimDevice d) => d.name == _kFlutterTestDevice);
+    return getDevices().firstWhere((SimDevice d) => d.name == deviceName);
   }
 
-  String _findSuitableDeviceType() {
+  SimDeviceType _findSuitableDeviceType() {
     List<Map<String, dynamic>> allTypes = _list(SimControlListSection.devicetypes);
     List<Map<String, dynamic>> usableTypes = allTypes
       .where((Map<String, dynamic> info) => info['name'].startsWith('iPhone'))
@@ -136,7 +137,10 @@
       );
     }
 
-    return usableTypes.first['identifier'];
+    return new SimDeviceType(
+      usableTypes.first['name'],
+      usableTypes.first['identifier']
+    );
   }
 
   String _findSuitableRuntime() {
@@ -300,6 +304,30 @@
   static const SimControlListSection pairs = const SimControlListSection._('pairs');
 }
 
+/// A simulated device type.
+///
+/// Simulated device types can be listed using the command
+/// `xcrun simctl list devicetypes`.
+class SimDeviceType {
+  SimDeviceType(this.name, this.identifier);
+
+  /// The name of the device type.
+  ///
+  /// Examples:
+  ///
+  ///     "iPhone 6s"
+  ///     "iPhone 6 Plus"
+  final String name;
+
+  /// The identifier of the device type.
+  ///
+  /// Examples:
+  ///
+  ///     "com.apple.CoreSimulator.SimDeviceType.iPhone-6s"
+  ///     "com.apple.CoreSimulator.SimDeviceType.iPhone-6-Plus"
+  final String identifier;
+}
+
 class SimDevice {
   SimDevice(this.category, this.data);
 
diff --git a/packages/flutter_tools/templates/create/lib/main.dart.tmpl b/packages/flutter_tools/templates/create/lib/main.dart.tmpl
index 3acf224..618bc1f 100644
--- a/packages/flutter_tools/templates/create/lib/main.dart.tmpl
+++ b/packages/flutter_tools/templates/create/lib/main.dart.tmpl
@@ -20,6 +20,7 @@
 }
 
 class FlutterDemo extends StatefulWidget {
+  @override
   _FlutterDemoState createState() => new _FlutterDemoState();
 }
 
@@ -32,6 +33,7 @@
     });
   }
 
+  @override
   Widget build(BuildContext context) {
     return new Scaffold(
       appBar: new AppBar(