Migrate current_results_ui to null safety.
This is a copy of PR https://github.com/dart-lang/dart_ci/pull/125
by Devon Carew (devoncarew).

Drop web-only packages and use flutter_lints.

Specify the deployment Flutter version in README.md.

Change-Id: Ic81b7910c725b6f0cdb28bea94e3c78489f824f1
Reviewed-on: https://dart-review.googlesource.com/c/dart_ci/+/233780
Reviewed-by: Devon Carew <devoncarew@google.com>
Commit-Queue: William Hesse <whesse@google.com>
diff --git a/current_results_ui/README.md b/current_results_ui/README.md
index f55fe59..08b5c4e 100644
--- a/current_results_ui/README.md
+++ b/current_results_ui/README.md
@@ -5,15 +5,7 @@
 ## About
 
 This web app displays results from the Current Results API in the dart-ci
-project. During development and while testing before deployment, it
-fetches results from the Current Results API in the dart-ci-staging project.
-
-It is written in Flutter web, and has a few uses of dart:html that prevent
-it from working on other Flutter platforms.
-
-It is deployed to the current_results directory of the dart_ci Firebase
-hosted web app at https://dart-ci.firebaseapp.com/ (dart-ci-staging for
-testing).
+project.
 
 ## Usage
 
@@ -24,3 +16,23 @@
 It includes options to filter the current results show by test
 name (partial prefixes of test name allowed) and by configuration (partial
 prefixes allowed).
+
+## Deployment
+
+It is written for deployment in Flutter web, but may work on other platforms.
+
+It is deployed to the current_results directory of the dart_ci Firebase
+hosted web app at https://dart-ci.firebaseapp.com/ (dart-ci-staging for
+testing).
+
+It is currently built and deployed with Flutter version 2.11.0-0.1.pre.
+
+Build with
+
+    flutter build web
+
+and deploy by copying the contents of build/web to
+
+    [results_feed]/build/web/current_results
+
+before deploying the results feed.
diff --git a/current_results_ui/analysis_options.yaml b/current_results_ui/analysis_options.yaml
new file mode 100644
index 0000000..b293c2f
--- /dev/null
+++ b/current_results_ui/analysis_options.yaml
@@ -0,0 +1,13 @@
+# The following line activates a set of recommended lints for Flutter apps,
+# packages, and plugins designed to encourage good coding practices.
+include: package:flutter_lints/flutter.yaml
+
+linter:
+  rules:
+    # Disabled as there are currently many violations.
+    use_key_in_widget_constructors: false
+    # Disabled - currently one violation.
+    avoid_print: false
+
+# Additional information about this file can be found at
+# https://dart.dev/guides/language/analysis-options
diff --git a/current_results_ui/lib/filter.dart b/current_results_ui/lib/filter.dart
index 765c982..f010896 100644
--- a/current_results_ui/lib/filter.dart
+++ b/current_results_ui/lib/filter.dart
@@ -45,11 +45,12 @@
           crossAxisAlignment: CrossAxisAlignment.start,
           children: [
             ConstrainedBox(
-              constraints: BoxConstraints(maxHeight: 100.0),
+              constraints: const BoxConstraints(maxHeight: 100.0),
               child: Scrollbar(
                 child: SingleChildScrollView(
                   child: Container(
-                    padding: EdgeInsets.only(left: 8.0, right: 8.0, top: 8.0),
+                    padding:
+                        const EdgeInsets.only(left: 8.0, right: 8.0, top: 8.0),
                     alignment: Alignment.topLeft,
                     child: Wrap(
                       spacing: 8.0,
@@ -79,13 +80,13 @@
               thickness: 2,
             ),
             Padding(
-              padding: EdgeInsets.only(left: 12.0),
+              padding: const EdgeInsets.only(left: 12.0),
               child: SizedBox(
                 width: 300.0,
                 height: 36.0,
                 child: TextField(
                   controller: controller,
-                  decoration: InputDecoration(
+                  decoration: const InputDecoration(
                       hintText: 'Test, configuration or experiment prefix'),
                   onSubmitted: (value) {
                     if (value.trim().isEmpty) return;
diff --git a/current_results_ui/lib/instructions.dart b/current_results_ui/lib/instructions.dart
index e074150..f2c99e4 100644
--- a/current_results_ui/lib/instructions.dart
+++ b/current_results_ui/lib/instructions.dart
@@ -3,14 +3,14 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'package:flutter/material.dart';
-import 'package:flutter/rendering.dart';
 
 class Instructions extends StatelessWidget {
+  @override
   Widget build(context) {
     return SingleChildScrollView(
       controller: ScrollController(),
       child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
-        Text(
+        const Text(
           'Enter a query to see current test results',
           style: TextStyle(fontSize: 24.0),
         ),
@@ -40,7 +40,7 @@
           },
           {'description': 'null-safe language tests', 'terms': 'language/'},
         ]) ...[
-          SizedBox(height: 12),
+          const SizedBox(height: 12),
           InkWell(
             onTap: () {
               Navigator.pushNamed(
@@ -64,8 +64,8 @@
             ),
           ),
         ],
-        SizedBox(height: 24.0),
-        Text(
+        const SizedBox(height: 24.0),
+        const Text(
           'About Current Results',
           style: TextStyle(fontSize: 24.0),
         ),
@@ -84,8 +84,8 @@
 
   Widget paragraph(String text) {
     return Container(
-        constraints: BoxConstraints(maxWidth: 500.0),
-        padding: EdgeInsets.only(top: 12.0),
-        child: Text(text, style: TextStyle(height: 1.5)));
+        constraints: const BoxConstraints(maxWidth: 500.0),
+        padding: const EdgeInsets.only(top: 12.0),
+        child: Text(text, style: const TextStyle(height: 1.5)));
   }
 }
diff --git a/current_results_ui/lib/main.dart b/current_results_ui/lib/main.dart
index 2c9fda4..cf9b35e 100644
--- a/current_results_ui/lib/main.dart
+++ b/current_results_ui/lib/main.dart
@@ -2,21 +2,21 @@
 // for details. 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:clippy/browser.dart' as clippy;
 import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
 import 'package:provider/provider.dart';
-import 'dart:html' as html;
+import 'package:url_launcher/url_launcher.dart' as url_launcher;
 
 import 'filter.dart';
 import 'query.dart';
 import 'results.dart';
 
 void main() {
-  runApp(Providers());
+  runApp(const Providers());
 }
 
 class CurrentResultsApp extends StatelessWidget {
-  const CurrentResultsApp({Key key}) : super(key: key);
+  const CurrentResultsApp({Key? key}) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
@@ -28,7 +28,7 @@
       ),
       initialRoute: '/',
       onGenerateRoute: (RouteSettings settings) {
-        final parameters = settings.name.substring(1).split('&');
+        final parameters = settings.name!.substring(1).split('&');
 
         final terms = parameters
             .firstWhere((parameter) => parameter.startsWith('filter='),
@@ -46,7 +46,7 @@
             builder: (context) {
               Provider.of<QueryResults>(context, listen: false).fetch(filter);
               // Not allowed to set state of tab controller in this builder.
-              WidgetsBinding.instance.addPostFrameCallback((_) {
+              WidgetsBinding.instance!.addPostFrameCallback((_) {
                 Provider.of<TabController>(context, listen: false).index = tab;
               });
               return const CurrentResultsScaffold();
@@ -63,7 +63,7 @@
 /// TabBar, and to the TabController object created by that
 /// DefaultTabController widget.
 class Providers extends StatelessWidget {
-  const Providers({Key key}) : super(key: key);
+  const Providers({Key? key}) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
@@ -76,7 +76,7 @@
           // ChangeNotifierProvider.value in a Builder is needed to make
           // the TabController available for widgets to observe.
           builder: (context) => ChangeNotifierProvider<TabController>.value(
-            value: DefaultTabController.of(context),
+            value: DefaultTabController.of(context)!,
             child: const CurrentResultsApp(),
           ),
         ),
@@ -86,7 +86,7 @@
 }
 
 class CurrentResultsScaffold extends StatelessWidget {
-  const CurrentResultsScaffold({Key key}) : super(key: key);
+  const CurrentResultsScaffold({Key? key}) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
@@ -103,30 +103,30 @@
                   fontSize: 24.0, color: Color.fromARGB(255, 63, 81, 181))),
           backgroundColor: Colors.white,
           bottom: TabBar(
-            tabs: [
+            tabs: const [
               Tab(text: 'ALL'),
               Tab(text: 'FAILURES'),
               Tab(text: 'FLAKY'),
             ],
-            indicatorColor: Color.fromARGB(255, 63, 81, 181),
-            labelColor: Color.fromARGB(255, 63, 81, 181),
+            indicatorColor: const Color.fromARGB(255, 63, 81, 181),
+            labelColor: const Color.fromARGB(255, 63, 81, 181),
             onTap: (tab) {
               // We cannot compare to the previous value, it is gone.
               pushRoute(context, tab: tab);
             },
           ),
         ),
-        persistentFooterButtons: [
-          const TestSummary(),
-          const ResultsSummary(),
-          const ApiPortalLink(),
-          const JsonLink(),
-          const TextPopup(),
+        persistentFooterButtons: const [
+          TestSummary(),
+          ResultsSummary(),
+          ApiPortalLink(),
+          JsonLink(),
+          TextPopup(),
         ],
         body: Align(
           alignment: Alignment.topCenter,
           child: Container(
-            constraints: BoxConstraints(maxWidth: 1000.0),
+            constraints: const BoxConstraints(maxWidth: 1000.0),
             child: Column(
               crossAxisAlignment: CrossAxisAlignment.start,
               children: [
@@ -154,12 +154,13 @@
   @override
   Widget build(BuildContext context) {
     return TextButton(
-      child: Text('API portal'),
-      onPressed: () => html.window.open(
-          'https://endpointsportal.dart-ci.cloud.goog'
-              '/docs/current-results-qvyo5rktwa-uc.a.run.app/g'
-              '/routes/v1/results/get',
-          '_blank'),
+      child: const Text('API portal'),
+      onPressed: () => url_launcher.launch(
+        'https://endpointsportal.dart-ci.cloud.goog'
+        '/docs/current-results-qvyo5rktwa-uc.a.run.app/g'
+        '/routes/v1/results/get',
+        webOnlyWindowName: '_blank',
+      ),
     );
   }
 }
@@ -172,13 +173,14 @@
     return Consumer<QueryResults>(
       builder: (context, results, child) {
         return TextButton(
-          child: Text('json'),
-          onPressed: () => html.window.open(
-              Uri.https(apiHost, 'v1/results', {
-                'filter': results.filter.terms.join(','),
-                'pageSize': '4000'
-              }).toString(),
-              '_blank'),
+          child: const Text('json'),
+          onPressed: () => url_launcher.launch(
+            Uri.https(apiHost, 'v1/results', {
+              'filter': results.filter.terms.join(','),
+              'pageSize': '4000',
+            }).toString(),
+            webOnlyWindowName: '_blank',
+          ),
         );
       },
     );
@@ -193,29 +195,29 @@
     return Consumer<QueryResults>(
       builder: (context, QueryResults results, child) {
         return TextButton(
-          child: Text('text'),
+          child: const Text('text'),
           onPressed: () => showDialog(
             context: context,
             builder: (BuildContext context) {
               final text = [resultTextHeader]
                   .followedBy(results.names
-                      .expand((name) => results.grouped[name].values)
+                      .expand((name) => results.grouped[name]!.values)
                       .expand((list) => list)
                       .map(resultAsCommaSeparated))
                   .join('\n');
               return AlertDialog(
-                title: Text('Results query as text'),
+                title: const Text('Results query as text'),
                 content: SelectableText(text),
                 actions: <Widget>[
-                  FlatButton(
-                    child: Text('Copy and dismiss'),
+                  TextButton(
+                    child: const Text('Copy and dismiss'),
                     onPressed: () {
-                      clippy.write(text);
+                      Clipboard.setData(ClipboardData(text: text));
                       Navigator.of(context).pop();
                     },
                   ),
-                  FlatButton(
-                    child: Text('Dismiss'),
+                  TextButton(
+                    child: const Text('Dismiss'),
                     onPressed: () => Navigator.of(context).pop(),
                   ),
                 ],
@@ -230,8 +232,8 @@
 
 class NoTransitionPageRoute extends MaterialPageRoute {
   NoTransitionPageRoute({
-    @required WidgetBuilder builder,
-    RouteSettings settings,
+    required WidgetBuilder builder,
+    RouteSettings? settings,
     bool maintainState = true,
   }) : super(
           builder: builder,
@@ -246,16 +248,12 @@
   }
 }
 
-void pushRoute(context, {Iterable<String> terms, int tab}) {
+void pushRoute(context, {Iterable<String>? terms, int? tab}) {
   if (terms == null && tab == null) {
     throw ArgumentError('pushRoute calls must have a named argument');
   }
-  if (tab == null) {
-    tab = Provider.of<TabController>(context, listen: false).index;
-  }
-  if (terms == null) {
-    terms = Provider.of<QueryResults>(context, listen: false).filter.terms;
-  }
+  tab ??= Provider.of<TabController>(context, listen: false).index;
+  terms ??= Provider.of<QueryResults>(context, listen: false).filter.terms;
   final tabItems = [if (tab == 0) 'showAll', if (tab == 2) 'flaky'];
   Navigator.pushNamed(
     context,
diff --git a/current_results_ui/lib/query.dart b/current_results_ui/lib/query.dart
index 31bbd32..fd77802 100644
--- a/current_results_ui/lib/query.dart
+++ b/current_results_ui/lib/query.dart
@@ -19,7 +19,7 @@
 
 class QueryResults extends ChangeNotifier {
   Filter filter = Filter('');
-  StreamSubscription<GetResultsResponse> fetcher;
+  StreamSubscription<GetResultsResponse>? fetcher;
   List<String> names = [];
   Map<String, Counts> counts = {};
   Map<String, Map<ChangeInResult, List<Result>>> grouped = {};
@@ -62,7 +62,7 @@
     final results = response.results;
     fetchedResultsCount += results.length;
     if (fetchedResultsCount >= maxFetchedResults) {
-      fetcher.cancel();
+      fetcher?.cancel();
       fetcher = null;
     }
     for (final result in results) {
diff --git a/current_results_ui/lib/results.dart b/current_results_ui/lib/results.dart
index 26691cd..7114603 100644
--- a/current_results_ui/lib/results.dart
+++ b/current_results_ui/lib/results.dart
@@ -2,16 +2,13 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
-// ignore: avoid_web_libraries_in_flutter
-import 'dart:html' as html;
-
 import 'package:flutter/material.dart';
-import 'package:flutter/rendering.dart';
 import 'package:provider/provider.dart';
+import 'package:url_launcher/url_launcher.dart' as url_launcher;
 
-import 'src/generated/query.pb.dart';
 import 'instructions.dart';
 import 'query.dart';
+import 'src/generated/query.pb.dart';
 
 const Color lightCoral = Color.fromARGB(255, 240, 128, 128);
 const Color gold = Color.fromARGB(255, 255, 215, 0);
@@ -30,8 +27,8 @@
           return Align(child: Instructions());
         }
         bool isFailed(String name) =>
-            queryResults.counts[name].countFailing > 0;
-        bool isFlaky(String name) => queryResults.counts[name].countFlaky > 0;
+            queryResults.counts[name]!.countFailing > 0;
+        bool isFlaky(String name) => queryResults.counts[name]!.countFlaky > 0;
         final filter = [(name) => true, isFailed, isFlaky][tabController.index];
         final filteredNames = queryResults.names.where(filter).toList();
         return ListView.builder(
@@ -39,8 +36,8 @@
           itemCount: filteredNames.length,
           itemBuilder: (BuildContext context, int index) {
             final name = filteredNames[index];
-            final changeGroups = queryResults.grouped[name];
-            final counts = queryResults.counts[name];
+            final changeGroups = queryResults.grouped[name]!;
+            final counts = queryResults.counts[name]!;
             return ExpandableResult(name, changeGroups, counts);
           },
         );
@@ -68,7 +65,7 @@
   CountItem._(this.text, this.color);
 
   factory CountItem(int count, Color color) {
-    var text;
+    String text;
     if (count > 0) {
       text = count.toString();
     } else {
@@ -80,9 +77,9 @@
 }
 
 List<CountItem> countItems(Counts counts) => [
-      CountItem(counts.countPassing, resultColors['pass']),
-      CountItem(counts.countFailing, resultColors['fail']),
-      CountItem(counts.countFlaky, resultColors['flaky'])
+      CountItem(counts.countPassing, resultColors['pass']!),
+      CountItem(counts.countFailing, resultColors['fail']!),
+      CountItem(counts.countFlaky, resultColors['flaky']!)
     ];
 
 class _ExpandableResultState extends State<ExpandableResult> {
@@ -97,7 +94,7 @@
       children: [
         Container(
           height: 28.0,
-          padding: EdgeInsets.only(top: 0.0, left: 8.0),
+          padding: const EdgeInsets.only(top: 0.0, left: 8.0),
           child: Row(
             crossAxisAlignment: CrossAxisAlignment.center,
             children: [
@@ -109,33 +106,36 @@
                 Container(
                   width: 24,
                   alignment: Alignment.center,
-                  margin: EdgeInsets.symmetric(horizontal: 1.0),
+                  margin: const EdgeInsets.symmetric(horizontal: 1.0),
                   decoration: BoxDecoration(
                     color: item.color,
                     shape: BoxShape.circle,
                   ),
-                  child: Text(item.text, style: TextStyle(fontSize: 14.0)),
+                  child:
+                      Text(item.text, style: const TextStyle(fontSize: 14.0)),
                 ),
               Expanded(
                 flex: 1,
                 child: Container(
-                  padding: EdgeInsets.only(left: 4.0),
+                  padding: const EdgeInsets.only(left: 4.0),
                   alignment: Alignment.centerLeft,
                   child: SelectableText(
                     name,
-                    style: TextStyle(fontSize: 16.0),
+                    style: const TextStyle(fontSize: 16.0),
                     maxLines: 1,
                   ),
                 ),
               ),
               IconButton(
-                  icon: Icon(Icons.history),
-                  onPressed: () => html.window.open(
-                      Uri(
-                              path: '/',
-                              fragment: 'showLatestFailures=false&test=$name')
-                          .toString(),
-                      '_blank')),
+                icon: const Icon(Icons.history),
+                onPressed: () => url_launcher.launch(
+                  Uri(
+                          path: '/',
+                          fragment: 'showLatestFailures=false&test=$name')
+                      .toString(),
+                  webOnlyWindowName: '_blank',
+                ),
+              ),
             ],
           ),
         ),
@@ -143,35 +143,35 @@
           for (final change in changeGroups.keys)
             Container(
               alignment: Alignment.topLeft,
-              padding: EdgeInsets.only(left: 48.0),
+              padding: const EdgeInsets.only(left: 48.0),
               child: Column(
                 crossAxisAlignment: CrossAxisAlignment.start,
                 children: [
                   Container(
-                    padding: EdgeInsets.only(top: 12.0),
+                    padding: const EdgeInsets.only(top: 12.0),
                     child: Text(
-                        '$change (${changeGroups[change].length} configurations)',
+                        '$change (${changeGroups[change]!.length} configurations)',
                         style: TextStyle(
                             backgroundColor: resultColors[change.kind],
                             fontSize: 16.0)),
                   ),
-                  for (final result in changeGroups[change])
+                  for (final result in changeGroups[change]!)
                     Row(children: [
                       Text(result.configuration),
                       if (change.kind == 'fail')
                         Padding(
-                            padding: EdgeInsets.only(left: 5),
+                            padding: const EdgeInsets.only(left: 5),
                             child: _link("log",
                                 _openTestLog(result.configuration, name))),
                       Padding(
-                          padding: EdgeInsets.only(left: 5),
+                          padding: const EdgeInsets.only(left: 5),
                           child: _link("source",
                               _openTestSource(result.revision, result.name))),
                     ])
                 ],
               ),
             ),
-        if (expanded) SizedBox(height: 12.0),
+        if (expanded) const SizedBox(height: 12.0),
       ],
     );
   }
@@ -179,23 +179,25 @@
 
 Widget _link(String text, Function onClick) {
   final link = Text(text,
-      style:
-          TextStyle(color: Colors.blue, decoration: TextDecoration.underline));
-  return InkWell(onTap: onClick, child: link);
+      style: const TextStyle(
+          color: Colors.blue, decoration: TextDecoration.underline));
+  return InkWell(onTap: onClick as void Function()?, child: link);
 }
 
 Function _openTestSource(String revision, String name) {
   return () {
-    html.window
-        .open('https://dart-ci.appspot.com/test/$revision/$name', '_blank');
+    url_launcher.launch(
+      'https://dart-ci.appspot.com/test/$revision/$name',
+      webOnlyWindowName: '_blank',
+    );
   };
 }
 
 Function _openTestLog(String configuration, String name) {
   return () {
-    html.window.open(
+    url_launcher.launch(
       'https://dart-ci.appspot.com/log/any/$configuration/latest/$name',
-      '_blank',
+      webOnlyWindowName: '_blank',
     );
   };
 }
@@ -228,7 +230,7 @@
   final String typeText;
   final Counts counts;
 
-  Summary(this.typeText, this.counts);
+  const Summary(this.typeText, this.counts);
 
   @override
   Widget build(BuildContext context) {
@@ -237,12 +239,12 @@
       children: [
         Text(
           typeText,
-          style: TextStyle(fontWeight: FontWeight.bold),
+          style: const TextStyle(fontWeight: FontWeight.bold),
         ),
         Pill(Colors.black26, counts.count, 'total'),
-        Pill(resultColors['fail'], counts.countFailing, 'failing'),
-        Pill(resultColors['flaky'], counts.countFlaky, 'flaky'),
-        SizedBox.fromSize(size: Size.fromWidth(8.0)),
+        Pill(resultColors['fail']!, counts.countFailing, 'failing'),
+        Pill(resultColors['flaky']!, counts.countFlaky, 'flaky'),
+        SizedBox.fromSize(size: const Size.fromWidth(8.0)),
       ],
     );
   }
@@ -253,7 +255,7 @@
   final int count;
   final String tooltip;
 
-  Pill(this.color, this.count, this.tooltip);
+  const Pill(this.color, this.count, this.tooltip);
 
   @override
   Widget build(BuildContext context) {
@@ -263,13 +265,13 @@
         //width: 24,
         height: 24,
         alignment: Alignment.center,
-        margin: EdgeInsets.symmetric(horizontal: 2.0),
-        padding: EdgeInsets.symmetric(horizontal: 8.0),
+        margin: const EdgeInsets.symmetric(horizontal: 2.0),
+        padding: const EdgeInsets.symmetric(horizontal: 8.0),
         decoration: BoxDecoration(
           color: color,
           borderRadius: BorderRadius.circular(14.0),
         ),
-        child: Text(count.toString(), style: TextStyle(fontSize: 14.0)),
+        child: Text(count.toString(), style: const TextStyle(fontSize: 14.0)),
       ),
     );
   }
diff --git a/current_results_ui/lib/src/generated/google/protobuf/empty.pb.dart b/current_results_ui/lib/src/generated/google/protobuf/empty.pb.dart
deleted file mode 100644
index bc2a099..0000000
--- a/current_results_ui/lib/src/generated/google/protobuf/empty.pb.dart
+++ /dev/null
@@ -1,38 +0,0 @@
-///
-//  Generated code. Do not modify.
-//  source: google/protobuf/empty.proto
-//
-// @dart = 2.3
-// ignore_for_file: camel_case_types,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type
-
-import 'dart:core' as $core;
-
-import 'package:protobuf/protobuf.dart' as $pb;
-
-class Empty extends $pb.GeneratedMessage {
-  static final $pb.BuilderInfo _i = $pb.BuilderInfo('Empty',
-      package: const $pb.PackageName('google.protobuf'),
-      createEmptyInstance: create)
-    ..hasRequiredFields = false;
-
-  Empty._() : super();
-  factory Empty() => create();
-  factory Empty.fromBuffer($core.List<$core.int> i,
-          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
-      create()..mergeFromBuffer(i, r);
-  factory Empty.fromJson($core.String i,
-          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
-      create()..mergeFromJson(i, r);
-  Empty clone() => Empty()..mergeFromMessage(this);
-  Empty copyWith(void Function(Empty) updates) =>
-      super.copyWith((message) => updates(message as Empty));
-  $pb.BuilderInfo get info_ => _i;
-  @$core.pragma('dart2js:noInline')
-  static Empty create() => Empty._();
-  Empty createEmptyInstance() => create();
-  static $pb.PbList<Empty> createRepeated() => $pb.PbList<Empty>();
-  @$core.pragma('dart2js:noInline')
-  static Empty getDefault() =>
-      _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Empty>(create);
-  static Empty _defaultInstance;
-}
diff --git a/current_results_ui/lib/src/generated/google/protobuf/empty.pbenum.dart b/current_results_ui/lib/src/generated/google/protobuf/empty.pbenum.dart
deleted file mode 100644
index 0fb3ebb..0000000
--- a/current_results_ui/lib/src/generated/google/protobuf/empty.pbenum.dart
+++ /dev/null
@@ -1,6 +0,0 @@
-///
-//  Generated code. Do not modify.
-//  source: google/protobuf/empty.proto
-//
-// @dart = 2.3
-// ignore_for_file: camel_case_types,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type
diff --git a/current_results_ui/lib/src/generated/google/protobuf/empty.pbjson.dart b/current_results_ui/lib/src/generated/google/protobuf/empty.pbjson.dart
deleted file mode 100644
index 588c6a0..0000000
--- a/current_results_ui/lib/src/generated/google/protobuf/empty.pbjson.dart
+++ /dev/null
@@ -1,10 +0,0 @@
-///
-//  Generated code. Do not modify.
-//  source: google/protobuf/empty.proto
-//
-// @dart = 2.3
-// ignore_for_file: camel_case_types,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type
-
-const Empty$json = const {
-  '1': 'Empty',
-};
diff --git a/current_results_ui/lib/src/generated/google/protobuf/empty.pbserver.dart b/current_results_ui/lib/src/generated/google/protobuf/empty.pbserver.dart
deleted file mode 100644
index 7a64fc1..0000000
--- a/current_results_ui/lib/src/generated/google/protobuf/empty.pbserver.dart
+++ /dev/null
@@ -1,8 +0,0 @@
-///
-//  Generated code. Do not modify.
-//  source: google/protobuf/empty.proto
-//
-// @dart = 2.3
-// ignore_for_file: camel_case_types,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type
-
-export 'empty.pb.dart';
diff --git a/current_results_ui/lib/src/generated/query.pb.dart b/current_results_ui/lib/src/generated/query.pb.dart
index 64eb747..244731c 100644
--- a/current_results_ui/lib/src/generated/query.pb.dart
+++ b/current_results_ui/lib/src/generated/query.pb.dart
@@ -177,8 +177,11 @@
             : 'results',
         $pb.PbFieldType.PM,
         subBuilder: Result.create)
-    ..aOS(2,
-        const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'nextPageToken')
+    ..aOS(
+        2,
+        const $core.bool.fromEnvironment('protobuf.omit_field_names')
+            ? ''
+            : 'nextPageToken')
     ..hasRequiredFields = false;
 
   GetResultsResponse._() : super();
@@ -263,12 +266,32 @@
         const $core.bool.fromEnvironment('protobuf.omit_field_names')
             ? ''
             : 'result')
-    ..aOS(4,
-        const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'expected')
-    ..aOB(5, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'flaky')
-    ..a<$core.int>(6, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'timeMs', $pb.PbFieldType.O3)
-    ..pPS(7, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'experiments')
-    ..aOS(8, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'revision')
+    ..aOS(
+        4,
+        const $core.bool.fromEnvironment('protobuf.omit_field_names')
+            ? ''
+            : 'expected')
+    ..aOB(
+        5,
+        const $core.bool.fromEnvironment('protobuf.omit_field_names')
+            ? ''
+            : 'flaky')
+    ..a<$core.int>(
+        6,
+        const $core.bool.fromEnvironment('protobuf.omit_field_names')
+            ? ''
+            : 'timeMs',
+        $pb.PbFieldType.O3)
+    ..pPS(
+        7,
+        const $core.bool.fromEnvironment('protobuf.omit_field_names')
+            ? ''
+            : 'experiments')
+    ..aOS(
+        8,
+        const $core.bool.fromEnvironment('protobuf.omit_field_names')
+            ? ''
+            : 'revision')
     ..hasRequiredFields = false;
 
   Result._() : super();
diff --git a/current_results_ui/lib/src/generated/query.pbjson.dart b/current_results_ui/lib/src/generated/query.pbjson.dart
index a19a7c9..931533e 100644
--- a/current_results_ui/lib/src/generated/query.pbjson.dart
+++ b/current_results_ui/lib/src/generated/query.pbjson.dart
@@ -3,7 +3,7 @@
 //  source: query.proto
 //
 // @dart = 2.12
-// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields,deprecated_member_use_from_same_package
+// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields,deprecated_member_use_from_same_package,constant_identifier_names
 
 import 'dart:core' as $core;
 import 'dart:convert' as $convert;
diff --git a/current_results_ui/pubspec.lock b/current_results_ui/pubspec.lock
index 433ec1a..dfada90 100644
--- a/current_results_ui/pubspec.lock
+++ b/current_results_ui/pubspec.lock
@@ -14,7 +14,7 @@
       name: async
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "2.5.0"
+    version: "2.8.2"
   boolean_selector:
     dependency: transitive
     description:
@@ -28,21 +28,14 @@
       name: characters
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.1.0"
+    version: "1.2.0"
   charcode:
     dependency: transitive
     description:
       name: charcode
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.2.0"
-  clippy:
-    dependency: "direct main"
-    description:
-      name: clippy
-      url: "https://pub.dartlang.org"
-    source: hosted
-    version: "1.0.0"
+    version: "1.3.1"
   clock:
     dependency: transitive
     description:
@@ -83,11 +76,23 @@
     description: flutter
     source: sdk
     version: "0.0.0"
+  flutter_lints:
+    dependency: "direct main"
+    description:
+      name: flutter_lints
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.4"
   flutter_test:
     dependency: "direct dev"
     description: flutter
     source: sdk
     version: "0.0.0"
+  flutter_web_plugins:
+    dependency: transitive
+    description: flutter
+    source: sdk
+    version: "0.0.0"
   googleapis_auth:
     dependency: transitive
     description:
@@ -101,14 +106,14 @@
       name: grpc
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "3.0.0"
+    version: "3.0.2"
   http:
     dependency: "direct main"
     description:
       name: http
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.13.3"
+    version: "0.13.4"
   http2:
     dependency: transitive
     description:
@@ -123,20 +128,41 @@
       url: "https://pub.dartlang.org"
     source: hosted
     version: "4.0.0"
+  js:
+    dependency: transitive
+    description:
+      name: js
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.6.4"
+  lints:
+    dependency: transitive
+    description:
+      name: lints
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.1"
   matcher:
     dependency: transitive
     description:
       name: matcher
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.12.10"
+    version: "0.12.11"
+  material_color_utilities:
+    dependency: transitive
+    description:
+      name: material_color_utilities
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.1.3"
   meta:
     dependency: transitive
     description:
       name: meta
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.3.0"
+    version: "1.7.0"
   nested:
     dependency: transitive
     description:
@@ -150,28 +176,28 @@
       name: path
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.8.0"
-  pedantic:
+    version: "1.8.1"
+  plugin_platform_interface:
     dependency: transitive
     description:
-      name: pedantic
+      name: plugin_platform_interface
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.11.0"
+    version: "2.1.2"
   protobuf:
     dependency: "direct main"
     description:
       name: protobuf
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "2.0.0"
+    version: "2.0.1"
   provider:
     dependency: "direct main"
     description:
       name: provider
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "4.3.3"
+    version: "6.0.2"
   sky_engine:
     dependency: transitive
     description: flutter
@@ -218,7 +244,7 @@
       name: test_api
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.3.0"
+    version: "0.4.9"
   typed_data:
     dependency: transitive
     description:
@@ -226,13 +252,69 @@
       url: "https://pub.dartlang.org"
     source: hosted
     version: "1.3.0"
+  url_launcher:
+    dependency: "direct main"
+    description:
+      name: url_launcher
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "6.0.20"
+  url_launcher_android:
+    dependency: transitive
+    description:
+      name: url_launcher_android
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "6.0.14"
+  url_launcher_ios:
+    dependency: transitive
+    description:
+      name: url_launcher_ios
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "6.0.14"
+  url_launcher_linux:
+    dependency: transitive
+    description:
+      name: url_launcher_linux
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.0.0"
+  url_launcher_macos:
+    dependency: transitive
+    description:
+      name: url_launcher_macos
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.0.0"
+  url_launcher_platform_interface:
+    dependency: transitive
+    description:
+      name: url_launcher_platform_interface
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.5"
+  url_launcher_web:
+    dependency: transitive
+    description:
+      name: url_launcher_web
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.7"
+  url_launcher_windows:
+    dependency: transitive
+    description:
+      name: url_launcher_windows
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.0.0"
   vector_math:
     dependency: transitive
     description:
       name: vector_math
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "2.1.0"
+    version: "2.1.1"
 sdks:
-  dart: ">=2.12.0 <3.0.0"
-  flutter: ">=1.16.0"
+  dart: ">=2.16.0-100.0.dev <3.0.0"
+  flutter: ">=2.5.0"
diff --git a/current_results_ui/pubspec.yaml b/current_results_ui/pubspec.yaml
index f3c88ab..ca19729 100644
--- a/current_results_ui/pubspec.yaml
+++ b/current_results_ui/pubspec.yaml
@@ -5,17 +5,17 @@
 version: 0.1.0
 
 environment:
-  sdk: ">=2.9.0 <3.0.0"
+  sdk: '>=2.12.0 <3.0.0'
 
 dependencies:
-  clippy: ^1.0.0
   flutter:
     sdk: flutter
-
-  http: ^0.13.1
-  grpc: ^3.0.0
-  protobuf: ^2.0.0
-  provider: ^4.3.2
+  flutter_lints: ^1.0.4
+  http: ^0.13.4
+  grpc: ^3.0.2
+  protobuf: ^2.0.1
+  provider: ^6.0.2
+  url_launcher: ^6.0.20
 
 dev_dependencies:
   flutter_test: