Remove remaining "## Sample code" segments, and fix the snippet generator. (#27793)

This converts all remaining "## Sample code" segments into snippets, and fixes
the snippet generator to handle multiple snippets in the same dartdoc block
properly.

I also generated, compiled, and ran each of the existing application samples,
and fixed them up to be more useful and/or just run without errors.

This PR fixes these problems with examples:

1. Switching tabs in a snippet now works if there is more than one snippet in
   a single dartdoc block.
2. Generation of snippet code now works if there is more than one snippet.
3. Contrast of text and links in the code sample block has been improved to
   recommended levels.
4. Added five new snippet templates, including a "freeform" template to make
   it possible to show examples that need to change the app instantiation.
5. Fixed several examples to run properly, a couple by adding the "Scaffold"
   widget to the template, a couple by just fixing their code.
6. Fixed visual look of some of the samples when they run by placing many
   samples inside of a Scaffold.
7. In order to make it easier to run locally, changed the sample analyzer to
   remove the contents of the supplied temp directory before running, since
   having files that hang around is problematic (only a problem when running
   locally with the `--temp` argument).
8. Added a `SampleCheckerException` class, and handle sample checking
   exceptions more gracefully.
9. Deprecated the old "## Sample code" designation, and added enforcement for
   the deprecation.
10. Removed unnecessary `new` from templates (although they never appeared in
   the samples thanks to dartfmt, but still).

Fixes #26398
Fixes #27411
diff --git a/dev/bots/analyze-sample-code.dart b/dev/bots/analyze-sample-code.dart
index a749dd2..6af1470 100644
--- a/dev/bots/analyze-sample-code.dart
+++ b/dev/bots/analyze-sample-code.dart
@@ -87,12 +87,42 @@
 
   Directory tempDirectory;
   if (parsedArguments.wasParsed('temp')) {
-    tempDirectory = Directory(parsedArguments['temp']);
-    if (!tempDirectory.existsSync()) {
-      tempDirectory.createSync(recursive: true);
+    tempDirectory = Directory(path.join(Directory.systemTemp.absolute.path, path.basename(parsedArguments['temp'])));
+    if (path.basename(parsedArguments['temp']) != parsedArguments['temp']) {
+      stderr.writeln('Supplied temporary directory name should be a name, not a path. Using ${tempDirectory.absolute.path} instead.');
+    }
+    print('Leaving temporary output in ${tempDirectory.absolute.path}.');
+    // Make sure that any directory left around from a previous run is cleared
+    // out.
+    if (tempDirectory.existsSync()) {
+      tempDirectory.deleteSync(recursive: true);
+    }
+    tempDirectory.createSync();
+  }
+  try {
+    exitCode = SampleChecker(flutterPackage, tempDirectory: tempDirectory).checkSamples();
+  } on SampleCheckerException catch (e) {
+    stderr.write(e);
+    exit(1);
+  }
+}
+
+class SampleCheckerException implements Exception {
+  SampleCheckerException(this.message, {this.file, this.line});
+  final String message;
+  final String file;
+  final int line;
+
+  @override
+  String toString() {
+    if (file != null || line != null) {
+      final String fileStr = file == null ? '' : '$file:';
+      final String lineStr = line == null ? '' : '$line:';
+      return '$fileStr$lineStr Error: $message';
+    } else {
+      return 'Error: $message';
     }
   }
-  exitCode = SampleChecker(flutterPackage, tempDirectory: tempDirectory).checkSamples();
 }
 
 /// Checks samples and code snippets for analysis errors.
@@ -129,10 +159,10 @@
   static final RegExp _dartDocSampleEndRegex = RegExp(r'{@end-tool}');
 
   /// A RegExp that matches the start of a code block within dartdoc.
-  static final RegExp _codeBlockStartRegex = RegExp(r'/// ```dart.*$');
+  static final RegExp _codeBlockStartRegex = RegExp(r'///\s+```dart.*$');
 
   /// A RegExp that matches the end of a code block within dartdoc.
-  static final RegExp _codeBlockEndRegex = RegExp(r'/// ```\s*$');
+  static final RegExp _codeBlockEndRegex = RegExp(r'///\s+```\s*$');
 
   /// A RegExp that matches a Dart constructor.
   static final RegExp _constructorRegExp = RegExp(r'[A-Z][a-zA-Z0-9<>.]*\(');
@@ -173,10 +203,7 @@
   }
 
   static List<File> _listDartFiles(Directory directory, {bool recursive = false}) {
-    return directory.listSync(recursive: recursive, followLinks: false)
-        .whereType<File>()
-        .where((File file) => path.extension(file.path) == '.dart')
-        .toList();
+    return directory.listSync(recursive: recursive, followLinks: false).whereType<File>().where((File file) => path.extension(file.path) == '.dart').toList();
   }
 
   /// Computes the headers needed for each sample file.
@@ -218,14 +245,14 @@
         }
         stderr.writeln('\nFound ${errors.length} sample code errors.');
       }
-      if (!_keepTmp) {
+      if (_keepTmp) {
+        print('Leaving temporary directory ${_tempDirectory.path} around for your perusal.');
+      } else {
         try {
           _tempDirectory.deleteSync(recursive: true);
         } on FileSystemException catch (e) {
           stderr.writeln('Failed to delete ${_tempDirectory.path}: $e');
         }
-      } else {
-        print('Leaving temporary directory ${_tempDirectory.path} around for your perusal.');
       }
       // If we made a snapshot, remove it (so as not to clutter up the tree).
       if (_snippetsSnapshotPath != null) {
@@ -288,8 +315,12 @@
     print('Generating snippet for ${snippet.start?.filename}:${snippet.start?.line}');
     final ProcessResult process = _runSnippetsScript(args);
     if (process.exitCode != 0) {
-      throw 'Unable to create snippet for ${snippet.start.filename}:${snippet.start.line} '
-          '(using input from ${inputFile.path}):\n${process.stdout}\n${process.stderr}';
+      throw SampleCheckerException(
+        'Unable to create snippet for ${snippet.start.filename}:${snippet.start.line} '
+            '(using input from ${inputFile.path}):\n${process.stdout}\n${process.stderr}',
+        file: snippet.start.filename,
+        line: snippet.start.line,
+      );
     }
     return outputFile;
   }
@@ -304,11 +335,14 @@
       final String relativeFilePath = path.relative(file.path, from: _flutterPackage.path);
       final List<String> sampleLines = file.readAsLinesSync();
       final List<Section> preambleSections = <Section>[];
+      // Whether or not we're in the file-wide preamble section ("Examples can assume").
       bool inPreamble = false;
+      // Whether or not we're in a code sample
       bool inSampleSection = false;
+      // Whether or not we're in a snippet code sample (with template) specifically.
       bool inSnippet = false;
+      // Whether or not we're in a '```dart' segment.
       bool inDart = false;
-      bool foundDart = false;
       int lineNumber = 0;
       final List<String> block = <String>[];
       List<String> snippetArgs = <String>[];
@@ -318,7 +352,7 @@
         final String trimmedLine = line.trim();
         if (inSnippet) {
           if (!trimmedLine.startsWith(_dartDocPrefix)) {
-            throw '$relativeFilePath:$lineNumber: Snippet section unterminated.';
+            throw SampleCheckerException('Snippet section unterminated.', file: relativeFilePath, line: lineNumber);
           }
           if (_dartDocSampleEndRegex.hasMatch(trimmedLine)) {
             snippets.add(
@@ -342,53 +376,51 @@
             preambleSections.add(_processBlock(startLine, block));
             block.clear();
           } else if (!line.startsWith('// ')) {
-            throw '$relativeFilePath:$lineNumber: Unexpected content in sample code preamble.';
+            throw SampleCheckerException('Unexpected content in sample code preamble.', file: relativeFilePath, line: lineNumber);
           } else {
             block.add(line.substring(3));
           }
         } else if (inSampleSection) {
-          if (!trimmedLine.startsWith(_dartDocPrefix) || trimmedLine.startsWith('$_dartDocPrefix ## ')) {
+          if (_dartDocSampleEndRegex.hasMatch(trimmedLine)) {
             if (inDart) {
-              throw '$relativeFilePath:$lineNumber: Dart section inexplicably unterminated.';
-            }
-            if (!foundDart) {
-              throw '$relativeFilePath:$lineNumber: No dart block found in sample code section';
+              throw SampleCheckerException("Dart section didn't terminate before end of sample", file: relativeFilePath, line: lineNumber);
             }
             inSampleSection = false;
-          } else {
-            if (inDart) {
-              if (_codeBlockEndRegex.hasMatch(trimmedLine)) {
-                inDart = false;
-                final Section processed = _processBlock(startLine, block);
-                if (preambleSections.isEmpty) {
-                  sections.add(processed);
-                } else {
-                  sections.add(Section.combine(preambleSections
-                    ..toList()
-                    ..add(processed)));
-                }
-                block.clear();
-              } else if (trimmedLine == _dartDocPrefix) {
-                block.add('');
+          }
+          if (inDart) {
+            if (_codeBlockEndRegex.hasMatch(trimmedLine)) {
+              inDart = false;
+              final Section processed = _processBlock(startLine, block);
+              if (preambleSections.isEmpty) {
+                sections.add(processed);
               } else {
-                final int index = line.indexOf(_dartDocPrefixWithSpace);
-                if (index < 0) {
-                  throw '$relativeFilePath:$lineNumber: Dart section inexplicably did not '
-                      'contain "$_dartDocPrefixWithSpace" prefix.';
-                }
-                block.add(line.substring(index + 4));
+                sections.add(Section.combine(preambleSections
+                  ..toList()
+                  ..add(processed)));
               }
-            } else if (_codeBlockStartRegex.hasMatch(trimmedLine)) {
-              assert(block.isEmpty);
-              startLine = Line(
-                '',
-                filename: relativeFilePath,
-                line: lineNumber + 1,
-                indent: line.indexOf(_dartDocPrefixWithSpace) + _dartDocPrefixWithSpace.length,
-              );
-              inDart = true;
-              foundDart = true;
+              block.clear();
+            } else if (trimmedLine == _dartDocPrefix) {
+              block.add('');
+            } else {
+              final int index = line.indexOf(_dartDocPrefixWithSpace);
+              if (index < 0) {
+                throw SampleCheckerException(
+                  'Dart section inexplicably did not contain "$_dartDocPrefixWithSpace" prefix.',
+                  file: relativeFilePath,
+                  line: lineNumber,
+                );
+              }
+              block.add(line.substring(index + 4));
             }
+          } else if (_codeBlockStartRegex.hasMatch(trimmedLine)) {
+            assert(block.isEmpty);
+            startLine = Line(
+              '',
+              filename: relativeFilePath,
+              line: lineNumber + 1,
+              indent: line.indexOf(_dartDocPrefixWithSpace) + _dartDocPrefixWithSpace.length,
+            );
+            inDart = true;
           }
         }
         if (!inSampleSection) {
@@ -397,11 +429,7 @@
             assert(block.isEmpty);
             startLine = Line('', filename: relativeFilePath, line: lineNumber + 1, indent: 3);
             inPreamble = true;
-          } else if (trimmedLine == '/// ## Sample code' ||
-              trimmedLine.startsWith('/// ## Sample code:') ||
-              trimmedLine == '/// ### Sample code' ||
-              trimmedLine.startsWith('/// ### Sample code:') ||
-              sampleMatch != null) {
+          } else if (sampleMatch != null) {
             inSnippet = sampleMatch != null ? sampleMatch[1] == 'snippet' : false;
             if (inSnippet) {
               startLine = Line(
@@ -418,7 +446,12 @@
               }
             }
             inSampleSection = !inSnippet;
-            foundDart = false;
+          } else if (RegExp(r'///\s*#+\s+[Ss]ample\s+[Cc]ode:?$').hasMatch(trimmedLine)) {
+            throw SampleCheckerException(
+              "Found deprecated '## Sample code' section: use {@tool sample}...{@end-tool} instead.",
+              file: relativeFilePath,
+              line: lineNumber,
+            );
           }
         }
       }
@@ -598,14 +631,17 @@
               ),
             ),
           );
-          throw 'Cannot analyze dartdocs; analysis errors exist in ${file.path}: $error';
+          throw SampleCheckerException(
+            'Cannot analyze dartdocs; analysis errors exist: $error',
+            file: file.path,
+            line: lineNumber,
+          );
         }
 
         if (errorCode == 'unused_element' || errorCode == 'unused_local_variable') {
           // We don't really care if sample code isn't used!
           continue;
         }
-
         if (isSnippet) {
           addAnalysisError(
             file,
@@ -630,14 +666,20 @@
                 Line('', filename: file.path, line: lineNumber),
               ),
             );
-            throw 'Failed to parse error message (read line number as $lineNumber; '
-                'total number of lines is ${fileContents.length}): $error';
+            throw SampleCheckerException('Failed to parse error message: $error', file: file.path, line: lineNumber);
           }
 
           final Section actualSection = sections[file.path];
+          if (actualSection == null) {
+            throw SampleCheckerException(
+              "Unknown section for ${file.path}. Maybe the temporary directory wasn't empty?",
+              file: file.path,
+              line: lineNumber,
+            );
+          }
           final Line actualLine = actualSection.code[lineNumber - 1];
 
-          if (actualLine.filename == null) {
+          if (actualLine?.filename == null) {
             if (errorCode == 'missing_identifier' && lineNumber > 1) {
               if (fileContents[lineNumber - 2].endsWith(',')) {
                 final Line actualLine = sections[file.path].code[lineNumber - 2];
@@ -698,7 +740,7 @@
   /// into valid Dart code.
   Section _processBlock(Line line, List<String> block) {
     if (block.isEmpty) {
-      throw '$line: Empty ```dart block in sample code.';
+      throw SampleCheckerException('$line: Empty ```dart block in sample code.');
     }
     if (block.first.startsWith('new ') || block.first.startsWith('const ') || block.first.startsWith(_constructorRegExp)) {
       _expressionId += 1;
@@ -721,8 +763,8 @@
         // treated as a separate code block.
         if (block[index] == '' || block[index] == '// ...') {
           if (subline == null)
-            throw '${Line('', filename: line.filename, line: line.line + index, indent: line.indent)}: '
-                'Unexpected blank line or "// ..." line near start of subblock in sample code.';
+            throw SampleCheckerException('${Line('', filename: line.filename, line: line.line + index, indent: line.indent)}: '
+                'Unexpected blank line or "// ..." line near start of subblock in sample code.');
           subblocks += 1;
           subsections.add(_processBlock(subline, buffer));
           buffer.clear();
diff --git a/dev/bots/test/analyze-sample-code-test-input/known_broken_documentation.dart b/dev/bots/test/analyze-sample-code-test-input/known_broken_documentation.dart
index e624931..6a00c15 100644
--- a/dev/bots/test/analyze-sample-code-test-input/known_broken_documentation.dart
+++ b/dev/bots/test/analyze-sample-code-test-input/known_broken_documentation.dart
@@ -18,8 +18,7 @@
 /// blabla 0.0, the penzance blabla is blabla not blabla at all. Bla the blabla
 /// 1.0, the blabla is blabla blabla blabla an blabla blabla.
 ///
-/// ### Sample code
-///
+/// {@tool sample}
 /// Bla blabla blabla some [Text] when the `_blabla` blabla blabla is true, and
 /// blabla it when it is blabla:
 ///
@@ -29,9 +28,9 @@
 ///   child: const Text('Poor wandering ones!'),
 /// )
 /// ```
+/// {@end-tool}
 ///
-/// ## Sample code
-///
+/// {@tool sample}
 /// Bla blabla blabla some [Text] when the `_blabla` blabla blabla is true, and
 /// blabla finale blabla:
 ///
@@ -41,3 +40,4 @@
 ///   child: const Text('Poor wandering ones!'),
 /// )
 /// ```
+/// {@end-tool}
diff --git a/dev/bots/test/analyze-sample-code_test.dart b/dev/bots/test/analyze-sample-code_test.dart
index 8d3d883..0d46585 100644
--- a/dev/bots/test/analyze-sample-code_test.dart
+++ b/dev/bots/test/analyze-sample-code_test.dart
@@ -17,9 +17,9 @@
       ..removeWhere((String line) => line.startsWith('Analyzer output:'));
     expect(process.exitCode, isNot(equals(0)));
     expect(stderrLines, <String>[
-      'known_broken_documentation.dart:27:9: new Opacity(',
+      'known_broken_documentation.dart:26:9: new Opacity(',
       '>>> Unnecessary new keyword (unnecessary_new)',
-      'known_broken_documentation.dart:39:9: new Opacity(',
+      'known_broken_documentation.dart:38:9: new Opacity(',
       '>>> Unnecessary new keyword (unnecessary_new)',
       '',
       'Found 1 sample code errors.',
diff --git a/dev/docs/assets/snippets.css b/dev/docs/assets/snippets.css
index 65a7d33..4fb200a 100644
--- a/dev/docs/assets/snippets.css
+++ b/dev/docs/assets/snippets.css
@@ -1,6 +1,6 @@
 /* Styles for handling custom code snippets */
 .snippet-container {
-  background-color: #45aae8;
+  background-color: #2372a3;
   padding: 10px;
   overflow: auto;
 }
@@ -30,8 +30,21 @@
   color: white;
 }
 
+.snippet-description a:link {
+  color: #c7fcf4;
+}
+.snippet-description a:visited {
+  color: #c7dbfc;
+}
+.snippet-description a:hover {
+  color: white;
+}
+.snippet-description a:active {
+  color: #80b0fc;
+}
+
 .snippet-buttons button {
-  background-color: #45aae8;
+  background-color: #2372a3;
   border-style: none;
   color: white;
   padding: 10px 24px;
@@ -82,7 +95,7 @@
   height: 28px;
   width: 28px;
   transition: .3s ease;
-  background-color: #45aae8;
+  background-color: #2372a3;
 }
 
 .copy-button {
@@ -102,7 +115,7 @@
 
 .copy-image {
   opacity: 0.65;
-  color: #45aae8;
+  color: #2372a3;
   font-size: 28px;
   padding-top: 4px;
 }
diff --git a/dev/docs/assets/snippets.js b/dev/docs/assets/snippets.js
index b51c96e..9d4da92 100644
--- a/dev/docs/assets/snippets.js
+++ b/dev/docs/assets/snippets.js
@@ -2,15 +2,10 @@
  * Scripting for handling custom code snippets
  */
 
-const shortSnippet = 'shortSnippet';
-const longSnippet = 'longSnippet';
-var visibleSnippet = shortSnippet;
-
 /**
- * Shows the requested snippet. Values for "name" can be "shortSnippet" or
- * "longSnippet".
+ * Shows the requested snippet, and stores the current state in visibleSnippet.
  */
-function showSnippet(name) {
+function showSnippet(name, visibleSnippet) {
   if (visibleSnippet == name) return;
   if (visibleSnippet != null) {
     var shown = document.getElementById(visibleSnippet);
@@ -39,6 +34,7 @@
   if (button != null) {
     button.setAttributeNode(selectedAttribute);
   }
+  return visibleSnippet;
 }
 
 // Finds a sibling to given element with the given id.
@@ -64,8 +60,8 @@
 // Copies the text inside the currently visible snippet to the clipboard, or the
 // given element, if any.
 function copyTextToClipboard(element) {
-  if (element == null) {
-    var elementSelector = '#' + visibleSnippet + ' .language-dart';
+  if (typeof element === 'string') {
+    var elementSelector = '#' + element + ' .language-dart';
     element = document.querySelector(elementSelector);
     if (element == null) {
       console.log(
diff --git a/dev/snippets/README.md b/dev/snippets/README.md
index 0606877..a1e7875 100644
--- a/dev/snippets/README.md
+++ b/dev/snippets/README.md
@@ -7,8 +7,9 @@
 This takes code in dartdocs, like this:
 
 ```dart
-/// The following is a skeleton of a stateless widget subclass called `GreenFrog`:
 /// {@tool snippet --template="stateless_widget"}
+/// The following is a skeleton of a stateless widget subclass called `GreenFrog`.
+/// ```dart
 /// class GreenFrog extends StatelessWidget {
 ///   const GreenFrog({ Key key }) : super(key: key);
 ///
@@ -17,6 +18,7 @@
 ///     return Container(color: const Color(0xFF2DBD3A));
 ///   }
 /// }
+/// ```
 /// {@end-tool}
 ```
 
diff --git a/dev/snippets/config/skeletons/application.html b/dev/snippets/config/skeletons/application.html
index afe9c4c..7d7e17b 100644
--- a/dev/snippets/config/skeletons/application.html
+++ b/dev/snippets/config/skeletons/application.html
@@ -1,26 +1,30 @@
 {@inject-html}
 <div class="snippet-buttons">
-  <button id="shortSnippetButton" onclick="showSnippet(shortSnippet);" selected>Sample</button>
-  <button id="longSnippetButton" onclick="showSnippet(longSnippet);">Sample in an App</button>
+  <script>var visibleSnippet{{serial}} = "shortSnippet{{serial}}";</script>
+  <button id="shortSnippet{{serial}}Button"
+          onclick="visibleSnippet{{serial}} = showSnippet('shortSnippet{{serial}}', visibleSnippet{{serial}});"
+          selected>Sample</button>
+  <button id="longSnippet{{serial}}Button"
+          onclick="visibleSnippet{{serial}} = showSnippet('longSnippet{{serial}}', visibleSnippet{{serial}});">Sample in an App</button>
 </div>
 <div class="snippet-container">
-  <div class="snippet" id="shortSnippet">
+  <div class="snippet" id="shortSnippet{{serial}}">
     {{description}}
     <div class="copyable-container">
       <button class="copy-button-overlay copy-button" title="Copy to clipboard"
-              onclick="copyTextToClipboard();">
+              onclick="copyTextToClipboard(visibleSnippet{{serial}});">
         <i class="material-icons copy-image">assignment</i>
       </button>
       <pre class="language-{{language}}"><code class="language-{{language}}">{{code}}</code></pre>
     </div>
   </div>
-  <div class="snippet" id="longSnippet" hidden>
+  <div class="snippet" id="longSnippet{{serial}}" hidden>
     <div class="snippet-description">To create a sample project with this code snippet, run:<br/>
       <span class="snippet-create-command">flutter create --sample={{id}} mysample</span>
     </div>
     <div class="copyable-container">
       <button class="copy-button-overlay copy-button" title="Copy to clipboard"
-              onclick="copyTextToClipboard();">
+              onclick="copyTextToClipboard(visibleSnippet{{serial}});">
         <i class="material-icons copy-image">assignment</i>
       </button>
       <pre class="language-{{language}}"><code class="language-{{language}}">{{app}}</code></pre>
diff --git a/dev/snippets/config/skeletons/sample.html b/dev/snippets/config/skeletons/sample.html
index fbdc5fe..66ae99b 100644
--- a/dev/snippets/config/skeletons/sample.html
+++ b/dev/snippets/config/skeletons/sample.html
@@ -1,6 +1,6 @@
 {@inject-html}
 <div class="snippet-buttons">
-  <button id="shortSnippetButton" selected>Sample</button>
+  <button id="shortSnippet{{serial}}Button" selected>Sample</button>
 </div>
 <div class="snippet-container">
   <div class="snippet">{{description}}
diff --git a/dev/snippets/config/templates/README.md b/dev/snippets/config/templates/README.md
index 66ab21b..8d7f260 100644
--- a/dev/snippets/config/templates/README.md
+++ b/dev/snippets/config/templates/README.md
@@ -49,6 +49,11 @@
 The templates available for using as an argument to the snippets tool are as
 follows:
 
+- [`freeform`](freeform.tmpl) :
+  This is a simple template for which you provide everything.  It has no code of
+  its own, just the sections for `imports`, `main`, and `preamble`. You must
+  provide the `main` section in order to have a `main()`.
+
 - [`stateful_widget`](stateful_widget.tmpl) :
   The default code block will be placed as the body of the `State` object of a
   StatefulWidget subclass. Because the default code block is placed as the body
@@ -58,8 +63,26 @@
   function calls are not allowed in the preamble.  It also has an `imports`
   section to import additional packages. Please only import things that are part
   of flutter or part of default dependencies for a `flutter create` project.
+  It creates a WidgetsApp around the child stateful widget.
 
 - [`stateless_widget`](stateless_widget.tmpl) :
   Identical to the `stateful_widget` template, except that the default code
-  block is inserted as the return value for a pre-existing `build` function in a
-  StatelessWidget, instead of being at the class level.
+  block is inserted as the `build` function in a
+  StatelessWidget. There is no need to include the @override before the build
+  funciton (the template adds this for you).
+
+- [`stateful_widget_material`](stateful_widget_material.tmpl) : Similar to
+  `stateful_widget`, except that it imports the material library, and uses
+  a MaterialApp instead of WidgetsApp.
+
+- [`stateless_widget_material`](stateless_widget_material.tmpl) : Similar to
+  `stateless_widget`, except that it imports the material library, and uses
+  a MaterialApp instead of WidgetsApp.
+
+- [`stateful_widget_scaffold`](stateful_widget_scaffold.tmpl) : Similar to
+  `stateful_widget_material`, except that it wraps the stateful widget with a
+  Scaffold.
+
+- [`stateless_widget_scaffold`](stateless_widget_scaffold.tmpl) : Similar to
+  `stateless_widget_material`, except that it wraps the stateless widget with a
+  Scaffold.
diff --git a/dev/snippets/config/templates/freeform.tmpl b/dev/snippets/config/templates/freeform.tmpl
new file mode 100644
index 0000000..82e2ec9
--- /dev/null
+++ b/dev/snippets/config/templates/freeform.tmpl
@@ -0,0 +1,11 @@
+// Flutter code sample for {{id}}
+
+{{description}}
+
+{{code-imports}}
+
+{{code-main}}
+
+{{code-preamble}}
+
+{{code}}
diff --git a/dev/snippets/config/templates/stateful_widget.tmpl b/dev/snippets/config/templates/stateful_widget.tmpl
index 0cac378..5f595ba 100644
--- a/dev/snippets/config/templates/stateful_widget.tmpl
+++ b/dev/snippets/config/templates/stateful_widget.tmpl
@@ -1,34 +1,36 @@
+// Flutter code sample for {{id}}
+
 {{description}}
 
-import 'package:flutter/material.dart';
+import 'package:flutter/widgets.dart';
 
 {{code-imports}}
 
 void main() => runApp(new MyApp());
 
+/// This Widget is the main application widget.
 class MyApp extends StatelessWidget {
-  // This widget is the root of your application.
   @override
   Widget build(BuildContext context) {
-    return new MaterialApp(
-      title: 'Flutter Code Sample for {{id}}',
-      theme: new ThemeData(
-        primarySwatch: Colors.blue,
-      ),
-      home: new MyStatefulWidget(),
+    return WidgetsApp(
+      title: 'Flutter Code Sample',
+      home: MyStatefulWidget(),
+      color: const Color(0xffffffff),
     );
   }
 }
 
 {{code-preamble}}
 
+/// This is the stateful widget that the main application instantiates.
 class MyStatefulWidget extends StatefulWidget {
   MyStatefulWidget({Key key}) : super(key: key);
 
   @override
-  _MyStatefulWidgetState createState() => new _MyStatefulWidgetState();
+  _MyStatefulWidgetState createState() => _MyStatefulWidgetState();
 }
 
+// This is the private State class that goes with MyStatefulWidget.
 class _MyStatefulWidgetState extends State<MyStatefulWidget> {
   {{code}}
 }
diff --git a/dev/snippets/config/templates/stateful_widget_material.tmpl b/dev/snippets/config/templates/stateful_widget_material.tmpl
new file mode 100644
index 0000000..67881ea
--- /dev/null
+++ b/dev/snippets/config/templates/stateful_widget_material.tmpl
@@ -0,0 +1,35 @@
+// Flutter code sample for {{id}}
+
+{{description}}
+
+import 'package:flutter/material.dart';
+
+{{code-imports}}
+
+void main() => runApp(new MyApp());
+
+/// This Widget is the main application widget.
+class MyApp extends StatelessWidget {
+  static const String _title = 'Flutter Code Sample';
+
+  @override
+  Widget build(BuildContext context) {
+    return MaterialApp(
+      title: _title,
+      home: MyStatefulWidget(),
+    );
+  }
+}
+
+{{code-preamble}}
+
+class MyStatefulWidget extends StatefulWidget {
+  MyStatefulWidget({Key key}) : super(key: key);
+
+  @override
+  _MyStatefulWidgetState createState() => _MyStatefulWidgetState();
+}
+
+class _MyStatefulWidgetState extends State<MyStatefulWidget> {
+  {{code}}
+}
diff --git a/dev/snippets/config/templates/stateful_widget_scaffold.tmpl b/dev/snippets/config/templates/stateful_widget_scaffold.tmpl
new file mode 100644
index 0000000..f59ac3f
--- /dev/null
+++ b/dev/snippets/config/templates/stateful_widget_scaffold.tmpl
@@ -0,0 +1,38 @@
+// Flutter code sample for {{id}}
+
+{{description}}
+
+import 'package:flutter/material.dart';
+
+{{code-imports}}
+
+void main() => runApp(new MyApp());
+
+/// This Widget is the main application widget.
+class MyApp extends StatelessWidget {
+  static const String _title = 'Flutter Code Sample';
+
+  @override
+  Widget build(BuildContext context) {
+    return MaterialApp(
+      title: _title,
+      home: Scaffold(
+        appBar: AppBar(title: Text(_title)),
+        body: MyStatefulWidget(),
+      ),
+    );
+  }
+}
+
+{{code-preamble}}
+
+class MyStatefulWidget extends StatefulWidget {
+  MyStatefulWidget({Key key}) : super(key: key);
+
+  @override
+  _MyStatefulWidgetState createState() => _MyStatefulWidgetState();
+}
+
+class _MyStatefulWidgetState extends State<MyStatefulWidget> {
+  {{code}}
+}
diff --git a/dev/snippets/config/templates/stateless_widget.tmpl b/dev/snippets/config/templates/stateless_widget.tmpl
index 74fce8b..0cbaaea 100644
--- a/dev/snippets/config/templates/stateless_widget.tmpl
+++ b/dev/snippets/config/templates/stateless_widget.tmpl
@@ -1,32 +1,33 @@
+// Flutter code sample for {{id}}
+
 {{description}}
 
-import 'package:flutter/material.dart';
+import 'package:flutter/widgets.dart';
 
 {{code-imports}}
 
 void main() => runApp(new MyApp());
 
+/// This Widget is the main application widget.
 class MyApp extends StatelessWidget {
-  // This widget is the root of your application.
   @override
   Widget build(BuildContext context) {
-    return new MaterialApp(
-      title: 'Flutter Code Sample for {{id}}',
-      theme: new ThemeData(
-        primarySwatch: Colors.blue,
-      ),
-      home: new MyStatelessWidget(),
+    return WidgetsApp(
+      title: 'Flutter Code Sample',
+      builder: (BuildContext context, Widget navigator) {
+        return MyStatelessWidget();
+      },
+      color: const Color(0xffffffff),
     );
   }
 }
 
 {{code-preamble}}
 
+/// This is the stateless widget that the main application instantiates.
 class MyStatelessWidget extends StatelessWidget {
   MyStatelessWidget({Key key}) : super(key: key);
 
   @override
-  Widget build(BuildContext context) {
-    return {{code}};
-  }
+  {{code}}
 }
diff --git a/dev/snippets/config/templates/stateless_widget_material.tmpl b/dev/snippets/config/templates/stateless_widget_material.tmpl
new file mode 100644
index 0000000..66c0bef
--- /dev/null
+++ b/dev/snippets/config/templates/stateless_widget_material.tmpl
@@ -0,0 +1,33 @@
+// Flutter code sample for {{id}}
+
+{{description}}
+
+import 'package:flutter/material.dart';
+
+{{code-imports}}
+
+void main() => runApp(new MyApp());
+
+/// This Widget is the main application widget.
+class MyApp extends StatelessWidget {
+  static const String _title = 'Flutter Code Sample';
+
+  @override
+  Widget build(BuildContext context) {
+    return MaterialApp(
+      title: _title,
+      home: MyStatelessWidget(),
+    );
+  }
+}
+
+
+{{code-preamble}}
+
+/// This is the stateless widget that the main application instantiates.
+class MyStatelessWidget extends StatelessWidget {
+  MyStatelessWidget({Key key}) : super(key: key);
+
+  @override
+  {{code}}
+}
diff --git a/dev/snippets/config/templates/stateless_widget_scaffold.tmpl b/dev/snippets/config/templates/stateless_widget_scaffold.tmpl
new file mode 100644
index 0000000..1f62e41
--- /dev/null
+++ b/dev/snippets/config/templates/stateless_widget_scaffold.tmpl
@@ -0,0 +1,36 @@
+// Flutter code sample for {{id}}
+
+{{description}}
+
+import 'package:flutter/material.dart';
+
+{{code-imports}}
+
+void main() => runApp(new MyApp());
+
+/// This Widget is the main application widget.
+class MyApp extends StatelessWidget {
+  static const String _title = 'Flutter Code Sample';
+
+  @override
+  Widget build(BuildContext context) {
+    return MaterialApp(
+      title: _title,
+      home: Scaffold(
+        appBar: AppBar(title: Text(_title)),
+        body: MyStatelessWidget(),
+      ),
+    );
+  }
+}
+
+
+{{code-preamble}}
+
+/// This is the stateless widget that the main application instantiates.
+class MyStatelessWidget extends StatelessWidget {
+  MyStatelessWidget({Key key}) : super(key: key);
+
+  @override
+  {{code}}
+}
diff --git a/dev/snippets/lib/main.dart b/dev/snippets/lib/main.dart
index 417fd4e..9c25637 100644
--- a/dev/snippets/lib/main.dart
+++ b/dev/snippets/lib/main.dart
@@ -11,6 +11,7 @@
 import 'configuration.dart';
 import 'snippets.dart';
 
+const String _kSerialOption = 'serial';
 const String _kElementOption = 'element';
 const String _kHelpOption = 'help';
 const String _kInputOption = 'input';
@@ -75,6 +76,11 @@
     defaultsTo: environment['ELEMENT_NAME'],
     help: 'The name of the element that this snippet belongs to.',
   );
+  parser.addOption(
+    _kSerialOption,
+    defaultsTo: environment['INVOCATION_INDEX'],
+    help: 'A unique serial number for this snippet tool invocation.',
+  );
   parser.addFlag(
     _kHelpOption,
     defaultsTo: false,
@@ -117,6 +123,7 @@
   final String packageName = args[_kPackageOption] != null && args[_kPackageOption].isNotEmpty ? args[_kPackageOption] : null;
   final String libraryName = args[_kLibraryOption] != null && args[_kLibraryOption].isNotEmpty ? args[_kLibraryOption] : null;
   final String elementName = args[_kElementOption] != null && args[_kElementOption].isNotEmpty ? args[_kElementOption] : null;
+  final String serial = args[_kSerialOption] != null && args[_kSerialOption].isNotEmpty ? args[_kSerialOption] : null;
   final List<String> id = <String>[];
   if (args[_kOutputOption] != null) {
     id.add(path.basename(path.basenameWithoutExtension(args[_kOutputOption])));
@@ -130,10 +137,13 @@
     if (elementName != null) {
       id.add(elementName);
     }
+    if (serial != null) {
+      id.add(serial);
+    }
     if (id.isEmpty) {
       errorExit('Unable to determine ID. At least one of --$_kPackageOption, '
-          '--$_kLibraryOption, --$_kElementOption, or the environment variables '
-          'PACKAGE_NAME, LIBRARY_NAME, or ELEMENT_NAME must be non-empty.');
+          '--$_kLibraryOption, --$_kElementOption, -$_kSerialOption, or the environment variables '
+          'PACKAGE_NAME, LIBRARY_NAME, ELEMENT_NAME, or INVOCATION_INDEX must be non-empty.');
     }
   }
 
@@ -149,6 +159,7 @@
       'sourceLine': environment['SOURCE_LINE'] != null
           ? int.tryParse(environment['SOURCE_LINE'])
           : null,
+      'serial': serial,
       'package': packageName,
       'library': libraryName,
       'element': elementName,
diff --git a/dev/snippets/lib/snippets.dart b/dev/snippets/lib/snippets.dart
index 0881694..e8764b1 100644
--- a/dev/snippets/lib/snippets.dart
+++ b/dev/snippets/lib/snippets.dart
@@ -31,7 +31,8 @@
   SnippetGenerator({Configuration configuration})
       : configuration = configuration ??
             // Flutter's root is four directories up from this script.
-            Configuration(flutterRoot: Directory(Platform.environment['FLUTTER_ROOT'] ?? path.canonicalize(path.join(path.dirname(path.fromUri(Platform.script)), '..', '..', '..')))) {
+            Configuration(flutterRoot: Directory(Platform.environment['FLUTTER_ROOT']
+                ?? path.canonicalize(path.join(path.dirname(path.fromUri(Platform.script)), '..', '..', '..')))) {
     this.configuration.createOutputDirectory();
   }
 
@@ -72,10 +73,10 @@
         // Remove any leading/trailing empty comment lines.
         // We don't want to remove ALL empty comment lines, only the ones at the
         // beginning and the end.
-        while (description.last == '// ') {
+        while (description.isNotEmpty && description.last == '// ') {
           description.removeLast();
         }
-        while (description.first == '// ') {
+        while (description.isNotEmpty && description.first == '// ') {
           description.removeAt(0);
         }
         return description.join('\n').trim();
@@ -98,7 +99,7 @@
   ///
   /// Takes into account the [type] and doesn't substitute in the id and the app
   /// if not a [SnippetType.application] snippet.
-  String interpolateSkeleton(SnippetType type, List<_ComponentTuple> injections, String skeleton) {
+  String interpolateSkeleton(SnippetType type, List<_ComponentTuple> injections, String skeleton, Map<String, Object> metadata) {
     final List<String> result = <String>[];
     const HtmlEscape htmlEscape = HtmlEscape();
     String language;
@@ -128,12 +129,13 @@
       'language': language ?? 'dart',
     }..addAll(type == SnippetType.application
         ? <String, String>{
+            'serial': metadata['serial'].toString() ?? '0',
             'id':
                 injections.firstWhere((_ComponentTuple tuple) => tuple.name == 'id').mergedContent,
             'app':
                 htmlEscape.convert(injections.firstWhere((_ComponentTuple tuple) => tuple.name == 'app').mergedContent),
           }
-        : <String, String>{'id': '', 'app': ''});
+        : <String, String>{'serial': '', 'id': '', 'app': ''});
     return skeleton.replaceAllMapped(RegExp('{{(${substitutions.keys.join('|')})}}'), (Match match) {
       return substitutions[match[1]];
     });
@@ -180,6 +182,16 @@
     return file.readAsStringSync(encoding: Encoding.getByName('utf-8'));
   }
 
+  String _addLineNumbers(String app) {
+    final StringBuffer buffer = StringBuffer();
+    int count = 0;
+    for (String line in app.split('\n')) {
+      count++;
+      buffer.writeln('${count.toString().padLeft(5, ' ')}: $line');
+    }
+    return buffer.toString();
+  }
+
   /// The main routine for generating snippets.
   ///
   /// The [input] is the file containing the dartdoc comments (minus the leading
@@ -220,7 +232,7 @@
         try {
           app = formatter.format(app);
         } on FormatterException catch (exception) {
-          stderr.write('Code to format:\n$app\n');
+          stderr.write('Code to format:\n${_addLineNumbers(app)}\n');
           errorExit('Unable to format snippet app template: $exception');
         }
 
@@ -250,6 +262,6 @@
         break;
     }
     final String skeleton = _loadFileAsUtf8(configuration.getHtmlSkeletonFile(type));
-    return interpolateSkeleton(type, snippetData, skeleton);
+    return interpolateSkeleton(type, snippetData, skeleton, metadata);
   }
 }
diff --git a/dev/tools/gen_keycodes/data/keyboard_key.tmpl b/dev/tools/gen_keycodes/data/keyboard_key.tmpl
index cf85979..bc030de 100644
--- a/dev/tools/gen_keycodes/data/keyboard_key.tmpl
+++ b/dev/tools/gen_keycodes/data/keyboard_key.tmpl
@@ -32,7 +32,7 @@
 /// look at the physical key to make sure that regardless of the character the
 /// key produces, you got the key that is in that location on the keyboard.
 ///
-/// {@tool snippet --template=stateful_widget}
+/// {@tool snippet --template=stateful_widget_scaffold}
 /// This example shows how to detect if the user has selected the logical "Q"
 /// key.
 ///
@@ -270,7 +270,7 @@
 /// looking for "the key next next to the TAB key", since on a French keyboard,
 /// the key next to the TAB key has an "A" on it.
 ///
-/// {@tool snippet --template=stateful_widget}
+/// {@tool snippet --template=stateful_widget_scaffold}
 /// This example shows how to detect if the user has selected the physical key
 /// to the right of the CAPS LOCK key.
 ///