[current results ui] Move expanded state to the expanded result widget

Change-Id: I5718a1793861893bdcd3e10bf742682b44ccfb84
Reviewed-on: https://dart-review.googlesource.com/c/dart_ci/+/162520
Reviewed-by: Karl Klose <karlklose@google.com>
diff --git a/current_results_ui/lib/results.dart b/current_results_ui/lib/results.dart
index 37f94aa..2322322 100644
--- a/current_results_ui/lib/results.dart
+++ b/current_results_ui/lib/results.dart
@@ -12,137 +12,148 @@
 import 'filter.dart';
 import 'query.dart';
 
-class ResultsPanel extends StatefulWidget {
+const Color lightCoral = Color.fromARGB(255, 240, 128, 128);
+const Color gold = Color.fromARGB(255, 255, 215, 0);
+const resultColors = {
+  'pass': Colors.lightGreen,
+  'flaky': gold,
+  'fail': lightCoral,
+};
+const kinds = ['pass', 'fail', 'flaky'];
+
+class ResultsPanel extends StatelessWidget {
   final QueryResults queryResults;
   final bool showAll;
 
   ResultsPanel(this.queryResults, {this.showAll = true});
 
   @override
-  ResultsPanelState createState() => ResultsPanelState();
+  Widget build(BuildContext context) {
+    if (queryResults.noQuery) {
+      return Align(child: QuerySuggestionsPage());
+    }
+    return ListView.builder(
+      itemCount: queryResults.names.length,
+      itemBuilder: (BuildContext context, int index) {
+        final name = queryResults.names[index];
+        final changeGroups = queryResults.grouped[name];
+        final counts = queryResults.counts[name];
+        final partialResults = queryResults.partialResults;
+        return ExpandableResult(
+            name, changeGroups, counts, showAll, partialResults);
+      },
+    );
+  }
 }
 
-class ResultsPanelState extends State<ResultsPanel> {
-  static const Color lightCoral = Color.fromARGB(255, 240, 128, 128);
-  static const Color gold = Color.fromARGB(255, 255, 215, 0);
-  static const resultColors = {
-    'pass': Colors.lightGreen,
-    'flaky': gold,
-    'fail': lightCoral,
-  };
+class ExpandableResult extends StatefulWidget {
+  final String name;
+  final changeGroups;
+  final counts;
+  final bool showAll;
+  final bool partialResults;
 
-  static const kinds = ['pass', 'fail', 'flaky'];
-  List<bool> expanded = [];
+  ExpandableResult(this.name, this.changeGroups, this.counts, this.showAll,
+      this.partialResults)
+      : super(key: Key(name));
+
+  @override
+  _ExpandableResultState createState() => _ExpandableResultState();
+}
+
+class _ExpandableResultState extends State<ExpandableResult> {
+  bool expanded = false;
 
   @override
   Widget build(BuildContext context) {
-    if (widget.queryResults.noQuery) {
-      return Align(child: QuerySuggestionsPage());
-    }
-    if (expanded.length != widget.queryResults.names.length) {
-      expanded = List<bool>.filled(widget.queryResults.names.length, false);
-    }
-    return ListView.builder(
-      itemCount: widget.queryResults.names.length,
-      itemBuilder: itemBuilder(widget.queryResults),
-    );
-  }
+    final name = widget.name;
+    final changeGroups = widget.changeGroups;
 
-  IndexedWidgetBuilder itemBuilder(QueryResults results) {
-    return (BuildContext context, int index) {
-      final name = results.names[index];
-      final changeGroups = results.grouped[name];
-      final counts = results.counts[name];
-      if (!widget.showAll &&
-          changeGroups.keys.every((change) => change.matches)) {
-        return Container(height: 0.0, width: 0.0);
-      }
-      return Column(
-        crossAxisAlignment: CrossAxisAlignment.start,
-        children: [
-          Container(
-            height: 28.0,
-            padding: EdgeInsets.only(top: 0.0, left: 8.0),
-            child: Row(
-              crossAxisAlignment: CrossAxisAlignment.center,
-              children: [
-                IconButton(
-                  icon: Icon(
-                      expanded[index] ? Icons.expand_less : Icons.expand_more),
-                  onPressed: () =>
-                      setState(() => expanded[index] = !expanded[index]),
-                ),
-                for (final kind in kinds)
-                  Container(
-                    width: 24,
-                    alignment: Alignment.center,
-                    margin: EdgeInsets.symmetric(horizontal: 1.0),
-                    decoration: BoxDecoration(
-                      color: counts.containsKey(kind)
-                          ? resultColors[kind]
-                          : Colors.transparent,
-                      shape: BoxShape.circle,
-                    ),
-                    child: Text('${counts[kind] ?? ''}',
-                        style: TextStyle(fontSize: 14.0)),
+    if (!widget.showAll &&
+        changeGroups.keys.every((change) => change.matches)) {
+      return Container(height: 0.0, width: 0.0);
+    }
+    return Column(
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: [
+        Container(
+          height: 28.0,
+          padding: EdgeInsets.only(top: 0.0, left: 8.0),
+          child: Row(
+            crossAxisAlignment: CrossAxisAlignment.center,
+            children: [
+              IconButton(
+                icon: Icon(expanded ? Icons.expand_less : Icons.expand_more),
+                onPressed: () => setState(() => expanded = !expanded),
+              ),
+              for (final kind in kinds)
+                Container(
+                  width: 24,
+                  alignment: Alignment.center,
+                  margin: EdgeInsets.symmetric(horizontal: 1.0),
+                  decoration: BoxDecoration(
+                    color: widget.counts.containsKey(kind)
+                        ? resultColors[kind]
+                        : Colors.transparent,
+                    shape: BoxShape.circle,
                   ),
-                Expanded(
-                  flex: 1,
-                  child: Container(
-                    padding: EdgeInsets.only(left: 4.0),
-                    alignment: Alignment.centerLeft,
-                    child: SingleChildScrollView(
-                      scrollDirection: Axis.horizontal,
-                      reverse: true,
-                      child: SelectableText(
-                        results.partialResults
-                            ? '$name (partial results)'
-                            : name,
-                        style: TextStyle(fontSize: 16.0),
-                      ),
+                  child: Text('${widget.counts[kind] ?? ''}',
+                      style: TextStyle(fontSize: 14.0)),
+                ),
+              Expanded(
+                flex: 1,
+                child: Container(
+                  padding: EdgeInsets.only(left: 4.0),
+                  alignment: Alignment.centerLeft,
+                  child: SingleChildScrollView(
+                    scrollDirection: Axis.horizontal,
+                    reverse: true,
+                    child: SelectableText(
+                      widget.partialResults ? '$name (partial results)' : name,
+                      style: TextStyle(fontSize: 16.0),
                     ),
                   ),
                 ),
-                IconButton(
-                    icon: Icon(Icons.history),
-                    onPressed: () => html.window.open(
-                        Uri(
-                                path: '/',
-                                fragment: widget.showAll
-                                    ? 'showLatestFailures=false&test=$name'
-                                    : 'test=$name')
-                            .toString(),
-                        '_blank')),
-              ],
-            ),
-          ),
-          if (expanded[index])
-            for (final change in changeGroups.keys
-                .where((key) => widget.showAll || !key.matches))
-              Container(
-                alignment: Alignment.topLeft,
-                padding: EdgeInsets.only(left: 48.0),
-                constraints: BoxConstraints.loose(Size.fromWidth(500.0)),
-                child: Column(
-                  crossAxisAlignment: CrossAxisAlignment.start,
-                  children: [
-                    Container(
-                      padding: EdgeInsets.only(top: 12.0),
-                      child: Text(
-                          '$change (${changeGroups[change].length} configurations)',
-                          style: TextStyle(
-                              backgroundColor: resultColors[change.kind],
-                              fontSize: 16.0)),
-                    ),
-                    for (final result in changeGroups[change])
-                      SelectableText(result.configuration),
-                  ],
-                ),
               ),
-          if (expanded[index]) SizedBox(height: 12.0),
-        ],
-      );
-    };
+              IconButton(
+                  icon: Icon(Icons.history),
+                  onPressed: () => html.window.open(
+                      Uri(
+                              path: '/',
+                              fragment: widget.showAll
+                                  ? 'showLatestFailures=false&test=$name'
+                                  : 'test=$name')
+                          .toString(),
+                      '_blank')),
+            ],
+          ),
+        ),
+        if (expanded)
+          for (final change in changeGroups.keys
+              .where((key) => widget.showAll || !key.matches))
+            Container(
+              alignment: Alignment.topLeft,
+              padding: EdgeInsets.only(left: 48.0),
+              constraints: BoxConstraints.loose(Size.fromWidth(500.0)),
+              child: Column(
+                crossAxisAlignment: CrossAxisAlignment.start,
+                children: [
+                  Container(
+                    padding: EdgeInsets.only(top: 12.0),
+                    child: Text(
+                        '$change (${changeGroups[change].length} configurations)',
+                        style: TextStyle(
+                            backgroundColor: resultColors[change.kind],
+                            fontSize: 16.0)),
+                  ),
+                  for (final result in changeGroups[change])
+                    SelectableText(result.configuration),
+                ],
+              ),
+            ),
+        if (expanded) SizedBox(height: 12.0),
+      ],
+    );
   }
 }