Update ExpansionTile, added a sample app (#10019)

diff --git a/examples/catalog/lib/expansion_tile_sample.dart b/examples/catalog/lib/expansion_tile_sample.dart
new file mode 100644
index 0000000..3e639a0
--- /dev/null
+++ b/examples/catalog/lib/expansion_tile_sample.dart
@@ -0,0 +1,87 @@
+// 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/material.dart';
+
+class Entry {
+  Entry(this.title, [this.children = const <Entry>[]]);
+  final String title;
+  final List<Entry> children;
+}
+
+final List<Entry> data = <Entry>[
+  new Entry('Chapter A',
+    <Entry>[
+      new Entry('Section A0',
+        <Entry>[
+          new Entry('Item A0.1'),
+          new Entry('Item A0.2'),
+          new Entry('Item A0.3'),
+        ],
+      ),
+      new Entry('Section A1'),
+      new Entry('Section A2'),
+    ],
+  ),
+  new Entry('Chapter B',
+    <Entry>[
+      new Entry('Section B0'),
+      new Entry('Section B1'),
+    ],
+  ),
+  new Entry('Chapter C',
+    <Entry>[
+      new Entry('Section C0'),
+      new Entry('Section C1'),
+      new Entry('Section C2',
+        <Entry>[
+          new Entry('Item C2.0'),
+          new Entry('Item C2.1'),
+          new Entry('Item C2.2'),
+          new Entry('Item C2.3'),
+        ],
+      ),
+    ],
+  ),
+];
+
+class EntryItem extends StatelessWidget {
+  EntryItem(this.entry);
+
+  final Entry entry;
+
+  Widget _buildTiles(Entry root) {
+    if (root.children.isEmpty)
+      return new ListTile(title: new Text(root.title));
+    return new ExpansionTile(
+      key: new ValueKey<Entry>(root),
+      title: new Text(root.title),
+      children: root.children.map(_buildTiles).toList(),
+    );
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return _buildTiles(entry);
+  }
+}
+
+class ExpansionTileSample extends StatelessWidget {
+  @override
+  Widget build(BuildContext context) {
+    return new Scaffold(
+      appBar: new AppBar(
+        title: const Text('ExpansionTile'),
+      ),
+      body: new ListView.builder(
+        itemBuilder: (BuildContext context, int index) => new EntryItem(data[index]),
+        itemCount: data.length,
+      ),
+    );
+  }
+}
+
+void main() {
+  runApp(new MaterialApp(home: new ExpansionTileSample()));
+}
diff --git a/examples/catalog/test/expansion_tile_sample_test.dart b/examples/catalog/test/expansion_tile_sample_test.dart
new file mode 100644
index 0000000..55fbf4e
--- /dev/null
+++ b/examples/catalog/test/expansion_tile_sample_test.dart
@@ -0,0 +1,91 @@
+// 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/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+import '../lib/expansion_tile_sample.dart' as expansion_tile_sample;
+import '../lib/expansion_tile_sample.dart' show Entry;
+
+void main() {
+  testWidgets("expansion_tile sample smoke test", (WidgetTester tester) async {
+    expansion_tile_sample.main();
+    await tester.pump();
+
+    // Initially only the top level EntryItems (the "chapters") are present.
+    for (Entry chapter in expansion_tile_sample.data) {
+      expect(find.text(chapter.title), findsOneWidget);
+      for (Entry section in chapter.children) {
+        expect(find.text(section.title), findsNothing);
+        for (Entry item in section.children)
+          expect(find.text(item.title), findsNothing);
+      }
+    }
+
+    Future<Null> scrollUpOneEntry() async {
+      await tester.dragFrom(const Offset(200.0, 200.0), const Offset(0.0, -88.00));
+      await tester.pumpAndSettle();
+    }
+
+    Future<Null> tapEntry(String title) async {
+      await tester.tap(find.text(title));
+      await tester.pumpAndSettle();
+    }
+
+    // Expand the chapters. Now the chapter and sections, but not the
+    // items, should be present.
+    for (Entry chapter in expansion_tile_sample.data.reversed)
+      await tapEntry(chapter.title);
+
+    for (Entry chapter in expansion_tile_sample.data) {
+      expect(find.text(chapter.title), findsOneWidget);
+      for (Entry section in chapter.children) {
+        expect(find.text(section.title), findsOneWidget);
+        await scrollUpOneEntry();
+        for (Entry item in section.children)
+          expect(find.text(item.title), findsNothing);
+      }
+      await scrollUpOneEntry();
+    }
+
+    // - scroll to the top -
+    await tester.flingFrom(const Offset(200.0, 200.0), const Offset(0.0, 100.0), 5000.0);
+    await tester.pumpAndSettle();
+
+    // Expand the sections. Now Widgets for all three levels should be present.
+    for (Entry chapter in expansion_tile_sample.data) {
+      for (Entry section in chapter.children) {
+        await tapEntry(section.title);
+        await scrollUpOneEntry();
+      }
+      await scrollUpOneEntry();
+    }
+
+    // We're scrolled to the bottom so the very last item is visible.
+    // Working in reverse order, so we don't need to do anymore scrolling,
+    // check that everything is visible and close the sections and
+    // chapters as we go up.
+    for (Entry chapter in expansion_tile_sample.data.reversed) {
+      expect(find.text(chapter.title), findsOneWidget);
+      for (Entry section in chapter.children.reversed) {
+        expect(find.text(section.title), findsOneWidget);
+        for (Entry item in section.children.reversed)
+          expect(find.text(item.title), findsOneWidget);
+        await tapEntry(section.title); // close the section
+      }
+      await tapEntry(chapter.title); // close the chapter
+    }
+
+    // Finally only the top level EntryItems (the "chapters") are present.
+    for (Entry chapter in expansion_tile_sample.data) {
+      expect(find.text(chapter.title), findsOneWidget);
+      for (Entry section in chapter.children) {
+        expect(find.text(section.title), findsNothing);
+        for (Entry item in section.children)
+          expect(find.text(item.title), findsNothing);
+      }
+    }
+
+  });
+}
diff --git a/packages/flutter/lib/src/material/expansion_tile.dart b/packages/flutter/lib/src/material/expansion_tile.dart
index 8c1e7f8..e288c2d 100644
--- a/packages/flutter/lib/src/material/expansion_tile.dart
+++ b/packages/flutter/lib/src/material/expansion_tile.dart
@@ -20,7 +20,9 @@
 /// the tile to reveal or hide the [children].
 ///
 /// This widget is typically used with [ListView] to create an
-/// "expand / collapse" list entry.
+/// "expand / collapse" list entry. When used with scrolling widgets like
+/// [ListView], a unique [key] must be specified to enable the [ExpansionTile] to
+/// save and restore its expanded state when it is scrolled in and out of view.
 ///
 /// See also:
 ///
@@ -110,7 +112,11 @@
       if (_isExpanded)
         _controller.forward();
       else
-        _controller.reverse();
+        _controller.reverse().then((Null value) {
+          setState(() {
+            // Rebuild without widget.children.
+          });
+        });
       PageStorage.of(context)?.writeState(context, _isExpanded);
     });
     if (widget.onExpansionChanged != null)
@@ -172,10 +178,12 @@
       ..begin = Colors.transparent
       ..end = widget.backgroundColor ?? Colors.transparent;
 
+    final bool closed = !_isExpanded && _controller.isDismissed;
     return new AnimatedBuilder(
       animation: _controller.view,
       builder: _buildChildren,
-      child: new Column(children: widget.children),
+      child: closed ? null : new Column(children: widget.children),
     );
+
   }
 }