[flutter_releases] Flutter beta 2.13.0-0.4.pre Framework Cherrypicks (#103101)

* Add the new hash

* [flutter.js] Wait for reg.update, then activate sw (if not active yet). (#101464)

* Avoid scheduling a forced frame when there is no child to the renderView (#102556)

* Migrate AppBar to Material 3 (#101884)

* Keeping the super parameters

* Fixing semi-colon issue

Co-authored-by: David Iglesias <ditman@gmail.com>
Co-authored-by: Dan Field <dnfield@google.com>
Co-authored-by: Darren Austin <darrenaustin@google.com>
diff --git a/bin/internal/engine.version b/bin/internal/engine.version
index 2d92142..9aa00d6 100644
--- a/bin/internal/engine.version
+++ b/bin/internal/engine.version
@@ -1 +1 @@
-3096903c8923608d3c1ccf8058a29c31a2bfbc53
+c5caf749fe75db788dba8def502c46c094435605
diff --git a/dev/bots/service_worker_test.dart b/dev/bots/service_worker_test.dart
index 0b30533..10432c7 100644
--- a/dev/bots/service_worker_test.dart
+++ b/dev/bots/service_worker_test.dart
@@ -17,13 +17,22 @@
 final String _flutterRoot = path.dirname(path.dirname(path.dirname(path.fromUri(Platform.script))));
 final String _flutter = path.join(_flutterRoot, 'bin', 'flutter$_bat');
 final String _testAppDirectory = path.join(_flutterRoot, 'dev', 'integration_tests', 'web');
+final String _testAppWebDirectory = path.join(_testAppDirectory, 'web');
 final String _appBuildDirectory = path.join(_testAppDirectory, 'build', 'web');
 final String _target = path.join('lib', 'service_worker_test.dart');
 final String _targetPath = path.join(_testAppDirectory, _target);
 
+enum ServiceWorkerTestType {
+  withoutFlutterJs,
+  withFlutterJs,
+  withFlutterJsShort,
+}
+
 // Run a web service worker test as a standalone Dart program.
 Future<void> main() async {
-  await runWebServiceWorkerTest(headless: false);
+  await runWebServiceWorkerTest(headless: false, testType: ServiceWorkerTestType.withFlutterJs);
+  await runWebServiceWorkerTest(headless: false, testType: ServiceWorkerTestType.withoutFlutterJs);
+  await runWebServiceWorkerTest(headless: false, testType: ServiceWorkerTestType.withFlutterJsShort);
 }
 
 Future<void> _setAppVersion(int version) async {
@@ -36,7 +45,23 @@
   );
 }
 
-Future<void> _rebuildApp({ required int version }) async {
+String _testTypeToIndexFile(ServiceWorkerTestType type) {
+  late String indexFile;
+  switch (type) {
+    case ServiceWorkerTestType.withFlutterJs:
+      indexFile = 'index_with_flutterjs.html';
+      break;
+    case ServiceWorkerTestType.withoutFlutterJs:
+      indexFile = 'index_without_flutterjs.html';
+      break;
+    case ServiceWorkerTestType.withFlutterJsShort:
+      indexFile = 'index_with_flutterjs_short.html';
+      break;
+  }
+  return indexFile;
+}
+
+Future<void> _rebuildApp({ required int version, required ServiceWorkerTestType testType }) async {
   await _setAppVersion(version);
   await runCommand(
     _flutter,
@@ -44,6 +69,14 @@
     workingDirectory: _testAppDirectory,
   );
   await runCommand(
+    'cp',
+    <String>[
+      _testTypeToIndexFile(testType),
+      'index.html',
+    ],
+    workingDirectory: _testAppWebDirectory,
+  );
+  await runCommand(
     _flutter,
     <String>['build', 'web', '--profile', '-t', _target],
     workingDirectory: _testAppDirectory,
@@ -69,9 +102,8 @@
 
 Future<void> runWebServiceWorkerTest({
   required bool headless,
+  required ServiceWorkerTestType testType,
 }) async {
-  await _rebuildApp(version: 1);
-
   final Map<String, int> requestedPathCounts = <String, int>{};
   void expectRequestCounts(Map<String, int> expectedCounts) {
     expect(requestedPathCounts, expectedCounts);
@@ -124,10 +156,64 @@
     );
   }
 
+  // Preserve old index.html as index_og.html so we can restore it later for other tests
+  await runCommand(
+    'mv',
+    <String>[
+      'index.html',
+      'index_og.html',
+    ],
+    workingDirectory: _testAppWebDirectory,
+  );
+
+  final bool shouldExpectFlutterJs = testType != ServiceWorkerTestType.withoutFlutterJs;
+
+  print('BEGIN runWebServiceWorkerTest(headless: $headless, testType: $testType)\n');
+
   try {
+    /////
+    // Attempt to load a different version of the service worker!
+    /////
+    await _rebuildApp(version: 1, testType: testType);
+
+    print('Call update() on the current web worker');
+    await startAppServer(cacheControl: 'max-age=0');
+    await waitForAppToLoad(<String, int> {
+      if (shouldExpectFlutterJs)
+        'flutter.js': 1,
+      'CLOSE': 1,
+    });
+    expect(reportedVersion, '1');
+    reportedVersion = null;
+
+    await server!.chrome.reloadPage(ignoreCache: true);
+    await waitForAppToLoad(<String, int> {
+      if (shouldExpectFlutterJs)
+        'flutter.js': 2,
+      'CLOSE': 2,
+    });
+    expect(reportedVersion, '1');
+    reportedVersion = null;
+
+    await _rebuildApp(version: 2, testType: testType);
+
+    await server!.chrome.reloadPage(ignoreCache: true);
+    await waitForAppToLoad(<String, int>{
+      if (shouldExpectFlutterJs)
+        'flutter.js': 3,
+      'CLOSE': 3,
+    });
+    expect(reportedVersion, '2');
+
+    reportedVersion = null;
+    requestedPathCounts.clear();
+    await server!.stop();
+
     //////////////////////////////////////////////////////
     // Caching server
     //////////////////////////////////////////////////////
+    await _rebuildApp(version: 1, testType: testType);
+
     print('With cache: test first page load');
     await startAppServer(cacheControl: 'max-age=3600');
     await waitForAppToLoad(<String, int>{
@@ -140,6 +226,8 @@
       // once by the initial page load, and once by the service worker.
       // Other resources are loaded once only by the service worker.
       'index.html': 2,
+      if (shouldExpectFlutterJs)
+        'flutter.js': 1,
       'main.dart.js': 1,
       'flutter_service_worker.js': 1,
       'assets/FontManifest.json': 1,
@@ -171,7 +259,7 @@
     reportedVersion = null;
 
     print('With cache: test page reload after rebuild');
-    await _rebuildApp(version: 2);
+    await _rebuildApp(version: 2, testType: testType);
 
     // Since we're caching, we need to ignore cache when reloading the page.
     await server!.chrome.reloadPage(ignoreCache: true);
@@ -181,6 +269,8 @@
     });
     expectRequestCounts(<String, int>{
       'index.html': 2,
+      if (shouldExpectFlutterJs)
+        'flutter.js': 1,
       'flutter_service_worker.js': 2,
       'main.dart.js': 1,
       'assets/NOTICES': 1,
@@ -200,7 +290,7 @@
     // Non-caching server
     //////////////////////////////////////////////////////
     print('No cache: test first page load');
-    await _rebuildApp(version: 3);
+    await _rebuildApp(version: 3, testType: testType);
     await startAppServer(cacheControl: 'max-age=0');
     await waitForAppToLoad(<String, int>{
       'CLOSE': 1,
@@ -209,6 +299,8 @@
 
     expectRequestCounts(<String, int>{
       'index.html': 2,
+      if (shouldExpectFlutterJs)
+        'flutter.js': 1,
       // We still download some resources multiple times if the server is non-caching.
       'main.dart.js': 2,
       'assets/FontManifest.json': 2,
@@ -231,10 +323,14 @@
     await server!.chrome.reloadPage();
     await waitForAppToLoad(<String, int>{
       'CLOSE': 1,
+      if (shouldExpectFlutterJs)
+        'flutter.js': 1,
       'flutter_service_worker.js': 1,
     });
 
     expectRequestCounts(<String, int>{
+      if (shouldExpectFlutterJs)
+        'flutter.js': 1,
       'flutter_service_worker.js': 1,
       'CLOSE': 1,
       if (!headless)
@@ -244,7 +340,7 @@
     reportedVersion = null;
 
     print('No cache: test page reload after rebuild');
-    await _rebuildApp(version: 4);
+    await _rebuildApp(version: 4, testType: testType);
 
     // TODO(yjbanov): when running Chrome with DevTools protocol, for some
     // reason a hard refresh is still required. This works without a hard
@@ -258,6 +354,8 @@
     });
     expectRequestCounts(<String, int>{
       'index.html': 2,
+      if (shouldExpectFlutterJs)
+        'flutter.js': 1,
       'flutter_service_worker.js': 2,
       'main.dart.js': 2,
       'assets/NOTICES': 1,
@@ -274,7 +372,17 @@
     expect(reportedVersion, '4');
     reportedVersion = null;
   } finally {
+    await runCommand(
+      'mv',
+      <String>[
+        'index_og.html',
+        'index.html',
+      ],
+      workingDirectory: _testAppWebDirectory,
+    );
     await _setAppVersion(1);
     await server?.stop();
   }
+
+  print('END runWebServiceWorkerTest(headless: $headless, testType: $testType)\n');
 }
diff --git a/dev/bots/test.dart b/dev/bots/test.dart
index f58b9eb..c2efd2d 100644
--- a/dev/bots/test.dart
+++ b/dev/bots/test.dart
@@ -1070,7 +1070,9 @@
     () => _runGalleryE2eWebTest('profile', canvasKit: true),
     () => _runGalleryE2eWebTest('release'),
     () => _runGalleryE2eWebTest('release', canvasKit: true),
-    () => runWebServiceWorkerTest(headless: true),
+    () => runWebServiceWorkerTest(headless: true, testType: ServiceWorkerTestType.withoutFlutterJs),
+    () => runWebServiceWorkerTest(headless: true, testType: ServiceWorkerTestType.withFlutterJs),
+    () => runWebServiceWorkerTest(headless: true, testType: ServiceWorkerTestType.withFlutterJsShort),
     () => _runWebStackTraceTest('profile', 'lib/stack_trace.dart'),
     () => _runWebStackTraceTest('release', 'lib/stack_trace.dart'),
     () => _runWebStackTraceTest('profile', 'lib/framework_stack_trace.dart'),
diff --git a/dev/integration_tests/web/web/index_with_flutterjs.html b/dev/integration_tests/web/web/index_with_flutterjs.html
new file mode 100644
index 0000000..8334b5b
--- /dev/null
+++ b/dev/integration_tests/web/web/index_with_flutterjs.html
@@ -0,0 +1,39 @@
+<!DOCTYPE HTML>
+<!-- Copyright 2014 The Flutter Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file. -->
+<html>
+<head>
+  <meta charset="UTF-8">
+  <meta content="IE=Edge" http-equiv="X-UA-Compatible">
+
+  <title>Web Test</title>
+  <!-- iOS meta tags & icons -->
+  <meta name="apple-mobile-web-app-capable" content="yes">
+  <meta name="apple-mobile-web-app-status-bar-style" content="black">
+  <meta name="apple-mobile-web-app-title" content="Web Test">
+  <link rel="manifest" href="manifest.json">
+  <script>
+    // The value below is injected by flutter build, do not touch.
+    var serviceWorkerVersion = null;
+  </script>
+  <!-- This script adds the flutter initialization JS code -->
+  <script src="flutter.js" defer></script>
+</head>
+<body>
+  <script>
+    window.addEventListener('load', function(ev) {
+      // Download main.dart.js
+      _flutter.loader.loadEntrypoint({
+        serviceWorker: {
+          serviceWorkerVersion: serviceWorkerVersion,
+        }
+      }).then(function(engineInitializer) {
+        return engineInitializer.initializeEngine();
+      }).then(function(appRunner) {
+        return appRunner.runApp();
+      });
+    });
+  </script>
+</body>
+</html>
diff --git a/dev/integration_tests/web/web/index_with_flutterjs_short.html b/dev/integration_tests/web/web/index_with_flutterjs_short.html
new file mode 100644
index 0000000..21a494f
--- /dev/null
+++ b/dev/integration_tests/web/web/index_with_flutterjs_short.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<!-- Copyright 2014 The Flutter Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file. -->
+<html>
+<head>
+  <meta charset="UTF-8">
+  <meta content="IE=Edge" http-equiv="X-UA-Compatible">
+
+  <title>Web Test</title>
+  <!-- iOS meta tags & icons -->
+  <meta name="apple-mobile-web-app-capable" content="yes">
+  <meta name="apple-mobile-web-app-status-bar-style" content="black">
+  <meta name="apple-mobile-web-app-title" content="Web Test">
+  <link rel="manifest" href="manifest.json">
+  <script>
+    // The value below is injected by flutter build, do not touch.
+    var serviceWorkerVersion = null;
+  </script>
+  <!-- This script adds the flutter initialization JS code -->
+  <script src="flutter.js" defer></script>
+</head>
+<body>
+  <script>
+    window.addEventListener('load', function(ev) {
+      // Download main.dart.js
+      _flutter.loader.loadEntrypoint({
+        serviceWorker: {
+          serviceWorkerVersion: serviceWorkerVersion,
+        }
+      }).then(function(engineInitializer) {
+        return engineInitializer.autoStart();
+      });
+    });
+  </script>
+</body>
+</html>
diff --git a/dev/integration_tests/web/web/index_without_flutterjs.html b/dev/integration_tests/web/web/index_without_flutterjs.html
new file mode 100644
index 0000000..05d4dee
--- /dev/null
+++ b/dev/integration_tests/web/web/index_without_flutterjs.html
@@ -0,0 +1,86 @@
+<!DOCTYPE HTML>
+<!-- Copyright 2014 The Flutter Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file. -->
+<html>
+<head>
+  <meta charset="UTF-8">
+  <meta content="IE=Edge" http-equiv="X-UA-Compatible">
+
+  <title>Web Test</title>
+  <!-- iOS meta tags & icons -->
+  <meta name="apple-mobile-web-app-capable" content="yes">
+  <meta name="apple-mobile-web-app-status-bar-style" content="black">
+  <meta name="apple-mobile-web-app-title" content="Web Test">
+  <link rel="manifest" href="manifest.json">
+</head>
+<body>
+  <!-- This script installs service_worker.js to provide PWA functionality to
+  application. For more information, see:
+  https://developers.google.com/web/fundamentals/primers/service-workers -->
+  <script>
+    var serviceWorkerVersion = null;
+    var scriptLoaded = false;
+    function loadMainDartJs() {
+      if (scriptLoaded) {
+        return;
+      }
+      scriptLoaded = true;
+      var scriptTag = document.createElement('script');
+      scriptTag.src = 'main.dart.js';
+      scriptTag.type = 'application/javascript';
+      document.body.append(scriptTag);
+    }
+
+    if ('serviceWorker' in navigator) {
+      // Service workers are supported. Use them.
+      window.addEventListener('load', function () {
+        // Wait for registration to finish before dropping the <script> tag.
+        // Otherwise, the browser will load the script multiple times,
+        // potentially different versions.
+        var serviceWorkerUrl = 'flutter_service_worker.js?v=' + serviceWorkerVersion;
+        navigator.serviceWorker.register(serviceWorkerUrl)
+          .then((reg) => {
+            function waitForActivation(serviceWorker) {
+              serviceWorker.addEventListener('statechange', () => {
+                if (serviceWorker.state == 'activated') {
+                  console.log('Installed new service worker.');
+                  loadMainDartJs();
+                }
+              });
+            }
+            if (!reg.active && (reg.installing || reg.waiting)) {
+              // No active web worker and we have installed or are installing
+              // one for the first time. Simply wait for it to activate.
+              waitForActivation(reg.installing ?? reg.waiting);
+            } else if (!reg.active.scriptURL.endsWith(serviceWorkerVersion)) {
+              // When the app updates the serviceWorkerVersion changes, so we
+              // need to ask the service worker to update.
+              console.log('New service worker available.');
+              reg.update();
+              waitForActivation(reg.installing);
+            } else {
+              // Existing service worker is still good.
+              console.log('Loading app from service worker.');
+              loadMainDartJs();
+            }
+          });
+
+        // If service worker doesn't succeed in a reasonable amount of time,
+        // fallback to plaint <script> tag.
+        setTimeout(() => {
+          if (!scriptLoaded) {
+            console.warn(
+              'Failed to load app from service worker. Falling back to plain <script> tag.',
+            );
+            loadMainDartJs();
+          }
+        }, 4000);
+      });
+    } else {
+      // Service workers not supported. Just drop the <script> tag.
+      loadMainDartJs();
+    }
+  </script>
+</body>
+</html>
diff --git a/dev/tools/gen_defaults/bin/gen_defaults.dart b/dev/tools/gen_defaults/bin/gen_defaults.dart
index 0694020..d2b4fc5 100644
--- a/dev/tools/gen_defaults/bin/gen_defaults.dart
+++ b/dev/tools/gen_defaults/bin/gen_defaults.dart
@@ -17,6 +17,7 @@
 import 'dart:convert';
 import 'dart:io';
 
+import 'package:gen_defaults/app_bar_template.dart';
 import 'package:gen_defaults/button_template.dart';
 import 'package:gen_defaults/card_template.dart';
 import 'package:gen_defaults/dialog_template.dart';
@@ -78,6 +79,7 @@
   tokens['colorsLight'] = _readTokenFile('color_light.json');
   tokens['colorsDark'] = _readTokenFile('color_dark.json');
 
+  AppBarTemplate('$materialLib/app_bar.dart', tokens).updateFile();
   ButtonTemplate('md.comp.elevated-button', '$materialLib/elevated_button.dart', tokens).updateFile();
   ButtonTemplate('md.comp.outlined-button', '$materialLib/outlined_button.dart', tokens).updateFile();
   ButtonTemplate('md.comp.text-button', '$materialLib/text_button.dart', tokens).updateFile();
diff --git a/dev/tools/gen_defaults/lib/app_bar_template.dart b/dev/tools/gen_defaults/lib/app_bar_template.dart
new file mode 100644
index 0000000..06b2ddf
--- /dev/null
+++ b/dev/tools/gen_defaults/lib/app_bar_template.dart
@@ -0,0 +1,58 @@
+// Copyright 2014 The Flutter 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 'template.dart';
+
+class AppBarTemplate extends TokenTemplate {
+  const AppBarTemplate(super.fileName, super.tokens)
+    : super(
+      colorSchemePrefix: '_colors.',
+      textThemePrefix: '_textTheme.',
+    );
+
+  @override
+  String generate() => '''
+// Generated version ${tokens["version"]}
+class _TokenDefaultsM3 extends AppBarTheme {
+  _TokenDefaultsM3(this.context)
+    : super(
+      elevation: ${elevation('md.comp.top-app-bar.small.container')},
+      scrolledUnderElevation: ${elevation('md.comp.top-app-bar.small.on-scroll.container')},
+      titleSpacing: NavigationToolbar.kMiddleSpacing,
+      toolbarHeight: ${tokens['md.comp.top-app-bar.small.container.height']},
+    );
+
+  final BuildContext context;
+  late final ThemeData _theme = Theme.of(context);
+  late final ColorScheme _colors = _theme.colorScheme;
+  late final TextTheme _textTheme = _theme.textTheme;
+
+  @override
+  Color? get backgroundColor => ${componentColor('md.comp.top-app-bar.small.container')};
+
+  @override
+  Color? get foregroundColor => ${color('md.comp.top-app-bar.small.headline.color')};
+
+  @override
+  Color? get surfaceTintColor => ${componentColor('md.comp.top-app-bar.small.container.surface-tint-layer')};
+
+  @override
+  IconThemeData? get iconTheme => IconThemeData(
+    color: ${componentColor('md.comp.top-app-bar.small.leading-icon')},
+    size: ${tokens['md.comp.top-app-bar.small.leading-icon.size']},
+  );
+
+  @override
+  IconThemeData? get actionsIconTheme => IconThemeData(
+    color: ${componentColor('md.comp.top-app-bar.small.trailing-icon')},
+    size: ${tokens['md.comp.top-app-bar.small.trailing-icon.size']},
+  );
+
+  @override
+  TextStyle? get toolbarTextStyle => _textTheme.bodyText2;
+
+  @override
+  TextStyle? get titleTextStyle => ${textStyle('md.comp.top-app-bar.small.headline')};
+}''';
+}
diff --git a/packages/flutter/lib/src/material/app_bar.dart b/packages/flutter/lib/src/material/app_bar.dart
index 086095c..28303fe 100644
--- a/packages/flutter/lib/src/material/app_bar.dart
+++ b/packages/flutter/lib/src/material/app_bar.dart
@@ -161,7 +161,9 @@
     this.flexibleSpace,
     this.bottom,
     this.elevation,
+    this.scrolledUnderElevation,
     this.shadowColor,
+    this.surfaceTintColor,
     this.shape,
     this.backgroundColor,
     this.foregroundColor,
@@ -373,7 +375,12 @@
   /// {@template flutter.material.appbar.elevation}
   /// The z-coordinate at which to place this app bar relative to its parent.
   ///
-  /// This property controls the size of the shadow below the app bar.
+  /// This property controls the size of the shadow below the app bar if
+  /// [shadowColor] is not null.
+  ///
+  /// If [surfaceTintColor] is not null then it will apply a surface tint overlay
+  /// to the background color (see [Material.surfaceTintColor] for more
+  /// detail).
   ///
   /// The value must be non-negative.
   ///
@@ -384,11 +391,37 @@
   ///
   /// See also:
   ///
+  ///  * [scrolledUnderElevation], which will be used when the app bar has
+  ///    something scrolled underneath it.
   ///  * [shadowColor], which is the color of the shadow below the app bar.
+  ///  * [surfaceTintColor], which determines the elevation overlay that will
+  ///    be applied to the background of the app bar.
   ///  * [shape], which defines the shape of the app bar's [Material] and its
   ///    shadow.
   final double? elevation;
 
+  /// {@template flutter.material.appbar.scrolledUnderElevation}
+  /// The elevation that will be used if this app bar has something
+  /// scrolled underneath it.
+  ///
+  /// If non-null then it [AppBarTheme.scrolledUnderElevation] of
+  /// [ThemeData.appBarTheme] will be used. If that is also null then [elevation]
+  /// will be used.
+  ///
+  /// The value must be non-negative.
+  ///
+  /// {@endtemplate}
+  ///
+  /// See also:
+  ///  * [elevation], which will be used if there is no content scrolled under
+  ///    the app bar.
+  ///  * [shadowColor], which is the color of the shadow below the app bar.
+  ///  * [surfaceTintColor], which determines the elevation overlay that will
+  ///    be applied to the background of the app bar.
+  ///  * [shape], which defines the shape of the app bar's [Material] and its
+  ///    shadow.
+  final double? scrolledUnderElevation;
+
   /// {@template flutter.material.appbar.shadowColor}
   /// The color of the shadow below the app bar.
   ///
@@ -403,6 +436,17 @@
   ///  * [shape], which defines the shape of the app bar and its shadow.
   final Color? shadowColor;
 
+  /// {@template flutter.material.appbar.surfaceTintColor}
+  /// The color of the surface tint overlay applied to the app bar's
+  /// background color to indicate elevation.
+  ///
+  /// If null no overlay will be applied.
+  /// {@endtemplate}
+  ///
+  /// See also:
+  ///   * [Material.surfaceTintColor], which described this feature in more detail.
+  final Color? surfaceTintColor;
+
   /// {@template flutter.material.appbar.shape}
   /// The shape of the app bar's [Material] as well as its shadow.
   ///
@@ -710,23 +754,24 @@
   ///  * [SystemChrome.setSystemUIOverlayStyle]
   final SystemUiOverlayStyle? systemOverlayStyle;
 
-
   bool _getEffectiveCenterTitle(ThemeData theme) {
-    if (centerTitle != null)
-      return centerTitle!;
-    if (theme.appBarTheme.centerTitle != null)
-      return theme.appBarTheme.centerTitle!;
-    assert(theme.platform != null);
-    switch (theme.platform) {
-      case TargetPlatform.android:
-      case TargetPlatform.fuchsia:
-      case TargetPlatform.linux:
-      case TargetPlatform.windows:
-        return false;
-      case TargetPlatform.iOS:
-      case TargetPlatform.macOS:
-        return actions == null || actions!.length < 2;
+    bool platformCenter() {
+      assert(theme.platform != null);
+      switch (theme.platform) {
+        case TargetPlatform.android:
+        case TargetPlatform.fuchsia:
+        case TargetPlatform.linux:
+        case TargetPlatform.windows:
+          return false;
+        case TargetPlatform.iOS:
+        case TargetPlatform.macOS:
+          return actions == null || actions!.length < 2;
+      }
     }
+
+    return centerTitle
+      ?? theme.appBarTheme.centerTitle
+      ?? platformCenter();
   }
 
   @override
@@ -734,9 +779,6 @@
 }
 
 class _AppBarState extends State<AppBar> {
-  static const double _defaultElevation = 4.0;
-  static const Color _defaultShadowColor = Color(0xFF000000);
-
   ScrollNotificationObserverState? _scrollNotificationObserver;
   bool _scrolledUnder = false;
 
@@ -796,8 +838,8 @@
     assert(!widget.primary || debugCheckHasMediaQuery(context));
     assert(debugCheckHasMaterialLocalizations(context));
     final ThemeData theme = Theme.of(context);
-    final ColorScheme colorScheme = theme.colorScheme;
     final AppBarTheme appBarTheme = AppBarTheme.of(context);
+    final AppBarTheme defaults = theme.useMaterial3 ? _TokenDefaultsM3(context) : _DefaultsM2(context);
     final ScaffoldState? scaffold = Scaffold.maybeOf(context);
     final ModalRoute<dynamic>? parentRoute = ModalRoute.of(context);
 
@@ -822,12 +864,23 @@
           states,
           widget.backgroundColor,
           appBarTheme.backgroundColor,
-          colorScheme.brightness == Brightness.dark ? colorScheme.surface : colorScheme.primary,
+          defaults.backgroundColor!,
         );
 
     final Color foregroundColor = widget.foregroundColor
       ?? appBarTheme.foregroundColor
-      ?? (colorScheme.brightness == Brightness.dark ? colorScheme.onSurface : colorScheme.onPrimary);
+      ?? defaults.foregroundColor!;
+
+    final double elevation = widget.elevation
+      ?? appBarTheme.elevation
+      ?? defaults.elevation!;
+
+    final double effectiveElevation = states.contains(MaterialState.scrolledUnder)
+      ? widget.scrolledUnderElevation
+        ?? appBarTheme.scrolledUnderElevation
+        ?? defaults.scrolledUnderElevation
+        ?? elevation
+      : elevation;
 
     IconThemeData overallIconTheme = backwardsCompatibility
       ? widget.iconTheme
@@ -835,10 +888,13 @@
         ?? theme.primaryIconTheme
       : widget.iconTheme
         ?? appBarTheme.iconTheme
-        ?? theme.iconTheme.copyWith(color: foregroundColor);
+        ?? defaults.iconTheme!.copyWith(color: foregroundColor);
 
     IconThemeData actionsIconTheme = widget.actionsIconTheme
       ?? appBarTheme.actionsIconTheme
+      ?? widget.iconTheme
+      ?? appBarTheme.iconTheme
+      ?? defaults.actionsIconTheme?.copyWith(color: foregroundColor)
       ?? overallIconTheme;
 
     TextStyle? toolbarTextStyle = backwardsCompatibility
@@ -847,7 +903,7 @@
         ?? theme.primaryTextTheme.bodyText2
       : widget.toolbarTextStyle
         ?? appBarTheme.toolbarTextStyle
-        ?? theme.textTheme.bodyText2?.copyWith(color: foregroundColor);
+        ?? defaults.toolbarTextStyle?.copyWith(color: foregroundColor);
 
     TextStyle? titleTextStyle = backwardsCompatibility
       ? widget.textTheme?.headline6
@@ -855,7 +911,7 @@
         ?? theme.primaryTextTheme.headline6
       : widget.titleTextStyle
         ?? appBarTheme.titleTextStyle
-        ?? theme.textTheme.headline6?.copyWith(color: foregroundColor);
+        ?? defaults.titleTextStyle?.copyWith(color: foregroundColor);
 
     if (widget.toolbarOpacity != 1.0) {
       final double opacity = const Interval(0.25, 1.0, curve: Curves.fastOutSlowIn).transform(widget.toolbarOpacity);
@@ -1051,6 +1107,7 @@
         )
       : widget.systemOverlayStyle
         ?? appBarTheme.systemOverlayStyle
+        ?? defaults.systemOverlayStyle
         ?? _systemOverlayStyleForBrightness(ThemeData.estimateBrightnessForColor(backgroundColor));
 
     return Semantics(
@@ -1059,13 +1116,14 @@
         value: overlayStyle,
         child: Material(
           color: backgroundColor,
-          elevation: widget.elevation
-            ?? appBarTheme.elevation
-            ?? _defaultElevation,
+          elevation: effectiveElevation,
           shadowColor: widget.shadowColor
             ?? appBarTheme.shadowColor
-            ?? _defaultShadowColor,
-          shape: widget.shape ?? appBarTheme.shape,
+            ?? defaults.shadowColor,
+          surfaceTintColor: widget.surfaceTintColor
+            ?? appBarTheme.surfaceTintColor
+            ?? defaults.surfaceTintColor,
+          shape: widget.shape ?? appBarTheme.shape ?? defaults.shape,
           child: Semantics(
             explicitChildNodes: true,
             child: appBar,
@@ -1085,7 +1143,9 @@
     required this.flexibleSpace,
     required this.bottom,
     required this.elevation,
+    required this.scrolledUnderElevation,
     required this.shadowColor,
+    required this.surfaceTintColor,
     required this.forceElevated,
     required this.backgroundColor,
     required this.foregroundColor,
@@ -1127,7 +1187,9 @@
   final Widget? flexibleSpace;
   final PreferredSizeWidget? bottom;
   final double? elevation;
+  final double? scrolledUnderElevation;
   final Color? shadowColor;
+  final Color? surfaceTintColor;
   final bool forceElevated;
   final Color? backgroundColor;
   final Color? foregroundColor;
@@ -1202,7 +1264,9 @@
           : flexibleSpace,
         bottom: bottom,
         elevation: forceElevated || isScrolledUnder ? elevation : 0.0,
+        scrolledUnderElevation: scrolledUnderElevation,
         shadowColor: shadowColor,
+        surfaceTintColor: surfaceTintColor,
         backgroundColor: backgroundColor,
         foregroundColor: foregroundColor,
         brightness: brightness,
@@ -1369,7 +1433,9 @@
     this.flexibleSpace,
     this.bottom,
     this.elevation,
+    this.scrolledUnderElevation,
     this.shadowColor,
+    this.surfaceTintColor,
     this.forceElevated = false,
     this.backgroundColor,
     this.foregroundColor,
@@ -1456,11 +1522,21 @@
   /// This property is used to configure an [AppBar].
   final double? elevation;
 
+  /// {@macro flutter.material.appbar.scrolledUnderElevation}
+  ///
+  /// This property is used to configure an [AppBar].
+  final double? scrolledUnderElevation;
+
   /// {@macro flutter.material.appbar.shadowColor}
   ///
   /// This property is used to configure an [AppBar].
   final Color? shadowColor;
 
+  /// {@macro flutter.material.appbar.surfaceTintColor}
+  ///
+  /// This property is used to configure an [AppBar].
+  final Color? surfaceTintColor;
+
   /// Whether to show the shadow appropriate for the [elevation] even if the
   /// content is not scrolled under the [AppBar].
   ///
@@ -1762,7 +1838,9 @@
           flexibleSpace: widget.flexibleSpace,
           bottom: widget.bottom,
           elevation: widget.elevation,
+          scrolledUnderElevation: widget.scrolledUnderElevation,
           shadowColor: widget.shadowColor,
+          surfaceTintColor: widget.surfaceTintColor,
           forceElevated: widget.forceElevated,
           backgroundColor: widget.backgroundColor,
           foregroundColor: widget.foregroundColor,
@@ -1835,3 +1913,82 @@
     alignChild();
   }
 }
+
+class _DefaultsM2 extends AppBarTheme {
+  _DefaultsM2(this.context)
+    : super(
+      elevation: 4.0,
+      shadowColor: const Color(0xFF000000),
+      titleSpacing: NavigationToolbar.kMiddleSpacing,
+      toolbarHeight: kToolbarHeight,
+    );
+
+  final BuildContext context;
+  late final ThemeData _theme = Theme.of(context);
+  late final ColorScheme _colors = _theme.colorScheme;
+
+  @override
+  Color? get backgroundColor => _colors.brightness == Brightness.dark ? _colors.surface : _colors.primary;
+
+  @override
+  Color? get foregroundColor => _colors.brightness == Brightness.dark ? _colors.onSurface : _colors.onPrimary;
+
+  @override
+  IconThemeData? get iconTheme => _theme.iconTheme;
+
+  @override
+  TextStyle? get toolbarTextStyle => _theme.textTheme.bodyText2;
+
+  @override
+  TextStyle? get titleTextStyle => _theme.textTheme.headline6;
+}
+
+// BEGIN GENERATED TOKEN PROPERTIES
+
+// Generated code to the end of this file. Do not edit by hand.
+// These defaults are generated from the Material Design Token
+// database by the script dev/tools/gen_defaults/bin/gen_defaults.dart.
+
+// Generated version v0_92
+class _TokenDefaultsM3 extends AppBarTheme {
+  _TokenDefaultsM3(this.context)
+    : super(
+      elevation: 0.0,
+      scrolledUnderElevation: 3.0,
+      titleSpacing: NavigationToolbar.kMiddleSpacing,
+      toolbarHeight: 64.0,
+    );
+
+  final BuildContext context;
+  late final ThemeData _theme = Theme.of(context);
+  late final ColorScheme _colors = _theme.colorScheme;
+  late final TextTheme _textTheme = _theme.textTheme;
+
+  @override
+  Color? get backgroundColor => _colors.surface;
+
+  @override
+  Color? get foregroundColor => _colors.onSurface;
+
+  @override
+  Color? get surfaceTintColor => _colors.surfaceTint;
+
+  @override
+  IconThemeData? get iconTheme => IconThemeData(
+    color: _colors.onSurface,
+    size: 24.0,
+  );
+
+  @override
+  IconThemeData? get actionsIconTheme => IconThemeData(
+    color: _colors.onSurfaceVariant,
+    size: 24.0,
+  );
+
+  @override
+  TextStyle? get toolbarTextStyle => _textTheme.bodyText2;
+
+  @override
+  TextStyle? get titleTextStyle => _textTheme.titleLarge;
+}
+// END GENERATED TOKEN PROPERTIES
diff --git a/packages/flutter/lib/src/material/app_bar_theme.dart b/packages/flutter/lib/src/material/app_bar_theme.dart
index 583e1bd..5eb0ff4 100644
--- a/packages/flutter/lib/src/material/app_bar_theme.dart
+++ b/packages/flutter/lib/src/material/app_bar_theme.dart
@@ -37,7 +37,9 @@
     Color? backgroundColor,
     this.foregroundColor,
     this.elevation,
+    this.scrolledUnderElevation,
     this.shadowColor,
+    this.surfaceTintColor,
     this.shape,
     this.iconTheme,
     this.actionsIconTheme,
@@ -121,10 +123,18 @@
   /// descendant [AppBar] widgets.
   final double? elevation;
 
+  /// Overrides the default value of [AppBar.scrolledUnderElevation] in all
+  /// descendant [AppBar] widgets.
+  final double? scrolledUnderElevation;
+
   /// Overrides the default value for [AppBar.shadowColor] in all
   /// descendant widgets.
   final Color? shadowColor;
 
+  /// Overrides the default value for [AppBar.surfaceTintColor] in all
+  /// descendant widgets.
+  final Color? surfaceTintColor;
+
   /// Overrides the default value for [AppBar.shape] in all
   /// descendant widgets.
   final ShapeBorder? shape;
@@ -237,7 +247,9 @@
     Color? backgroundColor,
     Color? foregroundColor,
     double? elevation,
+    double? scrolledUnderElevation,
     Color? shadowColor,
+    Color? surfaceTintColor,
     ShapeBorder? shape,
     IconThemeData? iconTheme,
     @Deprecated(
@@ -266,7 +278,9 @@
       backgroundColor: backgroundColor ?? color ?? this.backgroundColor,
       foregroundColor: foregroundColor ?? this.foregroundColor,
       elevation: elevation ?? this.elevation,
+      scrolledUnderElevation: scrolledUnderElevation ?? this.scrolledUnderElevation,
       shadowColor: shadowColor ?? this.shadowColor,
+      surfaceTintColor: surfaceTintColor ?? this.surfaceTintColor,
       shape: shape ?? this.shape,
       iconTheme: iconTheme ?? this.iconTheme,
       actionsIconTheme: actionsIconTheme ?? this.actionsIconTheme,
@@ -298,7 +312,9 @@
       backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t),
       foregroundColor: Color.lerp(a?.foregroundColor, b?.foregroundColor, t),
       elevation: lerpDouble(a?.elevation, b?.elevation, t),
+      scrolledUnderElevation: lerpDouble(a?.scrolledUnderElevation, b?.scrolledUnderElevation, t),
       shadowColor: Color.lerp(a?.shadowColor, b?.shadowColor, t),
+      surfaceTintColor: Color.lerp(a?.surfaceTintColor, b?.surfaceTintColor, t),
       shape: ShapeBorder.lerp(a?.shape, b?.shape, t),
       iconTheme: IconThemeData.lerp(a?.iconTheme, b?.iconTheme, t),
       actionsIconTheme: IconThemeData.lerp(a?.actionsIconTheme, b?.actionsIconTheme, t),
@@ -319,7 +335,9 @@
     backgroundColor,
     foregroundColor,
     elevation,
+    scrolledUnderElevation,
     shadowColor,
+    surfaceTintColor,
     shape,
     iconTheme,
     actionsIconTheme,
@@ -344,7 +362,9 @@
         && other.backgroundColor == backgroundColor
         && other.foregroundColor == foregroundColor
         && other.elevation == elevation
+        && other.scrolledUnderElevation == scrolledUnderElevation
         && other.shadowColor == shadowColor
+        && other.surfaceTintColor == surfaceTintColor
         && other.shape == shape
         && other.iconTheme == iconTheme
         && other.actionsIconTheme == actionsIconTheme
@@ -365,7 +385,9 @@
     properties.add(ColorProperty('backgroundColor', backgroundColor, defaultValue: null));
     properties.add(ColorProperty('foregroundColor', foregroundColor, defaultValue: null));
     properties.add(DiagnosticsProperty<double>('elevation', elevation, defaultValue: null));
+    properties.add(DiagnosticsProperty<double>('scrolledUnderElevation', scrolledUnderElevation, defaultValue: null));
     properties.add(ColorProperty('shadowColor', shadowColor, defaultValue: null));
+    properties.add(ColorProperty('surfaceTintColor', surfaceTintColor, defaultValue: null));
     properties.add(DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: null));
     properties.add(DiagnosticsProperty<IconThemeData>('iconTheme', iconTheme, defaultValue: null));
     properties.add(DiagnosticsProperty<IconThemeData>('actionsIconTheme', actionsIconTheme, defaultValue: null));
diff --git a/packages/flutter/lib/src/material/material.dart b/packages/flutter/lib/src/material/material.dart
index 8b21086..1e0c1dc 100644
--- a/packages/flutter/lib/src/material/material.dart
+++ b/packages/flutter/lib/src/material/material.dart
@@ -401,6 +401,9 @@
   Widget build(BuildContext context) {
     final ThemeData theme = Theme.of(context);
     final Color? backgroundColor = _getBackgroundColor(context);
+    final Color? modelShadowColor = widget.shadowColor ?? (theme.useMaterial3 ? null : theme.shadowColor);
+    // If no shadow color is specified, use 0 for elevation in the model so a drop shadow won't be painted.
+    final double modelElevation = modelShadowColor != null ? widget.elevation : 0;
     assert(
       backgroundColor != null || widget.type == MaterialType.transparency,
       'If Material type is not MaterialType.transparency, a color must '
@@ -450,9 +453,9 @@
         duration: widget.animationDuration,
         shape: BoxShape.rectangle,
         clipBehavior: widget.clipBehavior,
-        elevation: widget.elevation,
+        elevation: modelElevation,
         color: color,
-        shadowColor: widget.shadowColor ?? (theme.useMaterial3 ? const Color(0x00000000) : theme.shadowColor),
+        shadowColor: modelShadowColor ?? const Color(0x00000000),
         animateColor: false,
         child: contents,
       );
@@ -477,7 +480,7 @@
       clipBehavior: widget.clipBehavior,
       elevation: widget.elevation,
       color: backgroundColor!,
-      shadowColor: widget.shadowColor ?? (theme.useMaterial3 ? const Color(0x00000000) : theme.shadowColor),
+      shadowColor: modelShadowColor,
       surfaceTintColor: widget.surfaceTintColor,
       child: contents,
     );
@@ -745,7 +748,6 @@
        assert(clipBehavior != null),
        assert(elevation != null && elevation >= 0.0),
        assert(color != null),
-       assert(shadowColor != null),
        super(key: key, curve: curve, duration: duration);
 
   /// The widget below this widget in the tree.
@@ -780,7 +782,7 @@
   final Color color;
 
   /// The target shadow color.
-  final Color shadowColor;
+  final Color? shadowColor;
 
   /// The target surface tint color.
   final Color? surfaceTintColor;
@@ -811,11 +813,13 @@
       widget.elevation,
       (dynamic value) => Tween<double>(begin: value as double),
     ) as Tween<double>?;
-    _shadowColor = visitor(
-      _shadowColor,
-      widget.shadowColor,
-      (dynamic value) => ColorTween(begin: value as Color),
-    ) as ColorTween?;
+    _shadowColor =  widget.shadowColor != null
+      ? visitor(
+          _shadowColor,
+          widget.shadowColor,
+          (dynamic value) => ColorTween(begin: value as Color),
+        ) as ColorTween?
+      : null;
     _surfaceTintColor = widget.surfaceTintColor != null
       ? visitor(
           _surfaceTintColor,
@@ -837,15 +841,18 @@
     final Color color = Theme.of(context).useMaterial3
       ? ElevationOverlay.applySurfaceTint(widget.color, _surfaceTintColor?.evaluate(animation), elevation)
       : ElevationOverlay.applyOverlay(context, widget.color, elevation);
+    // If no shadow color is specified, use 0 for elevation in the model so a drop shadow won't be painted.
+    final double modelElevation = widget.shadowColor != null ? elevation : 0;
+    final Color shadowColor = _shadowColor?.evaluate(animation) ?? const Color(0x00000000);
     return PhysicalShape(
       clipper: ShapeBorderClipper(
         shape: shape,
         textDirection: Directionality.maybeOf(context),
       ),
       clipBehavior: widget.clipBehavior,
-      elevation: elevation,
+      elevation: modelElevation,
       color: color,
-      shadowColor: _shadowColor!.evaluate(animation)!,
+      shadowColor: shadowColor,
       child: _ShapeBorderPaint(
         shape: shape,
         borderOnForeground: widget.borderOnForeground,
diff --git a/packages/flutter/lib/src/material/theme_data.dart b/packages/flutter/lib/src/material/theme_data.dart
index 95b445a..9f66b1a 100644
--- a/packages/flutter/lib/src/material/theme_data.dart
+++ b/packages/flutter/lib/src/material/theme_data.dart
@@ -1257,6 +1257,7 @@
   /// Components that have been migrated to Material 3 are:
   ///
   ///   * [AlertDialog]
+  ///   * [AppBar]
   ///   * [Card]
   ///   * [Dialog]
   ///   * [ElevatedButton]
diff --git a/packages/flutter/lib/src/rendering/binding.dart b/packages/flutter/lib/src/rendering/binding.dart
index 7a33811..b3431e7 100644
--- a/packages/flutter/lib/src/rendering/binding.dart
+++ b/packages/flutter/lib/src/rendering/binding.dart
@@ -235,10 +235,13 @@
   ///
   /// See [dart:ui.PlatformDispatcher.onMetricsChanged].
   @protected
+  @visibleForTesting
   void handleMetricsChanged() {
     assert(renderView != null);
     renderView.configuration = createViewConfiguration();
-    scheduleForcedFrame();
+    if (renderView.child != null) {
+      scheduleForcedFrame();
+    }
   }
 
   /// Called when the platform text scale factor changes.
diff --git a/packages/flutter/test/material/app_bar_test.dart b/packages/flutter/test/material/app_bar_test.dart
index 3f78736..7501f43 100644
--- a/packages/flutter/test/material/app_bar_test.dart
+++ b/packages/flutter/test/material/app_bar_test.dart
@@ -955,6 +955,8 @@
   });
 
   testWidgets('AppBar uses the specified elevation or defaults to 4.0', (WidgetTester tester) async {
+    final bool useMaterial3 = ThemeData().useMaterial3;
+
     Widget buildAppBar([double? elevation]) {
       return MaterialApp(
         home: Scaffold(
@@ -968,15 +970,48 @@
       matching: find.byType(Material),
     ));
 
-    // Default elevation should be _AppBarState._defaultElevation = 4.0
+    // Default elevation should be used for the material.
     await tester.pumpWidget(buildAppBar());
-    expect(getMaterial().elevation, 4.0);
+    expect(getMaterial().elevation, useMaterial3 ? 0 : 4);
 
     // AppBar should use the specified elevation.
     await tester.pumpWidget(buildAppBar(8.0));
     expect(getMaterial().elevation, 8.0);
   });
 
+  testWidgets('scrolledUnderElevation', (WidgetTester tester) async {
+    Widget buildAppBar({double? elevation, double? scrolledUnderElevation}) {
+      return MaterialApp(
+        home: Scaffold(
+          appBar: AppBar(
+            title: const Text('Title'),
+            elevation: elevation,
+            scrolledUnderElevation: scrolledUnderElevation,
+          ),
+          body: ListView.builder(
+            itemCount: 100,
+            itemBuilder: (BuildContext context, int index) => ListTile(title: Text('Item $index')),
+          ),
+        ),
+      );
+    }
+
+    Material getMaterial() => tester.widget<Material>(find.descendant(
+      of: find.byType(AppBar),
+      matching: find.byType(Material),
+    ));
+
+    await tester.pumpWidget(buildAppBar(elevation: 2, scrolledUnderElevation: 10));
+    // Starts with the base elevation.
+    expect(getMaterial().elevation, 2);
+
+    await tester.fling(find.text('Item 2'), const Offset(0.0, -600.0), 2000.0);
+    await tester.pumpAndSettle();
+
+    // After scrolling it should be the scrolledUnderElevation.
+    expect(getMaterial().elevation, 10);
+  });
+
   group('SliverAppBar elevation', () {
     Widget buildSliverAppBar(bool forceElevated, {double? elevation, double? themeElevation}) {
       return MaterialApp(
@@ -997,15 +1032,16 @@
       // Regression test for https://github.com/flutter/flutter/issues/59158.
       AppBar getAppBar() => tester.widget<AppBar>(find.byType(AppBar));
       Material getMaterial() => tester.widget<Material>(find.byType(Material));
+      final bool useMaterial3 = ThemeData().useMaterial3;
 
       // When forceElevated is off, SliverAppBar should not be elevated.
       await tester.pumpWidget(buildSliverAppBar(false));
       expect(getMaterial().elevation, 0.0);
 
-      // Default elevation should be _AppBarState._defaultElevation = 4.0, and
+      // Default elevation should be used by the material, but
       // the AppBar's elevation should not be specified by SliverAppBar.
       await tester.pumpWidget(buildSliverAppBar(true));
-      expect(getMaterial().elevation, 4.0);
+      expect(getMaterial().elevation, useMaterial3 ? 0.0 : 4.0);
       expect(getAppBar().elevation, null);
 
       // SliverAppBar should use the specified elevation.
@@ -1313,6 +1349,8 @@
     final Key key = UniqueKey();
     await tester.pumpWidget(
       MaterialApp(
+        // Test was designed against InkSplash so need to make sure that is used.
+        theme: ThemeData(splashFactory: InkSplash.splashFactory),
         home: Center(
           child: AppBar(
             title: const Text('Abc'),
@@ -2006,44 +2044,55 @@
     ));
   });
 
-  testWidgets('AppBar draws a light system bar for a light theme with a dark background', (WidgetTester tester) async {
-    final ThemeData lightTheme = ThemeData(primarySwatch: Colors.deepOrange);
-    await tester.pumpWidget(MaterialApp(
-      theme: lightTheme,
-      home: Scaffold(
-        appBar: AppBar(
-          title: const Text('test'),
-        ),
-      ),
-    ));
-
-    expect(lightTheme.primaryColorBrightness, Brightness.dark);
-    expect(lightTheme.colorScheme.brightness, Brightness.light);
-    expect(SystemChrome.latestStyle, const SystemUiOverlayStyle(
-      statusBarBrightness: Brightness.dark,
-      statusBarIconBrightness: Brightness.light,
-    ));
-  });
-
-  testWidgets('AppBar draws a dark system bar for a dark theme with a light background', (WidgetTester tester) async {
-    final ThemeData darkTheme = ThemeData(brightness: Brightness.dark, cardColor: Colors.white);
-    await tester.pumpWidget(
-      MaterialApp(
-        theme: darkTheme,
+  testWidgets('Default system bar brightness based on AppBar background color brightness.', (WidgetTester tester) async {
+    Widget buildAppBar(ThemeData theme) {
+      return MaterialApp(
+        theme: theme,
         home: Scaffold(
-          appBar: AppBar(
-            title: const Text('test'),
-          ),
+          appBar: AppBar(title: const Text('Title')),
         ),
-      ),
-    );
+      );
+    }
 
-    expect(darkTheme.primaryColorBrightness, Brightness.dark);
-    expect(darkTheme.colorScheme.brightness, Brightness.dark);
-    expect(SystemChrome.latestStyle, const SystemUiOverlayStyle(
-      statusBarBrightness: Brightness.light,
-      statusBarIconBrightness: Brightness.dark,
-    ));
+    // Using a light theme.
+    {
+      await tester.pumpWidget(buildAppBar(ThemeData.from(colorScheme: const ColorScheme.light())));
+      final Material appBarMaterial = tester.widget<Material>(
+        find.descendant(
+          of: find.byType(AppBar),
+          matching: find.byType(Material),
+        ),
+      );
+      final Brightness appBarBrightness = ThemeData.estimateBrightnessForColor(appBarMaterial.color!);
+      final Brightness onAppBarBrightness = appBarBrightness == Brightness.light
+        ? Brightness.dark
+        : Brightness.light;
+
+      expect(SystemChrome.latestStyle, SystemUiOverlayStyle(
+        statusBarBrightness: appBarBrightness,
+        statusBarIconBrightness: onAppBarBrightness,
+      ));
+    }
+
+    // Using a dark theme.
+    {
+      await tester.pumpWidget(buildAppBar(ThemeData.from(colorScheme: const ColorScheme.dark())));
+      final Material appBarMaterial = tester.widget<Material>(
+        find.descendant(
+          of: find.byType(AppBar),
+          matching: find.byType(Material),
+        ),
+      );
+      final Brightness appBarBrightness = ThemeData.estimateBrightnessForColor(appBarMaterial.color!);
+      final Brightness onAppBarBrightness = appBarBrightness == Brightness.light
+          ? Brightness.dark
+          : Brightness.light;
+
+      expect(SystemChrome.latestStyle, SystemUiOverlayStyle(
+        statusBarBrightness: appBarBrightness,
+        statusBarIconBrightness: onAppBarBrightness,
+      ));
+    }
   });
 
   testWidgets('Changing SliverAppBar snap from true to false', (WidgetTester tester) async {
@@ -2208,6 +2257,8 @@
 
     Widget buildFrame() {
       return MaterialApp(
+        // Test designed against 2014 font sizes.
+        theme: ThemeData(textTheme: Typography.englishLike2014),
         home: Builder(
           builder: (BuildContext context) {
             return MediaQuery(
@@ -2246,6 +2297,8 @@
 
     Widget buildFrame() {
       return MaterialApp(
+        // Test designed against 2014 font sizes.
+        theme: ThemeData(textTheme: Typography.englishLike2014),
         home: Builder(
           builder: (BuildContext context) {
             return Directionality(
@@ -2537,6 +2590,7 @@
     await tester.pumpWidget(
       MaterialApp(
         theme: ThemeData.light().copyWith(
+          useMaterial3: false,
           appBarTheme: const AppBarTheme(
             backwardsCompatibility: false,
           ),
diff --git a/packages/flutter/test/material/app_bar_theme_test.dart b/packages/flutter/test/material/app_bar_theme_test.dart
index 171dbe5..856ccd2 100644
--- a/packages/flutter/test/material/app_bar_theme_test.dart
+++ b/packages/flutter/test/material/app_bar_theme_test.dart
@@ -15,8 +15,10 @@
   });
 
   testWidgets('Passing no AppBarTheme returns defaults', (WidgetTester tester) async {
+    final ThemeData theme = ThemeData();
     await tester.pumpWidget(
       MaterialApp(
+        theme: theme,
         home: Scaffold(
           appBar: AppBar(
             actions: <Widget>[
@@ -33,17 +35,33 @@
     final RichText actionIconText = _getAppBarIconRichText(tester);
     final DefaultTextStyle text = _getAppBarText(tester);
 
-    expect(SystemChrome.latestStyle!.statusBarBrightness, SystemUiOverlayStyle.light.statusBarBrightness);
-    expect(widget.color, Colors.blue);
-    expect(widget.elevation, 4.0);
-    expect(widget.shadowColor, Colors.black);
-    expect(widget.shape, null);
-    expect(iconTheme.data, const IconThemeData(color: Colors.white));
-    expect(actionsIconTheme.data, const IconThemeData(color: Colors.white));
-    expect(actionIconText.text.style!.color, Colors.white);
-    expect(text.style, Typography.material2014().englishLike.bodyText2!.merge(Typography.material2014().white.bodyText2));
-    expect(tester.getSize(find.byType(AppBar)).height, kToolbarHeight);
-    expect(tester.getSize(find.byType(AppBar)).width, 800);
+    if (theme.useMaterial3) {
+      expect(SystemChrome.latestStyle!.statusBarBrightness, Brightness.light);
+      expect(widget.color, theme.colorScheme.surface);
+      expect(widget.elevation, 0);
+      expect(widget.shadowColor, null);
+      expect(widget.surfaceTintColor, theme.colorScheme.surfaceTint);
+      expect(widget.shape, null);
+      expect(iconTheme.data, IconThemeData(color: theme.colorScheme.onSurface, size: 24));
+      expect(actionsIconTheme.data, IconThemeData(color: theme.colorScheme.onSurfaceVariant, size: 24));
+      expect(actionIconText.text.style!.color, Colors.black);
+      expect(text.style, Typography.material2021().englishLike.bodyText2!.merge(Typography.material2021().black.bodyText2).copyWith(color: theme.colorScheme.onSurface));
+      expect(tester.getSize(find.byType(AppBar)).height, kToolbarHeight);
+      expect(tester.getSize(find.byType(AppBar)).width, 800);
+    } else {
+      expect(SystemChrome.latestStyle!.statusBarBrightness, SystemUiOverlayStyle.light.statusBarBrightness);
+      expect(widget.color, Colors.blue);
+      expect(widget.elevation, 4.0);
+      expect(widget.shadowColor, Colors.black);
+      expect(widget.surfaceTintColor, null);
+      expect(widget.shape, null);
+      expect(iconTheme.data, const IconThemeData(color: Colors.white));
+      expect(actionsIconTheme.data, const IconThemeData(color: Colors.white));
+      expect(actionIconText.text.style!.color, Colors.white);
+      expect(text.style, Typography.material2014().englishLike.bodyText2!.merge(Typography.material2014().white.bodyText2));
+      expect(tester.getSize(find.byType(AppBar)).height, kToolbarHeight);
+      expect(tester.getSize(find.byType(AppBar)).width, 800);
+    }
   });
 
   testWidgets('AppBar uses values from AppBarTheme', (WidgetTester tester) async {
@@ -73,6 +91,7 @@
     expect(widget.color, appBarTheme.backgroundColor);
     expect(widget.elevation, appBarTheme.elevation);
     expect(widget.shadowColor, appBarTheme.shadowColor);
+    expect(widget.surfaceTintColor, appBarTheme.surfaceTintColor);
     expect(widget.shape, const StadiumBorder());
     expect(iconTheme.data, appBarTheme.iconTheme);
     expect(actionsIconTheme.data, appBarTheme.actionsIconTheme);
@@ -132,7 +151,8 @@
     const SystemUiOverlayStyle systemOverlayStyle = SystemUiOverlayStyle.light;
     const Color color = Colors.orange;
     const double elevation = 3.0;
-    const Color shadowColor = Colors.red;
+    const Color shadowColor = Colors.purple;
+    const Color surfaceTintColor = Colors.brown;
     const ShapeBorder shape = RoundedRectangleBorder();
     const IconThemeData iconThemeData = IconThemeData(color: Colors.green);
     const IconThemeData actionsIconThemeData = IconThemeData(color: Colors.lightBlue);
@@ -151,6 +171,7 @@
             systemOverlayStyle: systemOverlayStyle,
             elevation: elevation,
             shadowColor: shadowColor,
+            surfaceTintColor: surfaceTintColor,
             shape: shape,
             iconTheme: iconThemeData,
             actionsIconTheme: actionsIconThemeData,
@@ -174,6 +195,7 @@
     expect(widget.color, color);
     expect(widget.elevation, elevation);
     expect(widget.shadowColor, shadowColor);
+    expect(widget.surfaceTintColor, surfaceTintColor);
     expect(widget.shape, shape);
     expect(iconTheme.data, iconThemeData);
     expect(actionsIconTheme.data, actionsIconThemeData);
@@ -228,6 +250,7 @@
     expect(widget.color, appBarTheme.backgroundColor);
     expect(widget.elevation, appBarTheme.elevation);
     expect(widget.shadowColor, appBarTheme.shadowColor);
+    expect(widget.surfaceTintColor, appBarTheme.surfaceTintColor);
     expect(iconTheme.data, appBarTheme.iconTheme);
     expect(actionsIconTheme.data, appBarTheme.actionsIconTheme);
     expect(actionIconText.text.style!.color, appBarTheme.actionsIconTheme!.color);
@@ -235,15 +258,13 @@
   });
 
   testWidgets('ThemeData colorScheme is used when no AppBarTheme is set', (WidgetTester tester) async {
-    late ThemeData theme;
+    final ThemeData lightTheme = ThemeData.from(colorScheme: const ColorScheme.light());
+    final ThemeData darkTheme = ThemeData.from(colorScheme: const ColorScheme.dark());
     Widget buildFrame(ThemeData appTheme) {
       return MaterialApp(
         theme: appTheme,
         home: Builder(
           builder: (BuildContext context) {
-            // This ThemeData has been localized with ThemeData.localize. The
-            // appTheme parameter has not, so its textTheme is incomplete.
-            theme = Theme.of(context);
             return Scaffold(
               appBar: AppBar(
                 actions: <Widget>[
@@ -256,57 +277,120 @@
       );
     }
 
-    // AppBar defaults for light themes:
-    // - elevation: 4
-    // - shadow color: black
-    // - background color: ColorScheme.primary
-    // - foreground color: ColorScheme.onPrimary
-    // - actions text: style bodyText2, foreground color
-    // - status bar brightness: light (based on color scheme brightness)
-    {
-      await tester.pumpWidget(buildFrame(ThemeData.from(colorScheme: const ColorScheme.light())));
+    if (lightTheme.useMaterial3) {
+      // M3 AppBar defaults for light themes:
+      // - elevation: 0
+      // - shadow color: null
+      // - surface tint color: ColorScheme.surfaceTint
+      // - background color: ColorScheme.surface
+      // - foreground color: ColorScheme.onSurface
+      // - actions text: style bodyText2, foreground color
+      // - status bar brightness: light (based on color scheme brightness)
+      {
+        await tester.pumpWidget(buildFrame(lightTheme));
 
-      final Material widget = _getAppBarMaterial(tester);
-      final IconTheme iconTheme = _getAppBarIconTheme(tester);
-      final IconTheme actionsIconTheme = _getAppBarActionsIconTheme(tester);
-      final RichText actionIconText = _getAppBarIconRichText(tester);
-      final DefaultTextStyle text = _getAppBarText(tester);
+        final Material widget = _getAppBarMaterial(tester);
+        final IconTheme iconTheme = _getAppBarIconTheme(tester);
+        final IconTheme actionsIconTheme = _getAppBarActionsIconTheme(tester);
+        final RichText actionIconText = _getAppBarIconRichText(tester);
+        final DefaultTextStyle text = _getAppBarText(tester);
 
-      expect(SystemChrome.latestStyle!.statusBarBrightness, SystemUiOverlayStyle.light.statusBarBrightness);
-      expect(widget.color, theme.colorScheme.primary);
-      expect(widget.elevation, 4.0);
-      expect(widget.shadowColor, Colors.black);
-      expect(iconTheme.data.color, theme.colorScheme.onPrimary);
-      expect(actionsIconTheme.data.color, theme.colorScheme.onPrimary);
-      expect(actionIconText.text.style!.color, theme.colorScheme.onPrimary);
-      expect(text.style.compareTo(theme.textTheme.bodyText2!.copyWith(color: theme.colorScheme.onPrimary)), RenderComparison.identical);
-    }
+        expect(SystemChrome.latestStyle!.statusBarBrightness, Brightness.light);
+        expect(widget.color, lightTheme.colorScheme.surface);
+        expect(widget.elevation, 0);
+        expect(widget.shadowColor, null);
+        expect(widget.surfaceTintColor, lightTheme.colorScheme.surfaceTint);
+        expect(iconTheme.data.color, lightTheme.colorScheme.onSurface);
+        expect(actionsIconTheme.data.color, lightTheme.colorScheme.onSurface);
+        expect(actionIconText.text.style!.color, lightTheme.colorScheme.onSurface);
+        expect(text.style, Typography.material2021().englishLike.bodyText2!.merge(Typography.material2021().black.bodyText2).copyWith(color: lightTheme.colorScheme.onSurface));
+      }
 
-    // AppBar defaults for dark themes:
-    // - elevation: 4
-    // - shadow color: black
-    // - background color: ColorScheme.surface
-    // - foreground color: ColorScheme.onSurface
-    // - actions text: style bodyText2, foreground color
-    // - status bar brightness: dark (based on background color)
-    {
-      await tester.pumpWidget(buildFrame(ThemeData.from(colorScheme: const ColorScheme.dark())));
-      await tester.pumpAndSettle(); // Theme change animation
+      // M3 AppBar defaults for dark themes:
+      // - elevation: 0
+      // - shadow color: null
+      // - surface tint color: ColorScheme.surfaceTint
+      // - background color: ColorScheme.surface
+      // - foreground color: ColorScheme.onSurface
+      // - actions text: style bodyText2, foreground color
+      // - status bar brightness: dark (based on background color)
+      {
+        await tester.pumpWidget(buildFrame(ThemeData.from(colorScheme: const ColorScheme.dark())));
+        await tester.pumpAndSettle(); // Theme change animation
 
-      final Material widget = _getAppBarMaterial(tester);
-      final IconTheme iconTheme = _getAppBarIconTheme(tester);
-      final IconTheme actionsIconTheme = _getAppBarActionsIconTheme(tester);
-      final RichText actionIconText = _getAppBarIconRichText(tester);
-      final DefaultTextStyle text = _getAppBarText(tester);
+        final Material widget = _getAppBarMaterial(tester);
+        final IconTheme iconTheme = _getAppBarIconTheme(tester);
+        final IconTheme actionsIconTheme = _getAppBarActionsIconTheme(tester);
+        final RichText actionIconText = _getAppBarIconRichText(tester);
+        final DefaultTextStyle text = _getAppBarText(tester);
 
-      expect(SystemChrome.latestStyle!.statusBarBrightness, SystemUiOverlayStyle.light.statusBarBrightness);
-      expect(widget.color, theme.colorScheme.surface);
-      expect(widget.elevation, 4.0);
-      expect(widget.shadowColor, Colors.black);
-      expect(iconTheme.data.color, theme.colorScheme.onSurface);
-      expect(actionsIconTheme.data.color, theme.colorScheme.onSurface);
-      expect(actionIconText.text.style!.color, theme.colorScheme.onSurface);
-      expect(text.style.compareTo(theme.textTheme.bodyText2!.copyWith(color: theme.colorScheme.onSurface)), RenderComparison.identical);
+        expect(SystemChrome.latestStyle!.statusBarBrightness, Brightness.dark);
+        expect(widget.color, darkTheme.colorScheme.surface);
+        expect(widget.elevation, 0);
+        expect(widget.shadowColor, null);
+        expect(widget.surfaceTintColor, darkTheme.colorScheme.surfaceTint);
+        expect(iconTheme.data.color, darkTheme.colorScheme.onSurface);
+        expect(actionsIconTheme.data.color, darkTheme.colorScheme.onSurface);
+        expect(actionIconText.text.style!.color, darkTheme.colorScheme.onSurface);
+        expect(text.style, Typography.material2021().englishLike.bodyText2!.merge(Typography.material2021().black.bodyText2).copyWith(color: darkTheme.colorScheme.onSurface));
+      }
+    } else {
+      // AppBar defaults for light themes:
+      // - elevation: 4
+      // - shadow color: black
+      // - surface tint color: null
+      // - background color: ColorScheme.primary
+      // - foreground color: ColorScheme.onPrimary
+      // - actions text: style bodyText2, foreground color
+      // - status bar brightness: light (based on color scheme brightness)
+      {
+        await tester.pumpWidget(buildFrame(lightTheme));
+
+        final Material widget = _getAppBarMaterial(tester);
+        final IconTheme iconTheme = _getAppBarIconTheme(tester);
+        final IconTheme actionsIconTheme = _getAppBarActionsIconTheme(tester);
+        final RichText actionIconText = _getAppBarIconRichText(tester);
+        final DefaultTextStyle text = _getAppBarText(tester);
+
+        expect(SystemChrome.latestStyle!.statusBarBrightness, SystemUiOverlayStyle.light.statusBarBrightness);
+        expect(widget.color, lightTheme.colorScheme.primary);
+        expect(widget.elevation, 4.0);
+        expect(widget.shadowColor, Colors.black);
+        expect(widget.surfaceTintColor, null);
+        expect(iconTheme.data.color, lightTheme.colorScheme.onPrimary);
+        expect(actionsIconTheme.data.color, lightTheme.colorScheme.onPrimary);
+        expect(actionIconText.text.style!.color, lightTheme.colorScheme.onPrimary);
+        expect(text.style, Typography.material2014().englishLike.bodyText2!.merge(Typography.material2014().black.bodyText2).copyWith(color: lightTheme.colorScheme.onPrimary));
+      }
+
+      // AppBar defaults for dark themes:
+      // - elevation: 4
+      // - shadow color: black
+      // - surface tint color: null
+      // - background color: ColorScheme.surface
+      // - foreground color: ColorScheme.onSurface
+      // - actions text: style bodyText2, foreground color
+      // - status bar brightness: dark (based on background color)
+      {
+        await tester.pumpWidget(buildFrame(darkTheme));
+        await tester.pumpAndSettle(); // Theme change animation
+
+        final Material widget = _getAppBarMaterial(tester);
+        final IconTheme iconTheme = _getAppBarIconTheme(tester);
+        final IconTheme actionsIconTheme = _getAppBarActionsIconTheme(tester);
+        final RichText actionIconText = _getAppBarIconRichText(tester);
+        final DefaultTextStyle text = _getAppBarText(tester);
+
+        expect(SystemChrome.latestStyle!.statusBarBrightness, SystemUiOverlayStyle.light.statusBarBrightness);
+        expect(widget.color, darkTheme.colorScheme.surface);
+        expect(widget.elevation, 4.0);
+        expect(widget.shadowColor, Colors.black);
+        expect(widget.surfaceTintColor, null);
+        expect(iconTheme.data.color, darkTheme.colorScheme.onSurface);
+        expect(actionsIconTheme.data.color, darkTheme.colorScheme.onSurface);
+        expect(actionIconText.text.style!.color, darkTheme.colorScheme.onSurface);
+        expect(text.style, Typography.material2014().englishLike.bodyText2!.merge(Typography.material2014().black.bodyText2).copyWith(color: darkTheme.colorScheme.onSurface));
+      }
     }
   });
 
@@ -315,7 +399,7 @@
 
     Widget buildFrame({ Color? appIconColor, Color? appBarIconColor }) {
       return MaterialApp(
-        theme: ThemeData.from(colorScheme: const ColorScheme.light()),
+        theme: ThemeData.from(useMaterial3: false, colorScheme: const ColorScheme.light()),
         home: IconTheme(
           data: IconThemeData(color: appIconColor),
           child: Builder(
@@ -408,6 +492,22 @@
     expect(appBar.shadowColor, Colors.yellow);
   });
 
+  testWidgets('AppBar.surfaceTintColor takes priority over AppBarTheme.surfaceTintColor', (WidgetTester tester) async {
+    await tester.pumpWidget(MaterialApp(
+      theme: ThemeData(appBarTheme: const AppBarTheme(surfaceTintColor: Colors.red)),
+      home: Scaffold(
+        appBar: AppBar(
+          title: const Text('Title'),
+          surfaceTintColor: Colors.yellow,
+        ),
+      ),
+    ));
+
+    final AppBar appBar = tester.widget(find.byType(AppBar));
+    // The AppBar.surfaceTintColor should be used instead of AppBarTheme.surfaceTintColor.
+    expect(appBar.surfaceTintColor, Colors.yellow);
+  });
+
   testWidgets('AppBar uses AppBarTheme.titleSpacing', (WidgetTester tester) async {
     const double kTitleSpacing = 10;
     await tester.pumpWidget(MaterialApp(
@@ -493,6 +593,7 @@
       backgroundColor: Color(0xff000001),
       elevation: 8.0,
       shadowColor: Color(0xff000002),
+      surfaceTintColor: Color(0xff000003),
       centerTitle: true,
       titleSpacing: 40.0,
     ).debugFillProperties(builder);
@@ -507,6 +608,7 @@
       'backgroundColor: Color(0xff000001)',
       'elevation: 8.0',
       'shadowColor: Color(0xff000002)',
+      'surfaceTintColor: Color(0xff000003)',
       'centerTitle: true',
       'titleSpacing: 40.0',
     ]);
@@ -524,6 +626,7 @@
   const Color backgroundColor = Colors.lightBlue;
   const double elevation = 6.0;
   const Color shadowColor = Colors.red;
+  const Color surfaceTintColor = Colors.green;
   const IconThemeData iconThemeData = IconThemeData(color: Colors.black);
   const IconThemeData actionsIconThemeData = IconThemeData(color: Colors.pink);
   return const AppBarTheme(
@@ -532,6 +635,7 @@
     backgroundColor: backgroundColor,
     elevation: elevation,
     shadowColor: shadowColor,
+    surfaceTintColor: surfaceTintColor,
     shape: StadiumBorder(),
     iconTheme: iconThemeData,
     toolbarHeight: 96,
diff --git a/packages/flutter/test/rendering/binding_test.dart b/packages/flutter/test/rendering/binding_test.dart
new file mode 100644
index 0000000..c10df12
--- /dev/null
+++ b/packages/flutter/test/rendering/binding_test.dart
@@ -0,0 +1,22 @@
+// Copyright 2014 The Flutter 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/rendering.dart';
+import 'package:flutter/scheduler.dart';
+import 'package:flutter/widgets.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+  WidgetsFlutterBinding.ensureInitialized();
+
+  test('handleMetricsChanged does not scheduleForcedFrame unless there is a child to the renderView', () async {
+    expect(SchedulerBinding.instance.hasScheduledFrame, false);
+    RendererBinding.instance.handleMetricsChanged();
+    expect(SchedulerBinding.instance.hasScheduledFrame, false);
+
+    RendererBinding.instance.renderView.child = RenderLimitedBox();
+    RendererBinding.instance.handleMetricsChanged();
+    expect(SchedulerBinding.instance.hasScheduledFrame, true);
+  });
+}
diff --git a/packages/flutter_tools/lib/src/web/flutter_js.dart b/packages/flutter_tools/lib/src/web/flutter_js.dart
index dfff827..af8950b 100644
--- a/packages/flutter_tools/lib/src/web/flutter_js.dart
+++ b/packages/flutter_tools/lib/src/web/flutter_js.dart
@@ -31,7 +31,7 @@
     // we support. In the meantime, we use the "revealing module" pattern.
 
     // Watchdog to prevent injecting the main entrypoint multiple times.
-    _scriptLoaded = false;
+    _scriptLoaded = null;
 
     // Resolver for the pending promise returned by loadEntrypoint.
     _didCreateEngineInitializerResolve = null;
@@ -61,31 +61,38 @@
         console.warn("Do not call didCreateEngineInitializer by hand. Start with loadEntrypoint instead.");
       }
       this._didCreateEngineInitializerResolve(engineInitializer);
+      // Remove this method after it's done, so Flutter Web can hot restart.
+      delete this.didCreateEngineInitializer;
     }).bind(this);
 
     _loadEntrypoint(entrypointUrl) {
-      if (this._scriptLoaded) {
-        return null;
+      if (!this._scriptLoaded) {
+        this._scriptLoaded = new Promise((resolve, reject) => {
+          let scriptTag = document.createElement("script");
+          scriptTag.src = entrypointUrl;
+          scriptTag.type = "application/javascript";
+          this._didCreateEngineInitializerResolve = resolve; // Cache the resolve, so it can be called from Flutter.
+          scriptTag.addEventListener("error", reject);
+          document.body.append(scriptTag);
+        });
       }
 
-      this._scriptLoaded = true;
-
-      return new Promise((resolve, reject) => {
-        let scriptTag = document.createElement("script");
-        scriptTag.src = entrypointUrl;
-        scriptTag.type = "application/javascript";
-        this._didCreateEngineInitializerResolve = resolve; // Cache the resolve, so it can be called from Flutter.
-        scriptTag.addEventListener("error", reject);
-        document.body.append(scriptTag);
-      });
+      return this._scriptLoaded;
     }
 
     _waitForServiceWorkerActivation(serviceWorker, entrypointUrl) {
-      if (!serviceWorker) return;
+      if (!serviceWorker || serviceWorker.state == "activated") {
+        if (!serviceWorker) {
+          console.warn("Cannot activate a null service worker. Falling back to plain <script> tag.");
+        } else {
+          console.debug("Service worker already active.");
+        }
+        return this._loadEntrypoint(entrypointUrl);
+      }
       return new Promise((resolve, _) => {
         serviceWorker.addEventListener("statechange", () => {
           if (serviceWorker.state == "activated") {
-            console.log("Installed new service worker.");
+            console.debug("Installed new service worker.");
             resolve(this._loadEntrypoint(entrypointUrl));
           }
         });
@@ -103,22 +110,26 @@
         timeoutMillis = 4000,
       } = serviceWorkerOptions;
 
-      var serviceWorkerUrl = "flutter_service_worker.js?v=" + serviceWorkerVersion;
+      let serviceWorkerUrl = "flutter_service_worker.js?v=" + serviceWorkerVersion;
       let loader = navigator.serviceWorker.register(serviceWorkerUrl)
           .then((reg) => {
             if (!reg.active && (reg.installing || reg.waiting)) {
               // No active web worker and we have installed or are installing
               // one for the first time. Simply wait for it to activate.
-              return this._waitForServiceWorkerActivation(reg.installing || reg.waiting, entrypointUrl);
+              let sw = reg.installing || reg.waiting;
+              return this._waitForServiceWorkerActivation(sw, entrypointUrl);
             } else if (!reg.active.scriptURL.endsWith(serviceWorkerVersion)) {
               // When the app updates the serviceWorkerVersion changes, so we
               // need to ask the service worker to update.
-              console.log("New service worker available.");
-              reg.update();
-              return this._waitForServiceWorkerActivation(reg.installing, entrypointUrl);
+              console.debug("New service worker available.");
+              return reg.update().then((reg) => {
+                console.debug("Service worker updated.");
+                let sw = reg.installing || reg.waiting || reg.active;
+                return this._waitForServiceWorkerActivation(sw, entrypointUrl);
+              });
             } else {
               // Existing service worker is still good.
-              console.log("Loading app from service worker.");
+              console.debug("Loading app from service worker.");
               return this._loadEntrypoint(entrypointUrl);
             }
           });